Compare commits
159 Commits
2.0.0-rc8
...
open-recen
Author | SHA1 | Date | |
---|---|---|---|
![]() |
750486a8f0 | ||
![]() |
a04527d3b8 | ||
![]() |
33ec67109b | ||
![]() |
4e590ab618 | ||
![]() |
026e80e7fc | ||
![]() |
fdf6f0f9c8 | ||
![]() |
0151e4c224 | ||
![]() |
e8b0ea4f2d | ||
![]() |
7c1ca04c75 | ||
![]() |
0ba88d5ab6 | ||
![]() |
96e229d803 | ||
![]() |
d07d83fdfe | ||
![]() |
5f82577bc1 | ||
![]() |
35fcfb89c1 | ||
![]() |
6e3fe08c4c | ||
![]() |
7f06b148f4 | ||
![]() |
bf303d1b2f | ||
![]() |
59ca91d805 | ||
![]() |
69bb0aa385 | ||
![]() |
565970e779 | ||
![]() |
fec3b1138b | ||
![]() |
dcc0c0aa5d | ||
![]() |
76673cb553 | ||
![]() |
8f95fd6ca6 | ||
![]() |
4907ef2a47 | ||
![]() |
9ae3402631 | ||
![]() |
d0dfc656e6 | ||
![]() |
df3a34eec6 | ||
![]() |
20cc34ca9d | ||
![]() |
1b7f86b231 | ||
![]() |
0d545bea0e | ||
![]() |
204d71b2dd | ||
![]() |
5cb9166c83 | ||
![]() |
7828cc11ac | ||
![]() |
34a7fdb733 | ||
![]() |
7c361cf2d1 | ||
![]() |
8beade0867 | ||
![]() |
3afc2d7e4b | ||
![]() |
d40401437a | ||
![]() |
10ac7fd50a | ||
![]() |
07962e81d4 | ||
![]() |
785775327b | ||
![]() |
80dfa5b7dd | ||
![]() |
40425d49e0 | ||
![]() |
0c87fa9877 | ||
![]() |
5b79320302 | ||
![]() |
1da2dfc349 | ||
![]() |
d7bbfc515d | ||
![]() |
0c22884729 | ||
![]() |
fc9107c084 | ||
![]() |
474d5e5975 | ||
![]() |
f7f644cf36 | ||
![]() |
b5f9aa0f15 | ||
![]() |
cc5cf3b165 | ||
![]() |
125bd64c91 | ||
![]() |
ca47e8a09a | ||
![]() |
52804a5b52 | ||
![]() |
3ec62642dd | ||
![]() |
1281ad1932 | ||
![]() |
de32bddc20 | ||
![]() |
79ea0fa9a6 | ||
![]() |
683219dc1c | ||
![]() |
d674ab9b73 | ||
![]() |
5be1f9d7fe | ||
![]() |
9e2b73a045 | ||
![]() |
75e00c2bae | ||
![]() |
989300f25d | ||
![]() |
5226636fed | ||
![]() |
8b3f3c69fc | ||
![]() |
a39ab47e70 | ||
![]() |
9cabd40429 | ||
![]() |
6e3681896c | ||
![]() |
8a1cabd2bc | ||
![]() |
7a3e6789d1 | ||
![]() |
92bc5ecf7b | ||
![]() |
aebec0f942 | ||
![]() |
54db9bbce8 | ||
![]() |
676eb2f588 | ||
![]() |
ce273adf77 | ||
![]() |
0b33b51700 | ||
![]() |
36ac47b975 | ||
![]() |
bf193b1cac | ||
![]() |
879aedeaa3 | ||
![]() |
d556ee95c0 | ||
![]() |
d93c9ba654 | ||
![]() |
8a0dc1be7e | ||
![]() |
564862e173 | ||
![]() |
d7f7010bb5 | ||
![]() |
e156dcc213 | ||
![]() |
27a2a6ca03 | ||
![]() |
581379f86f | ||
![]() |
b62f3dec84 | ||
![]() |
90d2950bdd | ||
![]() |
5b7d64c1c1 | ||
![]() |
55927ac3dd | ||
![]() |
40c93bc19a | ||
![]() |
59b8a2d6bb | ||
![]() |
124738d810 | ||
![]() |
19c0334a91 | ||
![]() |
f22be3c587 | ||
![]() |
9373a0bcaf | ||
![]() |
5087ff08f2 | ||
![]() |
71d5a1520a | ||
![]() |
ec160df25e | ||
![]() |
7fbf3dc656 | ||
![]() |
7680194feb | ||
![]() |
2fdb19ea75 | ||
![]() |
8610332afc | ||
![]() |
1f7c2eb52c | ||
![]() |
119dfa78d9 | ||
![]() |
337d22efbd | ||
![]() |
5ff9ce0028 | ||
![]() |
d4833affc6 | ||
![]() |
8ad10b5adf | ||
![]() |
fe31d15b9f | ||
![]() |
99664ee544 | ||
![]() |
57841b3c0a | ||
![]() |
ed41b25889 | ||
![]() |
4f27725b35 | ||
![]() |
73835eced3 | ||
![]() |
46fcc71dd8 | ||
![]() |
453a657172 | ||
![]() |
1514d014a9 | ||
![]() |
e4d9243486 | ||
![]() |
fb690c97e8 | ||
![]() |
a0038315da | ||
![]() |
aea550fe33 | ||
![]() |
813444408e | ||
![]() |
d8be8888ef | ||
![]() |
431c3bdf2b | ||
![]() |
c51b201362 | ||
![]() |
7fed8febf1 | ||
![]() |
f4a68e793e | ||
![]() |
7d961537eb | ||
![]() |
d7a2d83990 | ||
![]() |
a36524e02a | ||
![]() |
1073c3fc7d | ||
![]() |
69d7e8e96c | ||
![]() |
7f2b849963 | ||
![]() |
0ce065e496 | ||
![]() |
0b0958c20e | ||
![]() |
06acd7fcde | ||
![]() |
b1e00e6ff2 | ||
![]() |
ea42dc52fd | ||
![]() |
6586cb37a8 | ||
![]() |
9b7ab14253 | ||
![]() |
d6899af5e7 | ||
![]() |
087cab177b | ||
![]() |
5da558dfd9 | ||
![]() |
953859831c | ||
![]() |
a13a8771d1 | ||
![]() |
5499c25528 | ||
![]() |
1e469627b4 | ||
![]() |
34ef25c4e4 | ||
![]() |
d1aa446c89 | ||
![]() |
e454acba41 | ||
![]() |
75abb70bcd | ||
![]() |
7ba98a212c | ||
![]() |
6ae6ba5b3d |
@@ -17,7 +17,6 @@ module.exports = {
|
||||
'scripts/*',
|
||||
'electron/*',
|
||||
'electron-app/*',
|
||||
'browser-app/*',
|
||||
'plugins/*',
|
||||
'arduino-ide-extension/src/node/cli-protocol',
|
||||
],
|
||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -8,6 +8,12 @@ contact_links:
|
||||
- name: Support request
|
||||
url: https://forum.arduino.cc/
|
||||
about: We can help you out on the Arduino Forum!
|
||||
- name: Issue report guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/issues.md#issue-report-guide
|
||||
about: Learn about submitting issue reports to this repository.
|
||||
- name: Contributor guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
|
||||
about: Learn about contributing to this project.
|
||||
- name: Discuss development work on the project
|
||||
url: https://groups.google.com/a/arduino.cc/g/developers
|
||||
about: Arduino Developers Mailing List
|
||||
|
71
.github/workflows/build.yml
vendored
@@ -4,28 +4,52 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '!.github/workflows/build.yml'
|
||||
- '.vscode/**'
|
||||
- 'docs/**'
|
||||
- 'scripts/**'
|
||||
- 'static/**'
|
||||
- '*.md'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '!.github/workflows/build.yml'
|
||||
- '.vscode/**'
|
||||
- 'docs/**'
|
||||
- 'scripts/**'
|
||||
- 'static/**'
|
||||
- '*.md'
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
name: build (${{ matrix.config.os }})
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: windows-2019
|
||||
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX # Name of the secret that contains the certificate.
|
||||
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD # Name of the secret that contains the certificate password.
|
||||
certificate-extension: pfx # File extension for the certificate.
|
||||
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
|
||||
- os: macos-latest
|
||||
# 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
|
||||
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
|
||||
certificate-password-secret: KEYCHAIN_PASSWORD
|
||||
certificate-extension: p12
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 90
|
||||
|
||||
@@ -44,6 +68,17 @@ jobs:
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Taskfile
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Package
|
||||
shell: bash
|
||||
env:
|
||||
@@ -55,29 +90,20 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
|
||||
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
|
||||
run: |
|
||||
# See: https://www.electron.build/code-signing
|
||||
if [ $IS_FORK = true ]; then
|
||||
echo "Skipping the app signing: building from a fork."
|
||||
if [ $CAN_SIGN = false ]; then
|
||||
echo "Skipping the app signing: certificate not provided."
|
||||
else
|
||||
if [ "${{ runner.OS }}" = "macOS" ]; then
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
|
||||
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
|
||||
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
|
||||
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
|
||||
|
||||
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
|
||||
|
||||
elif [ "${{ runner.OS }}" = "Windows" ]; then
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
|
||||
npm config set msvs_version 2017 --global
|
||||
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
|
||||
|
||||
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
|
||||
fi
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
|
||||
echo "${{ secrets[matrix.config.certificate-secret] }}" | base64 --decode > "$CSC_LINK"
|
||||
export CSC_KEY_PASSWORD="${{ secrets[matrix.config.certificate-password-secret] }}"
|
||||
fi
|
||||
|
||||
if [ "${{ runner.OS }}" = "Windows" ]; then
|
||||
npm config set msvs_version 2017 --global
|
||||
fi
|
||||
npx node-gyp install
|
||||
yarn --cwd ./electron/packager/
|
||||
yarn --cwd ./electron/packager/ package
|
||||
@@ -190,7 +216,7 @@ jobs:
|
||||
|
||||
release:
|
||||
needs: changelog
|
||||
if: github.repository == 'arduino/arduino-ide' && startsWith(github.ref, 'refs/tags/')
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
@@ -215,6 +241,7 @@ jobs:
|
||||
body: ${{ needs.changelog.outputs.BODY }}
|
||||
|
||||
- name: Publish Release [S3]
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||
|
15
.github/workflows/check-i18n-task.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Check Internationalization
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
|
||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on:
|
||||
push:
|
||||
@@ -31,6 +35,17 @@ jobs:
|
||||
node-version: '14.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Taskfile
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
|
15
.github/workflows/i18n-nightly-push.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: i18n-nightly-push
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every day at 1AM
|
||||
@@ -18,6 +22,17 @@ jobs:
|
||||
node-version: '14.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
|
15
.github/workflows/i18n-weekly-pull.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: i18n-weekly-pull
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every monday at 2AM
|
||||
@@ -18,6 +22,17 @@ jobs:
|
||||
node-version: '14.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
|
13
.github/workflows/themes-weekly-pull.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
NODE_VERSION: 14.x
|
||||
|
||||
jobs:
|
||||
@@ -22,6 +24,17 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
|
32
.vscode/launch.json
vendored
@@ -20,7 +20,6 @@
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:../plugins",
|
||||
"--hosted-plugin-inspect=9339",
|
||||
"--nosplash",
|
||||
"--content-trace",
|
||||
"--open-devtools"
|
||||
],
|
||||
@@ -81,37 +80,6 @@
|
||||
"port": 9222,
|
||||
"webRoot": "${workspaceFolder}/electron-app"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Browser)",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:plugins"
|
||||
],
|
||||
"windows": {
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
30
.vscode/tasks.json
vendored
@@ -12,17 +12,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Start Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app start",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch IDE Extension",
|
||||
"type": "shell",
|
||||
@@ -34,17 +23,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch Electron App",
|
||||
"type": "shell",
|
||||
@@ -56,14 +34,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Browser]",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino IDE - Watch IDE Extension",
|
||||
"Arduino IDE - Watch Browser App"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Electron]",
|
||||
"type": "shell",
|
||||
|
154
BUILDING.md
@@ -1,153 +1,3 @@
|
||||
# Development
|
||||
|
||||
This page includes technical documentation for developers who want to build the IDE locally and contribute to the project.
|
||||
|
||||
## Architecture overview
|
||||
|
||||
The 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 one main process.
|
||||
|
||||
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
|
||||
|
||||
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 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 IDE, only the _backend_ allows OS interaction.
|
||||
|
||||
The _backend_ process is responsible for:
|
||||
- providing access to the filesystem,
|
||||
- communicating with the [Arduino CLI](https://github.com/arduino/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
|
||||
|
||||
|
||||
## 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 IDE locally.
|
||||
Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
|
||||
> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above.
|
||||
|
||||
Once you have all the tools installed, you can build the editor following these steps
|
||||
|
||||
1. Install the dependencies and build
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
2. Rebuild the dependencies
|
||||
```sh
|
||||
yarn rebuild:browser
|
||||
```
|
||||
|
||||
3. Rebuild the electron dependencies
|
||||
```sh
|
||||
yarn rebuild:electron
|
||||
```
|
||||
|
||||
4. Start the application
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Notes for Windows contributors
|
||||
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
|
||||
|
||||
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
|
||||
|
||||
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
|
||||
|
||||
### CI
|
||||
|
||||
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
|
||||
|
||||
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts for each operating system can be downloaded from the GitHub Actions page.
|
||||
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
|
||||
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
|
||||
- Create a local tag:
|
||||
```sh
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
## Notes for macOS contributors
|
||||
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
|
||||
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
|
||||
Due to this limitation, Mac users have two options for testing contributions from forks:
|
||||
|
||||
### The Safe approach (recommended)
|
||||
|
||||
Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test.
|
||||
|
||||
### The Risky approach
|
||||
|
||||
*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.*
|
||||
1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!).
|
||||
1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push.
|
||||
1. Re-enable Gatekeeper after tests are done, following the guide linked above.
|
||||
|
||||
### Creating a release
|
||||
|
||||
You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
|
||||
|
||||
- Make sure the `main` state represents what you want to release and you're on `main`.
|
||||
- Prepare a release-candidate build on a branch:
|
||||
```bash
|
||||
git branch 0.2.0-rc \
|
||||
&& git checkout 0.2.0-rc
|
||||
```
|
||||
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
|
||||
```bash
|
||||
yarn update:version 0.2.0
|
||||
```
|
||||
- This should generate multiple outgoing changes with the version update.
|
||||
- Commit your changes and push to the remote:
|
||||
```bash
|
||||
git add . \
|
||||
&& git commit -s -m "Updated versions to 0.2.0" \
|
||||
&& git push
|
||||
```
|
||||
- Create the GH PR the workflow starts automatically.
|
||||
- Once you're happy with the RC, merge the changes to the `main`.
|
||||
- Create a tag and push it:
|
||||
```bash
|
||||
git tag -a 0.2.0 -m "0.2.0" \
|
||||
&& git push origin 0.2.0
|
||||
```
|
||||
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
|
||||
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
|
||||
- If you do not like the generated changelog, modify it and update the GH release.
|
||||
|
||||
## FAQ
|
||||
|
||||
* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?*
|
||||
|
||||
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.
|
||||
|
||||
* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?*
|
||||
|
||||
[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 IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
|
||||
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
|
||||
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
|
||||
# Development Guide
|
||||
|
||||
This documentation has been moved [**here**](docs/development.md#development-guide).
|
||||
|
51
README.md
@@ -1,45 +1,18 @@
|
||||
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
|
||||
|
||||
# Arduino IDE 2.x (beta)
|
||||
# Arduino IDE 2.x
|
||||
|
||||
[](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
|
||||
|
||||
This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||
|
||||
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
|
||||
|
||||
> ⚠️ This is **beta** software. Help us test it!
|
||||
|
||||

|
||||
|
||||
## Download
|
||||
|
||||
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
|
||||
|
||||
### Nightly builds
|
||||
|
||||
These builds are generated every day at 03:00 GMT from the `main` branch and
|
||||
should be considered unstable:
|
||||
|
||||
| Platform | 32 bit | 64 bit |
|
||||
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| Linux | | [Nightly Linux AppImage 64 bit]<br />[Nightly Linux ZIP file 64 bit] |
|
||||
| Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
||||
| Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
|
||||
| macOS | | [Nightly macOS 64 bit] |
|
||||
|
||||
[🚧 work in progress...]: https://github.com/arduino/arduino-ide/issues/107
|
||||
[nightly linux appimage 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.AppImage
|
||||
[nightly linux zip file 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
|
||||
[nightly windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
|
||||
[nightly windows 64 bit msi]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
|
||||
[nightly windows 64 bit zip]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
|
||||
[nightly macos 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
|
||||
|
||||
> These links return an HTTP `302: Found` response, redirecting to latest
|
||||
> generated builds by replacing `latest` with the latest available build
|
||||
> date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
||||
> replaced with `20190806`)
|
||||
You can download the latest release version and nightly builds from the [software download page on the Arduino website](https://www.arduino.cc/en/software).
|
||||
|
||||
## Support
|
||||
|
||||
@@ -47,10 +20,9 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
|
||||
|
||||
## Bugs & Issues
|
||||
|
||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
|
||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository.
|
||||
|
||||
- Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
|
||||
- Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
|
||||
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
|
||||
|
||||
### Security
|
||||
|
||||
@@ -62,16 +34,15 @@ e-mail contact: security@arduino.cc
|
||||
|
||||
## Contributions and development
|
||||
|
||||
Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users.
|
||||
Contributions are very welcome! There are several ways to participate in this project, including:
|
||||
|
||||
This repository contains the main code, but two more repositories are included during the build process:
|
||||
- Fixing bugs
|
||||
- Beta testing
|
||||
- Translation
|
||||
|
||||
- [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
|
||||
- [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
|
||||
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
|
||||
|
||||
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
|
||||
|
||||
You can help with the translation of the Arduino IDE to your language here: [Arduino IDE on Transifex](https://www.transifex.com/arduino-1/ide2/dashboard/).
|
||||
See the [**development guide**](docs/development.md) for a technical overview of the application and instructions for building the code.
|
||||
|
||||
## Donations
|
||||
|
||||
|
@@ -62,6 +62,15 @@ The Config Service knows about your system, like for example the default sketch
|
||||
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
|
||||
`yarn --cwd arduino-ide-extension generate-protocol`
|
||||
|
||||
### Update **clangd** and **ClangFormat**
|
||||
|
||||
The [**clangd** C++ language server](https://clangd.llvm.org/) and the [**ClangFormat** code formatter](https://clang.llvm.org/docs/ClangFormat.html) tool dependencies are managed in parallel. Updating them to a different version is done by the following procedure:
|
||||
|
||||
1. If the target version is not already [available from the `arduino/clang-static-binaries` repository](https://github.com/arduino/clang-static-binaries/releases), submit [an issue there](https://github.com/arduino/clang-static-binaries/issues) requesting a build and wait for that to be completed.
|
||||
1. Validate the **ClangFormat** configuration for the target version by following the instructions [**here**](https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration#clangformat-version-updates)
|
||||
1. Submit a pull request in the `arduino/arduino-ide` repository to update the version in the `arduino.clangd.version` key of [`package.json`](package.json).
|
||||
1. Submit a pull request in [the `arduino/tooling-project-assets` repository](https://github.com/arduino/tooling-project-assets) to update the version in the `vars.DEFAULT_CLANG_FORMAT_VERSION` field of [`Taskfile.yml`](https://github.com/arduino/tooling-project-assets/blob/main/Taskfile.yml).
|
||||
|
||||
### Customize Icons
|
||||
ArduinoIde uses a customized version of FontAwesome.
|
||||
In order to update/replace icons follow the following steps:
|
||||
|
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.0-rc8",
|
||||
"version": "2.0.0",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-serial-plotter && yarn copy-i18n && yarn clean && yarn download-examples && yarn build && yarn test",
|
||||
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-i18n && yarn clean && yarn download-examples && yarn build && yarn test",
|
||||
"clean": "rimraf lib",
|
||||
"compose-changelog": "node ./scripts/compose-changelog.js",
|
||||
"download-cli": "node ./scripts/download-cli.js",
|
||||
"download-fwuploader": "node ./scripts/download-fwuploader.js",
|
||||
"copy-serial-plotter": "npx ncp ../node_modules/arduino-serial-plotter-webapp ./build/arduino-serial-plotter-webapp",
|
||||
"copy-i18n": "npx ncp ../i18n ./build/i18n",
|
||||
"download-ls": "node ./scripts/download-ls.js",
|
||||
"download-examples": "node ./scripts/download-examples.js",
|
||||
@@ -62,6 +61,7 @@
|
||||
"atob": "^2.1.2",
|
||||
"auth0-js": "^9.14.0",
|
||||
"btoa": "^1.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"dateformat": "^3.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"deepmerge": "2.0.1",
|
||||
@@ -147,16 +147,18 @@
|
||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/theia/core/browser-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
||||
},
|
||||
{
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
|
||||
},
|
||||
{
|
||||
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
||||
}
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.24.0"
|
||||
"version": "0.27.1"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.0"
|
||||
@@ -165,7 +167,7 @@
|
||||
"version": "14.0.0"
|
||||
},
|
||||
"languageServer": {
|
||||
"version": "0.6.0"
|
||||
"version": "0.7.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,141 +1,87 @@
|
||||
// @ts-check
|
||||
|
||||
(async () => {
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const semver = require('semver');
|
||||
const moment = require('moment');
|
||||
const downloader = require('./downloader');
|
||||
const { taskBuildFromGit } = require('./utils');
|
||||
|
||||
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 version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) {
|
||||
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 (!version) {
|
||||
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
if (!cli) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { version } = cli;
|
||||
return version;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!version) {
|
||||
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
|
||||
shell.exit(1);
|
||||
if (!suffix) {
|
||||
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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 {
|
||||
|
||||
// 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.');
|
||||
|
||||
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
} else {
|
||||
taskBuildFromGit(version, destinationPath, 'CLI');
|
||||
}
|
||||
})();
|
||||
|
@@ -7,22 +7,23 @@
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const downloader = require('./downloader');
|
||||
const { goBuildFromGit } = require('./utils');
|
||||
|
||||
const [DEFAULT_ALS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
|
||||
const [DEFAULT_LS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) return undefined;
|
||||
if (!pkg) return [undefined, undefined];
|
||||
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) return undefined;
|
||||
if (!arduino) return [undefined, undefined];
|
||||
|
||||
const { languageServer, clangd } = arduino;
|
||||
if (!languageServer) return undefined;
|
||||
if (!clangd) return undefined;
|
||||
if (!languageServer) return [undefined, undefined];
|
||||
if (!clangd) return [undefined, undefined];
|
||||
|
||||
return [languageServer.version, clangd.version];
|
||||
})();
|
||||
|
||||
if (!DEFAULT_ALS_VERSION) {
|
||||
if (!DEFAULT_LS_VERSION) {
|
||||
shell.echo(
|
||||
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
|
||||
);
|
||||
@@ -39,8 +40,8 @@
|
||||
const yargs = require('yargs')
|
||||
.option('ls-version', {
|
||||
alias: 'lv',
|
||||
default: DEFAULT_ALS_VERSION,
|
||||
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`,
|
||||
default: DEFAULT_LS_VERSION,
|
||||
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_LS_VERSION}.`,
|
||||
})
|
||||
.option('clangd-version', {
|
||||
alias: 'cv',
|
||||
@@ -56,7 +57,7 @@
|
||||
.version(false)
|
||||
.parse();
|
||||
|
||||
const alsVersion = yargs['ls-version'];
|
||||
const lsVersion = yargs['ls-version'];
|
||||
const clangdVersion = yargs['clangd-version'];
|
||||
const force = yargs['force-download'];
|
||||
const { platform, arch } = process;
|
||||
@@ -87,6 +88,8 @@
|
||||
lsSuffix = 'Windows_64bit.zip';
|
||||
clangdSuffix = 'Windows_64bit';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported platform/arch: ${platformArch}.`);
|
||||
}
|
||||
if (!lsSuffix || !clangdSuffix) {
|
||||
shell.echo(
|
||||
@@ -95,12 +98,16 @@
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${
|
||||
alsVersion === 'nightly'
|
||||
? 'nightly/arduino-language-server'
|
||||
: 'arduino-language-server_' + alsVersion
|
||||
}_${lsSuffix}`;
|
||||
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
|
||||
if (typeof lsVersion === 'string') {
|
||||
const lsUrl = `https://downloads.arduino.cc/arduino-language-server/${
|
||||
lsVersion === 'nightly'
|
||||
? 'nightly/arduino-language-server'
|
||||
: 'arduino-language-server_' + lsVersion
|
||||
}_${lsSuffix}`;
|
||||
downloader.downloadUnzipAll(lsUrl, build, lsExecutablePath, force);
|
||||
} else {
|
||||
goBuildFromGit(lsVersion, lsExecutablePath, 'language-server');
|
||||
}
|
||||
|
||||
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
|
||||
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
|
||||
|
@@ -86,6 +86,7 @@ exports.downloadUnzipFile = async (
|
||||
* @param targetDir {string} Directory into which to decompress the archive
|
||||
* @param targetFile {string} Path to the main file expected after decompressing
|
||||
* @param force {boolean} Whether to download even if the target file exists
|
||||
* @param decompressOptions {import('decompress').DecompressOptions}
|
||||
*/
|
||||
exports.downloadUnzipAll = async (
|
||||
url,
|
||||
|
110
arduino-ide-extension/scripts/utils.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
|
||||
*
|
||||
* @param version {object} the version object.
|
||||
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
|
||||
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
|
||||
*/
|
||||
exports.taskBuildFromGit = (version, destinationPath, taskName) => {
|
||||
return buildFromGit('task', version, destinationPath, taskName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clones something from GitHub and builds it with `Golang`.
|
||||
*
|
||||
* @param version {object} the version object.
|
||||
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
|
||||
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
|
||||
*/
|
||||
exports.goBuildFromGit = (version, destinationPath, taskName) => {
|
||||
return buildFromGit('go', version, destinationPath, taskName);
|
||||
};
|
||||
|
||||
/**
|
||||
* The `command` is either `go` or `task`.
|
||||
*/
|
||||
function buildFromGit(command, version, destinationPath, taskName) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = require('temp');
|
||||
const shell = require('shelljs');
|
||||
|
||||
// We assume an object with `owner`, `repo`, commitish?` properties.
|
||||
if (typeof version !== 'object') {
|
||||
shell.echo(
|
||||
`Expected a \`{ owner, repo, commitish }\` object. Got <${version}> instead.`
|
||||
);
|
||||
}
|
||||
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 ${taskName} from ${url}. Commitish: ${
|
||||
commitish ? commitish : 'HEAD'
|
||||
}`
|
||||
);
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
shell.echo(
|
||||
`Skipping the ${taskName} build because it already exists: ${destinationPath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const buildFolder = path.join(__dirname, '..', 'build');
|
||||
if (shell.mkdir('-p', buildFolder).code !== 0) {
|
||||
shell.echo('Could not create build folder.');
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const tempRepoPath = temp.mkdirSync();
|
||||
shell.echo(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
|
||||
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Cloned ${taskName} 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 ${taskName}...`);
|
||||
if (shell.exec(`${command} build`, { cwd: tempRepoPath }).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Done ${taskName} build.`);
|
||||
|
||||
const binName = path.basename(destinationPath);
|
||||
if (!fs.existsSync(path.join(tempRepoPath, binName))) {
|
||||
shell.echo(
|
||||
`Could not find the ${taskName} at ${path.join(tempRepoPath, binName)}.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const binPath = path.join(tempRepoPath, binName);
|
||||
shell.echo(
|
||||
`>>> Copying ${taskName} from ${binPath} to ${destinationPath}...`
|
||||
);
|
||||
if (shell.cp(binPath, destinationPath).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Copied the ${taskName}.`);
|
||||
|
||||
shell.echo(`<<< Verifying ${taskName}...`);
|
||||
if (!fs.existsSync(destinationPath)) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`>>> Verified ${taskName}.`);
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
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 TOGGLE_COMPILE_FOR_DEBUG: Command = {
|
||||
id: 'arduino-toggle-compile-for-debug',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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',
|
||||
};
|
||||
}
|
@@ -1,37 +1,19 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import {
|
||||
BoardsService,
|
||||
SketchesService,
|
||||
ExecutableService,
|
||||
Sketch,
|
||||
LibraryService,
|
||||
ArduinoDaemon,
|
||||
} from '../common/protocol';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
ILogger,
|
||||
DisposableCollection,
|
||||
} from '@theia/core';
|
||||
import {
|
||||
Dialog,
|
||||
FrontendApplication,
|
||||
FrontendApplicationContribution,
|
||||
LocalStorageService,
|
||||
OnWillStopAction,
|
||||
SaveableWidget,
|
||||
StatusBar,
|
||||
StatusBarAlignment,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
@@ -39,46 +21,24 @@ import {
|
||||
TabBarToolbarContribution,
|
||||
TabBarToolbarRegistry,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
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 {
|
||||
EditorCommands,
|
||||
EditorMainMenu,
|
||||
EditorManager,
|
||||
EditorOpenerOptions,
|
||||
} from '@theia/editor/lib/browser';
|
||||
import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/browser';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { ArduinoPreferences } from './arduino-preferences';
|
||||
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 { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { ArduinoPreferences } from './arduino-preferences';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog';
|
||||
import { IDEUpdater } from '../common/protocol/ide-updater';
|
||||
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { HostedPluginEvents } from './hosted-plugin-events';
|
||||
|
||||
const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
|
||||
export const SKIP_IDE_VERSION = 'skipIDEVersion';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendContribution
|
||||
@@ -89,100 +49,23 @@ export class ArduinoFrontendContribution
|
||||
MenuContribution,
|
||||
ColorContribution
|
||||
{
|
||||
@inject(ILogger)
|
||||
private readonly logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(EditorManager)
|
||||
private readonly editorManager: EditorManager;
|
||||
|
||||
@inject(FileService)
|
||||
private readonly fileService: FileService;
|
||||
|
||||
@inject(SketchesService)
|
||||
private readonly sketchService: SketchesService;
|
||||
|
||||
@inject(BoardsConfigDialog)
|
||||
private readonly boardsConfigDialog: BoardsConfigDialog;
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(StatusBar)
|
||||
private readonly statusBar: StatusBar;
|
||||
|
||||
@inject(EditorMode)
|
||||
private readonly editorMode: EditorMode;
|
||||
|
||||
@inject(HostedPluginEvents)
|
||||
private readonly hostedPluginEvents: HostedPluginEvents;
|
||||
|
||||
@inject(ExecutableService)
|
||||
private readonly executableService: ExecutableService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
private readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorageService: LocalStorageService;
|
||||
|
||||
@inject(FileSystemFrontendContribution)
|
||||
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
|
||||
|
||||
@inject(IDEUpdater)
|
||||
private readonly updater: IDEUpdater;
|
||||
|
||||
@inject(IDEUpdaterDialog)
|
||||
private readonly updaterDialog: IDEUpdaterDialog;
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
private readonly daemon: ArduinoDaemon;
|
||||
|
||||
protected invalidConfigPopup:
|
||||
| Promise<void | 'No' | 'Yes' | undefined>
|
||||
| undefined;
|
||||
protected toDisposeOnStop = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
const isFirstStartup = !(await this.localStorageService.getData(
|
||||
INIT_LIBS_AND_PACKAGES
|
||||
));
|
||||
if (isFirstStartup) {
|
||||
await this.localStorageService.setData(INIT_LIBS_AND_PACKAGES, true);
|
||||
const avrPackage = await this.boardsService.getBoardPackage({
|
||||
id: 'arduino:avr',
|
||||
});
|
||||
const builtInLibrary = (
|
||||
await this.libraryService.search({
|
||||
query: 'Arduino_BuiltIn',
|
||||
})
|
||||
)[0];
|
||||
|
||||
!!avrPackage && (await this.boardsService.install({ item: avrPackage }));
|
||||
!!builtInLibrary &&
|
||||
(await this.libraryService.install({
|
||||
item: builtInLibrary,
|
||||
installDependencies: true,
|
||||
}));
|
||||
}
|
||||
if (!window.navigator.onLine) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
this.messageService.warn(
|
||||
@@ -192,241 +75,32 @@ export class ArduinoFrontendContribution
|
||||
)
|
||||
);
|
||||
}
|
||||
const updateStatusBar = ({
|
||||
selectedBoard,
|
||||
selectedPort,
|
||||
}: BoardsConfig.Config) => {
|
||||
this.statusBar.setElement('arduino-selected-board', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedBoard
|
||||
? `$(microchip) ${selectedBoard.name}`
|
||||
: `$(close) ${nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
)}`,
|
||||
className: 'arduino-selected-board',
|
||||
});
|
||||
if (selectedBoard) {
|
||||
this.statusBar.setElement('arduino-selected-port', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedPort
|
||||
? nls.localize(
|
||||
'arduino/common/selectedOn',
|
||||
'on {0}',
|
||||
selectedPort.address
|
||||
)
|
||||
: nls.localize('arduino/common/notConnected', '[not connected]'),
|
||||
className: 'arduino-selected-port',
|
||||
});
|
||||
}
|
||||
};
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (
|
||||
CurrentSketch.isValid(sketch) &&
|
||||
!(await this.sketchService.isTemp(sketch))
|
||||
) {
|
||||
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
|
||||
this.toDisposeOnStop.push(
|
||||
this.fileService.onDidFilesChange(async (event) => {
|
||||
for (const { type, resource } of event.changes) {
|
||||
if (
|
||||
type === FileChangeType.ADDED &&
|
||||
resource.parent.toString() === sketch.uri
|
||||
) {
|
||||
const reloadedSketch = await this.sketchService.loadSketch(
|
||||
sketch.uri
|
||||
);
|
||||
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
||||
this.ensureOpened(resource.toString(), true, {
|
||||
mode: 'open',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onStart(app: FrontendApplication): Promise<void> {
|
||||
this.updater
|
||||
.init(
|
||||
this.arduinoPreferences.get('arduino.ide.updateChannel'),
|
||||
this.arduinoPreferences.get('arduino.ide.updateBaseUrl')
|
||||
)
|
||||
.then(() => this.updater.checkForUpdates(true))
|
||||
.then(async (updateInfo) => {
|
||||
if (!updateInfo) return;
|
||||
const versionToSkip = await this.localStorageService.getData<string>(
|
||||
SKIP_IDE_VERSION
|
||||
);
|
||||
if (versionToSkip === updateInfo.version) return;
|
||||
this.updaterDialog.open(updateInfo);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/ide-updater/errorCheckingForUpdates',
|
||||
'Error while checking for Arduino IDE updates.\n{0}',
|
||||
e.message
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
|
||||
if (selectedBoard) {
|
||||
const { name, fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
this.startLanguageServer(fqbn, name);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceClientImpl.boardsConfig)
|
||||
);
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
);
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.arduinoPreferences.onPreferenceChanged((event) => {
|
||||
if (event.newValue !== event.oldValue) {
|
||||
switch (event.preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
start(this.boardsServiceClientImpl.boardsConfig);
|
||||
break;
|
||||
case 'arduino.window.zoomLevel':
|
||||
if (typeof event.newValue === 'number') {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
webContents.setZoomLevel(event.newValue || 0);
|
||||
}
|
||||
break;
|
||||
case 'arduino.ide.updateChannel':
|
||||
case 'arduino.ide.updateBaseUrl':
|
||||
this.updater.init(
|
||||
this.arduinoPreferences.get('arduino.ide.updateChannel'),
|
||||
this.arduinoPreferences.get('arduino.ide.updateBaseUrl')
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.arduinoPreferences.ready.then(() => {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
|
||||
webContents.setZoomLevel(zoomLevel);
|
||||
});
|
||||
|
||||
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
|
||||
|
||||
this.fileSystemFrontendContribution.onDidChangeEditorFile(
|
||||
({ type, editor }) => {
|
||||
if (type === FileChangeType.DELETED) {
|
||||
const editorWidget = editor;
|
||||
if (SaveableWidget.is(editorWidget)) {
|
||||
editorWidget.closeWithoutSaving();
|
||||
} else {
|
||||
editorWidget.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDisposeOnStop.dispose();
|
||||
}
|
||||
|
||||
protected languageServerFqbn?: string;
|
||||
protected languageServerStartMutex = new Mutex();
|
||||
protected async startLanguageServer(
|
||||
fqbn: string,
|
||||
name: string | undefined
|
||||
): Promise<void> {
|
||||
const port = await this.daemon.tryGetPort();
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
const release = await this.languageServerStartMutex.acquire();
|
||||
try {
|
||||
await this.hostedPluginEvents.didStart;
|
||||
const details = await this.boardsService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
// Core is not installed for the selected board.
|
||||
console.info(
|
||||
`Could not start language server for ${fqbn}. The core is not installed for the board.`
|
||||
this.appStateService.reachedState('ready').then(() =>
|
||||
this.arduinoPreferences.ready.then(() => {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
const zoomLevel = this.arduinoPreferences.get(
|
||||
'arduino.window.zoomLevel'
|
||||
);
|
||||
if (this.languageServerFqbn) {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand(
|
||||
'arduino.languageserver.stop'
|
||||
);
|
||||
console.info(
|
||||
`Stopped language server process for ${this.languageServerFqbn}.`
|
||||
);
|
||||
this.languageServerFqbn = undefined;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to start language server process for ${this.languageServerFqbn}`,
|
||||
e
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fqbn === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbn}`);
|
||||
const log = this.arduinoPreferences.get('arduino.language.log');
|
||||
let currentSketchPath: string | undefined = undefined;
|
||||
if (log) {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
currentSketchPath = await this.fileService.fsPath(
|
||||
new URI(currentSketch.uri)
|
||||
);
|
||||
}
|
||||
}
|
||||
const { clangdUri, lsUri } = await this.executableService.list();
|
||||
const [clangdPath, lsPath] = await Promise.all([
|
||||
this.fileService.fsPath(new URI(clangdUri)),
|
||||
this.fileService.fsPath(new URI(lsUri)),
|
||||
]);
|
||||
|
||||
this.languageServerFqbn = await Promise.race([
|
||||
new Promise<undefined>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`Timeout after ${20_000} ms.`)),
|
||||
20_000
|
||||
)
|
||||
),
|
||||
this.commandRegistry.executeCommand<string>(
|
||||
'arduino.languageserver.start',
|
||||
{
|
||||
lsPath,
|
||||
cliDaemonAddr: `localhost:${port}`,
|
||||
clangdPath,
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
cliDaemonInstance: '1',
|
||||
board: {
|
||||
fqbn,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
}
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
webContents.setZoomLevel(zoomLevel);
|
||||
})
|
||||
);
|
||||
// Removes the _Settings_ (cog) icon from the left sidebar
|
||||
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
@@ -436,13 +110,21 @@ export class ArduinoFrontendContribution
|
||||
<BoardsToolBarItem
|
||||
key="boardsToolbarItem"
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceClient={this.boardsServiceClientImpl}
|
||||
boardsServiceProvider={this.boardsServiceProvider}
|
||||
/>
|
||||
),
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
priority: 7,
|
||||
});
|
||||
registry.registerItem({
|
||||
id: 'toggle-serial-plotter',
|
||||
command: SerialPlotterContribution.Commands.OPEN_TOOLBAR.id,
|
||||
tooltip: nls.localize(
|
||||
'arduino/serial/openSerialPlotter',
|
||||
'Serial Plotter'
|
||||
),
|
||||
});
|
||||
registry.registerItem({
|
||||
id: 'toggle-serial-monitor',
|
||||
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
||||
@@ -451,24 +133,6 @@ export class ArduinoFrontendContribution
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
|
||||
execute: () => this.editorMode.toggleCompileForDebug(),
|
||||
isToggled: () => this.editorMode.compileForDebug,
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
|
||||
execute: async (uri: URI) => {
|
||||
this.openSketchFiles(uri);
|
||||
},
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
|
||||
execute: async (query?: string | undefined) => {
|
||||
const boardsConfig = await this.boardsConfigDialog.open(query);
|
||||
if (boardsConfig) {
|
||||
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
for (const command of [
|
||||
EditorCommands.SPLIT_EDITOR_DOWN,
|
||||
EditorCommands.SPLIT_EDITOR_LEFT,
|
||||
@@ -501,104 +165,12 @@ export class ArduinoFrontendContribution
|
||||
ArduinoMenus.TOOLS,
|
||||
nls.localize('arduino/menu/tools', 'Tools')
|
||||
);
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
|
||||
label: nls.localize(
|
||||
'arduino/debug/optimizeForDebugging',
|
||||
'Optimize for Debugging'
|
||||
),
|
||||
order: '5',
|
||||
});
|
||||
}
|
||||
|
||||
protected async openSketchFiles(uri: URI): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(uri.toString());
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
||||
await this.ensureOpened(uri);
|
||||
}
|
||||
if (mainFileUri.endsWith('.pde')) {
|
||||
const message = nls.localize(
|
||||
'arduino/common/oldFormat',
|
||||
"The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?",
|
||||
sketch.name
|
||||
);
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
this.messageService
|
||||
.info(message, nls.localize('arduino/common/later', 'Later'), yes)
|
||||
.then(async (answer) => {
|
||||
if (answer === yes) {
|
||||
this.commandRegistry.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
||||
this.messageService.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected async ensureOpened(
|
||||
uri: string,
|
||||
forceOpen = false,
|
||||
options?: EditorOpenerOptions | undefined
|
||||
): Promise<unknown> {
|
||||
const widget = this.editorManager.all.find(
|
||||
(widget) => widget.editor.uri.toString() === uri
|
||||
);
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(
|
||||
new URI(uri),
|
||||
options ?? {
|
||||
mode: 'reveal',
|
||||
preview: false,
|
||||
counter: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerColors(colors: ColorRegistry): void {
|
||||
colors.register(
|
||||
{
|
||||
id: 'arduino.branding.primary',
|
||||
defaults: {
|
||||
dark: 'statusBar.background',
|
||||
light: 'statusBar.background',
|
||||
},
|
||||
description:
|
||||
'The primary branding color, such as dialog titles, library, and board manager list labels.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.branding.secondary',
|
||||
defaults: {
|
||||
dark: 'statusBar.background',
|
||||
light: 'statusBar.background',
|
||||
},
|
||||
description:
|
||||
'Secondary branding color for list selections, dropdowns, and widget borders.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.foreground',
|
||||
defaults: {
|
||||
dark: 'editorWidget.background',
|
||||
light: 'editorWidget.background',
|
||||
hc: 'editorWidget.background',
|
||||
},
|
||||
description:
|
||||
'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.background',
|
||||
id: 'arduino.toolbar.button.background',
|
||||
defaults: {
|
||||
dark: 'button.background',
|
||||
light: 'button.background',
|
||||
@@ -608,15 +180,35 @@ export class ArduinoFrontendContribution
|
||||
'Background color of the toolbar items. Such as Upload, Verify, etc.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.hoverBackground',
|
||||
id: 'arduino.toolbar.button.hoverBackground',
|
||||
defaults: {
|
||||
dark: 'button.hoverBackground',
|
||||
light: 'button.foreground',
|
||||
hc: 'textLink.foreground',
|
||||
light: 'button.hoverBackground',
|
||||
hc: 'button.background',
|
||||
},
|
||||
description:
|
||||
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.button.secondary.label',
|
||||
defaults: {
|
||||
dark: 'secondaryButton.foreground',
|
||||
light: 'button.foreground',
|
||||
hc: 'activityBar.inactiveForeground',
|
||||
},
|
||||
description:
|
||||
'Foreground color of the toolbar items. Such as Serial Monitor and Serial Plotter',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.button.secondary.hoverBackground',
|
||||
defaults: {
|
||||
dark: 'secondaryButton.hoverBackground',
|
||||
light: 'button.hoverBackground',
|
||||
hc: 'textLink.foreground',
|
||||
},
|
||||
description:
|
||||
'Background color of the toolbar items when hovering over them, such as "Serial Monitor" and "Serial Plotter"',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.toggleBackground',
|
||||
defaults: {
|
||||
@@ -628,76 +220,73 @@ export class ArduinoFrontendContribution
|
||||
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.foreground',
|
||||
id: 'arduino.toolbar.dropdown.border',
|
||||
defaults: {
|
||||
dark: 'editor.foreground',
|
||||
light: 'editor.foreground',
|
||||
hc: 'editor.foreground',
|
||||
dark: 'dropdown.border',
|
||||
light: 'dropdown.border',
|
||||
hc: 'dropdown.border',
|
||||
},
|
||||
description: 'Color of the text in the Output view.',
|
||||
description: 'Border color of the Board Selector.',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.borderActive',
|
||||
defaults: {
|
||||
dark: 'focusBorder',
|
||||
light: 'focusBorder',
|
||||
hc: 'focusBorder',
|
||||
},
|
||||
description: "Border color of the Board Selector when it's active",
|
||||
},
|
||||
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.background',
|
||||
defaults: {
|
||||
dark: 'tab.unfocusedActiveBackground',
|
||||
light: 'dropdown.background',
|
||||
hc: 'dropdown.background',
|
||||
},
|
||||
description: 'Background color of the Board Selector.',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.label',
|
||||
defaults: {
|
||||
dark: 'dropdown.foreground',
|
||||
light: 'dropdown.foreground',
|
||||
hc: 'dropdown.foreground',
|
||||
},
|
||||
description: 'Font color of the Board Selector.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.background',
|
||||
id: 'arduino.toolbar.dropdown.iconSelected',
|
||||
defaults: {
|
||||
dark: 'editor.background',
|
||||
light: 'editor.background',
|
||||
hc: 'editor.background',
|
||||
dark: 'list.activeSelectionIconForeground',
|
||||
light: 'list.activeSelectionIconForeground',
|
||||
hc: 'list.activeSelectionIconForeground',
|
||||
},
|
||||
description: 'Background color of the Output view.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onWillStop(): OnWillStopAction {
|
||||
return {
|
||||
reason: 'temp-sketch',
|
||||
action: () => {
|
||||
return this.showTempSketchDialog();
|
||||
description:
|
||||
'Color of the selected protocol icon in the Board Selector.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async showTempSketchDialog(): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return true;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp) {
|
||||
return true;
|
||||
}
|
||||
const messageBoxResult = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message: nls.localize(
|
||||
'arduino/sketch/saveTempSketch',
|
||||
'Save your sketch to open it again later.'
|
||||
),
|
||||
title: nls.localize(
|
||||
'theia/core/quitTitle',
|
||||
'Are you sure you want to quit?'
|
||||
),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
Dialog.CANCEL,
|
||||
nls.localizeByDefault('Save As...'),
|
||||
nls.localizeByDefault("Don't Save"),
|
||||
],
|
||||
id: 'arduino.toolbar.dropdown.option.backgroundHover',
|
||||
defaults: {
|
||||
dark: 'list.hoverBackground',
|
||||
light: 'list.hoverBackground',
|
||||
hc: 'list.hoverBackground',
|
||||
},
|
||||
description: 'Background color on hover of the Board Selector options.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.option.backgroundSelected',
|
||||
defaults: {
|
||||
dark: 'list.activeSelectionBackground',
|
||||
light: 'list.activeSelectionBackground',
|
||||
hc: 'list.activeSelectionBackground',
|
||||
},
|
||||
description:
|
||||
'Background color of the selected board in the Board Selector.',
|
||||
}
|
||||
);
|
||||
const result = messageBoxResult.response;
|
||||
if (result === 2) {
|
||||
return true;
|
||||
} else if (result === 1) {
|
||||
return !!(await this.commandRegistry.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: false,
|
||||
wipeOriginal: true,
|
||||
}
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -50,13 +50,17 @@ import {
|
||||
ApplicationShell as TheiaApplicationShell,
|
||||
ShellLayoutRestorer as TheiaShellLayoutRestorer,
|
||||
CommonFrontendContribution as TheiaCommonFrontendContribution,
|
||||
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||
TabBarRendererFactory,
|
||||
ContextMenuRenderer,
|
||||
createTreeContainer,
|
||||
TreeWidget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import { ApplicationShell } from './theia/core/application-shell';
|
||||
import {
|
||||
ApplicationShell,
|
||||
DockPanelRenderer,
|
||||
} from './theia/core/application-shell';
|
||||
import { FrontendApplication } from './theia/core/frontend-application';
|
||||
import {
|
||||
BoardsConfigDialog,
|
||||
@@ -80,10 +84,12 @@ import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browse
|
||||
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 {
|
||||
MonacoThemeJson,
|
||||
MonacoThemingService,
|
||||
} from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import {
|
||||
ArduinoDaemonPath,
|
||||
ArduinoDaemon,
|
||||
@@ -99,7 +105,8 @@ import {
|
||||
} 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 { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import {
|
||||
FileSystemExt,
|
||||
FileSystemExtPath,
|
||||
@@ -121,7 +128,6 @@ import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
import { SaveSketch } from './contributions/save-sketch';
|
||||
import { VerifySketch } from './contributions/verify-sketch';
|
||||
import { UploadSketch } from './contributions/upload-sketch';
|
||||
import { SurveyNotification } from './contributions/survey-notification';
|
||||
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
|
||||
import { EditContributions } from './contributions/edit-contributions';
|
||||
import { OpenSketchExternal } from './contributions/open-sketch-external';
|
||||
@@ -136,8 +142,6 @@ import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handl
|
||||
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,
|
||||
@@ -164,7 +168,7 @@ import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service
|
||||
import { ResponseServiceImpl } from './response-service-impl';
|
||||
import {
|
||||
ResponseService,
|
||||
ResponseServiceArduino,
|
||||
ResponseServiceClient,
|
||||
ResponseServicePath,
|
||||
} from '../common/protocol/response-service';
|
||||
import { NotificationCenter } from './notification-center';
|
||||
@@ -210,7 +214,10 @@ import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-
|
||||
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
||||
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
||||
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import {
|
||||
MonacoEditorFactory,
|
||||
MonacoEditorProvider as TheiaMonacoEditorProvider,
|
||||
} from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { StorageWrapper } from './storage-wrapper';
|
||||
import { NotificationManager } from './theia/messages/notifications-manager';
|
||||
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
@@ -302,20 +309,60 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
|
||||
import { CompilerErrors } from './contributions/compiler-errors';
|
||||
import { WidgetManager } from './theia/core/widget-manager';
|
||||
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||
import { StartupTasks } from './contributions/startup-task';
|
||||
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
|
||||
import { Daemon } from './contributions/daemon';
|
||||
import { FirstStartupInstaller } from './contributions/first-startup-installer';
|
||||
import { OpenSketchFiles } from './contributions/open-sketch-files';
|
||||
import { InoLanguage } from './contributions/ino-language';
|
||||
import { SelectedBoard } from './contributions/selected-board';
|
||||
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
|
||||
import { OpenBoardsConfig } from './contributions/open-boards-config';
|
||||
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
|
||||
import { MonacoThemeServiceIsReady } from './utils/window';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { StatusBarImpl } from './theia/core/status-bar';
|
||||
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
|
||||
import { EditorMenuContribution } from './theia/editor/editor-file';
|
||||
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
|
||||
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
|
||||
import { PreferencesEditorWidget } from './theia/preferences/preference-editor-widget';
|
||||
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
|
||||
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
|
||||
import {
|
||||
BoardsFilterRenderer,
|
||||
LibraryFilterRenderer,
|
||||
} from './widgets/component-list/filter-renderer';
|
||||
import { CheckForUpdates } from './contributions/check-for-updates';
|
||||
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
||||
import { StartupTaskProvider } from '../electron-common/startup-task';
|
||||
import { DeleteSketch } from './contributions/delete-sketch';
|
||||
|
||||
MonacoThemingService.register({
|
||||
id: 'arduino-theme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/default.color-theme.json'),
|
||||
});
|
||||
|
||||
MonacoThemingService.register({
|
||||
id: 'arduino-theme-dark',
|
||||
label: 'Dark (Arduino)',
|
||||
uiTheme: 'vs-dark',
|
||||
json: require('../../src/browser/data/dark.color-theme.json'),
|
||||
});
|
||||
const registerArduinoThemes = () => {
|
||||
const themes: MonacoThemeJson[] = [
|
||||
{
|
||||
id: 'arduino-theme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/default.color-theme.json'),
|
||||
},
|
||||
{
|
||||
id: 'arduino-theme-dark',
|
||||
label: 'Dark (Arduino)',
|
||||
uiTheme: 'vs-dark',
|
||||
json: require('../../src/browser/data/dark.color-theme.json'),
|
||||
},
|
||||
];
|
||||
themes.forEach((theme) => MonacoThemingService.register(theme));
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const global = window as any;
|
||||
const ready = global[MonacoThemeServiceIsReady] as Deferred;
|
||||
if (ready) {
|
||||
ready.promise.then(registerArduinoThemes);
|
||||
} else {
|
||||
registerArduinoThemes();
|
||||
}
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Commands and toolbar items
|
||||
@@ -331,6 +378,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
|
||||
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService)
|
||||
@@ -387,6 +436,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
|
||||
bind(CommandContribution).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)
|
||||
@@ -422,7 +472,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialogProps).toConstantValue({
|
||||
title: nls.localize('arduino/common/selectBoard', 'Select Board'),
|
||||
title: nls.localize(
|
||||
'arduino/board/boardConfigDialogTitle',
|
||||
'Select Other Board and Port'
|
||||
),
|
||||
});
|
||||
|
||||
// Core service
|
||||
@@ -483,14 +536,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
WorkspaceVariableContribution
|
||||
);
|
||||
|
||||
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
|
||||
bind(EditorMode).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(EditorMode);
|
||||
|
||||
// Survey notification
|
||||
bind(SurveyNotification).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(SurveyNotification);
|
||||
|
||||
bind(SurveyNotificationService)
|
||||
.toDynamicValue((context) => {
|
||||
return ElectronIpcConnectionProvider.createProxy(
|
||||
@@ -548,8 +593,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
return container.get(TabBarToolbar);
|
||||
}
|
||||
);
|
||||
bind(OutputWidget).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputWidget).toService(OutputWidget);
|
||||
bind(OutputChannelManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
||||
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
||||
@@ -566,7 +609,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
|
||||
bind(EditorManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorManager).to(EditorManager);
|
||||
rebind(TheiaEditorManager).toService(EditorManager);
|
||||
|
||||
// replace search icon
|
||||
rebind(TheiaSearchInWorkspaceFactory)
|
||||
@@ -614,6 +657,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(WindowContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaWindowContribution).toService(WindowContribution);
|
||||
|
||||
// To remove `File` > `Close Editor`.
|
||||
bind(EditorMenuContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorMenuContribution).toService(EditorMenuContribution);
|
||||
|
||||
// To disable the highlighting of non-unicode characters in the _Output_ view
|
||||
bind(OutputEditorFactory).toSelf().inSingletonScope();
|
||||
// Rebind to `TheiaOutputEditorFactory` when https://github.com/eclipse-theia/theia/pull/11615 is available.
|
||||
rebind(MonacoEditorFactory).toService(OutputEditorFactory);
|
||||
|
||||
bind(ArduinoDaemon)
|
||||
.toDynamicValue((context) =>
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
@@ -698,6 +750,21 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, PlotterFrontendContribution);
|
||||
Contribution.configure(bind, Format);
|
||||
Contribution.configure(bind, CompilerErrors);
|
||||
Contribution.configure(bind, StartupTasks);
|
||||
Contribution.configure(bind, IndexesUpdateProgress);
|
||||
Contribution.configure(bind, Daemon);
|
||||
Contribution.configure(bind, FirstStartupInstaller);
|
||||
Contribution.configure(bind, OpenSketchFiles);
|
||||
Contribution.configure(bind, InoLanguage);
|
||||
Contribution.configure(bind, SelectedBoard);
|
||||
Contribution.configure(bind, CheckForIDEUpdates);
|
||||
Contribution.configure(bind, OpenBoardsConfig);
|
||||
Contribution.configure(bind, SketchFilesTracker);
|
||||
Contribution.configure(bind, CheckForUpdates);
|
||||
Contribution.configure(bind, DeleteSketch);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
|
||||
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
||||
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
||||
@@ -719,7 +786,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
});
|
||||
|
||||
bind(ResponseService).toService(ResponseServiceImpl);
|
||||
bind(ResponseServiceArduino).toService(ResponseServiceImpl);
|
||||
bind(ResponseServiceClient).toService(ResponseServiceImpl);
|
||||
|
||||
bind(NotificationCenter).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(NotificationCenter);
|
||||
@@ -795,6 +862,26 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(WidgetManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaWidgetManager).toService(WidgetManager);
|
||||
|
||||
// To avoid running a status bar update on every single `keypress` event from the editor.
|
||||
bind(StatusBarImpl).toSelf().inSingletonScope();
|
||||
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
|
||||
|
||||
// Debounced update for the tab-bar toolbar when typing in the editor.
|
||||
bind(DockPanelRenderer).toSelf();
|
||||
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
|
||||
|
||||
// Avoid running the "reset scroll" interval tasks until the preference editor opens.
|
||||
rebind(PreferencesWidget)
|
||||
.toDynamicValue(({ container }) => {
|
||||
const child = createPreferencesWidgetContainer(container);
|
||||
child.bind(PreferencesEditorWidget).toSelf().inSingletonScope();
|
||||
child
|
||||
.rebind(TheiaPreferencesEditorWidget)
|
||||
.toService(PreferencesEditorWidget);
|
||||
return child.get(PreferencesWidget);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Preferences
|
||||
bindArduinoPreferences(bind);
|
||||
|
||||
|
@@ -51,6 +51,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.language.realTimeDiagnostics': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/language.realTimeDiagnostics',
|
||||
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.compile.verbose': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
@@ -177,10 +185,10 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.cloud.sketchSyncEnpoint': {
|
||||
'arduino.cloud.sketchSyncEndpoint': {
|
||||
type: 'string',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/cloud.sketchSyncEnpoint',
|
||||
'arduino/preferences/cloud.sketchSyncEndpoint',
|
||||
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.'
|
||||
),
|
||||
default: 'https://api2.arduino.cc/create',
|
||||
@@ -233,11 +241,20 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.checkForUpdates': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/checkForUpdate',
|
||||
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface ArduinoConfiguration {
|
||||
'arduino.language.log': boolean;
|
||||
'arduino.language.realTimeDiagnostics': boolean;
|
||||
'arduino.compile.verbose': boolean;
|
||||
'arduino.compile.experimental': boolean;
|
||||
'arduino.compile.revealRange': ErrorRevealStrategy;
|
||||
@@ -254,13 +271,14 @@ export interface ArduinoConfiguration {
|
||||
'arduino.cloud.pull.warn': boolean;
|
||||
'arduino.cloud.push.warn': boolean;
|
||||
'arduino.cloud.pushpublic.warn': boolean;
|
||||
'arduino.cloud.sketchSyncEnpoint': string;
|
||||
'arduino.cloud.sketchSyncEndpoint': string;
|
||||
'arduino.auth.clientID': string;
|
||||
'arduino.auth.domain': string;
|
||||
'arduino.auth.audience': string;
|
||||
'arduino.auth.registerUri': string;
|
||||
'arduino.survey.notification': boolean;
|
||||
'arduino.cli.daemon.debug': boolean;
|
||||
'arduino.checkForUpdates': boolean;
|
||||
}
|
||||
|
||||
export const ArduinoPreferences = Symbol('ArduinoPreferences');
|
||||
|
@@ -5,19 +5,38 @@ import {
|
||||
BoardsService,
|
||||
BoardsPackage,
|
||||
Board,
|
||||
Port,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { Installable, ResponseServiceArduino } from '../../common/protocol';
|
||||
import { Installable, ResponseServiceClient } from '../../common/protocol';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { InstallManually } from '../../common/nls';
|
||||
|
||||
interface AutoInstallPromptAction {
|
||||
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
|
||||
isAcceptance?: boolean;
|
||||
key: string;
|
||||
handler: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
type AutoInstallPromptActions = AutoInstallPromptAction[];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// * Cases in which we do not show the auto-install prompt:
|
||||
// 1. When a related platform is already installed
|
||||
// 2. When a prompt is already showing in the UI
|
||||
// 3. When a board is unplugged
|
||||
@injectable()
|
||||
export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@@ -27,8 +46,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(ResponseServiceArduino)
|
||||
protected readonly responseService: ResponseServiceArduino;
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
@@ -36,97 +55,227 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
|
||||
protected notifications: Board[] = [];
|
||||
|
||||
// * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually")
|
||||
// we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused"
|
||||
// an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt
|
||||
// showing again
|
||||
private portSelectedOnLastRefusal: Port | undefined;
|
||||
private lastRefusedPackageId: string | undefined;
|
||||
|
||||
onStart(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(
|
||||
this.ensureCoreExists.bind(this)
|
||||
);
|
||||
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
|
||||
}
|
||||
const setEventListeners = () => {
|
||||
this.boardsServiceClient.onBoardsConfigChanged((config) => {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
|
||||
protected ensureCoreExists(config: BoardsConfig.Config): void {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
if (
|
||||
selectedBoard &&
|
||||
selectedPort &&
|
||||
!this.notifications.find((board) => Board.sameAs(board, selectedBoard))
|
||||
) {
|
||||
this.notifications.push(selectedBoard);
|
||||
this.boardsService.search({}).then((packages) => {
|
||||
// filter packagesForBoard selecting matches from the cli (installed packages)
|
||||
// and matches based on the board name
|
||||
// NOTE: this ensures the Deprecated & new packages are all in the array
|
||||
// so that we can check if any of the valid packages is already installed
|
||||
const packagesForBoard = packages.filter(
|
||||
(pkg) =>
|
||||
BoardsPackage.contains(selectedBoard, pkg) ||
|
||||
pkg.boards.some((board) => board.name === selectedBoard.name)
|
||||
);
|
||||
const boardWasUnplugged =
|
||||
!selectedPort && this.portSelectedOnLastRefusal;
|
||||
|
||||
this.clearLastRefusedPromptInfo();
|
||||
|
||||
// check if one of the packages for the board is already installed. if so, no hint
|
||||
if (
|
||||
packagesForBoard.some(({ installedVersion }) => !!installedVersion)
|
||||
boardWasUnplugged ||
|
||||
!selectedBoard ||
|
||||
this.promptAlreadyShowingForBoard(selectedBoard)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// filter the installable (not installed) packages,
|
||||
// CLI returns the packages already sorted with the deprecated ones at the end of the list
|
||||
// in order to ensure the new ones are preferred
|
||||
const candidates = packagesForBoard.filter(
|
||||
({ installable, installedVersion }) =>
|
||||
installable && !installedVersion
|
||||
);
|
||||
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||
});
|
||||
|
||||
const candidate = candidates[0];
|
||||
if (candidate) {
|
||||
const version = candidate.availableVersions[0]
|
||||
? `[v ${candidate.availableVersions[0]}]`
|
||||
: '';
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const manualInstall = nls.localize(
|
||||
'arduino/board/installManually',
|
||||
'Install Manually'
|
||||
);
|
||||
// tslint:disable-next-line:max-line-length
|
||||
this.messageService
|
||||
.info(
|
||||
nls.localize(
|
||||
'arduino/board/installNow',
|
||||
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
|
||||
candidate.name,
|
||||
version,
|
||||
selectedBoard.name
|
||||
),
|
||||
manualInstall,
|
||||
yes
|
||||
)
|
||||
.then(async (answer) => {
|
||||
const index = this.notifications.findIndex((board) =>
|
||||
Board.sameAs(board, selectedBoard)
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.notifications.splice(index, 1);
|
||||
}
|
||||
if (answer === yes) {
|
||||
await Installable.installWithProgress({
|
||||
installable: this.boardsService,
|
||||
item: candidate,
|
||||
messageService: this.messageService,
|
||||
responseService: this.responseService,
|
||||
version: candidate.availableVersions[0],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (answer === manualInstall) {
|
||||
this.boardsManagerFrontendContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) =>
|
||||
widget.refresh(candidate.name.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
});
|
||||
// we "clearRefusedPackageInfo" if a "refused" package is eventually
|
||||
// installed, though this is not strictly necessary. It's more of a
|
||||
// cleanup, to ensure the related variables are representative of
|
||||
// current state.
|
||||
this.notificationCenter.onPlatformDidInstall((installed) => {
|
||||
if (this.lastRefusedPackageId === installed.item.id) {
|
||||
this.clearLastRefusedPromptInfo();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// we should invoke this.ensureCoreExists only once we're sure
|
||||
// everything has been reconciled
|
||||
this.boardsServiceClient.reconciled.then(() => {
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.boardsServiceClient.boardsConfig;
|
||||
|
||||
if (selectedBoard) {
|
||||
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||
}
|
||||
|
||||
setEventListeners();
|
||||
});
|
||||
}
|
||||
|
||||
private removeNotificationByBoard(selectedBoard: Board): void {
|
||||
const index = this.notifications.findIndex((notification) =>
|
||||
Board.sameAs(notification, selectedBoard)
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.notifications.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private clearLastRefusedPromptInfo(): void {
|
||||
this.lastRefusedPackageId = undefined;
|
||||
this.portSelectedOnLastRefusal = undefined;
|
||||
}
|
||||
|
||||
private setLastRefusedPromptInfo(
|
||||
packageId: string,
|
||||
selectedPort?: Port
|
||||
): void {
|
||||
this.lastRefusedPackageId = packageId;
|
||||
this.portSelectedOnLastRefusal = selectedPort;
|
||||
}
|
||||
|
||||
private promptAlreadyShowingForBoard(board: Board): boolean {
|
||||
return Boolean(
|
||||
this.notifications.find((notification) =>
|
||||
Board.sameAs(notification, board)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void {
|
||||
this.notifications.push(selectedBoard);
|
||||
this.boardsService.search({}).then((packages) => {
|
||||
const candidate = this.getInstallCandidate(packages, selectedBoard);
|
||||
|
||||
if (candidate) {
|
||||
this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort);
|
||||
} else {
|
||||
this.removeNotificationByBoard(selectedBoard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getInstallCandidate(
|
||||
packages: BoardsPackage[],
|
||||
selectedBoard: Board
|
||||
): BoardsPackage | undefined {
|
||||
// filter packagesForBoard selecting matches from the cli (installed packages)
|
||||
// and matches based on the board name
|
||||
// NOTE: this ensures the Deprecated & new packages are all in the array
|
||||
// so that we can check if any of the valid packages is already installed
|
||||
const packagesForBoard = packages.filter(
|
||||
(pkg) =>
|
||||
BoardsPackage.contains(selectedBoard, pkg) ||
|
||||
pkg.boards.some((board) => board.name === selectedBoard.name)
|
||||
);
|
||||
|
||||
// check if one of the packages for the board is already installed. if so, no hint
|
||||
if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// filter the installable (not installed) packages,
|
||||
// CLI returns the packages already sorted with the deprecated ones at the end of the list
|
||||
// in order to ensure the new ones are preferred
|
||||
const candidates = packagesForBoard.filter(
|
||||
({ installable, installedVersion }) => installable && !installedVersion
|
||||
);
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
private showAutoInstallPrompt(
|
||||
candidate: BoardsPackage,
|
||||
selectedBoard: Board,
|
||||
selectedPort?: Port
|
||||
): void {
|
||||
const candidateName = candidate.name;
|
||||
const version = candidate.availableVersions[0]
|
||||
? `[v ${candidate.availableVersions[0]}]`
|
||||
: '';
|
||||
|
||||
const info = this.generatePromptInfoText(
|
||||
candidateName,
|
||||
version,
|
||||
selectedBoard.name
|
||||
);
|
||||
|
||||
const actions = this.createPromptActions(candidate);
|
||||
|
||||
const onRefuse = () => {
|
||||
this.setLastRefusedPromptInfo(candidate.id, selectedPort);
|
||||
};
|
||||
const handleAction = this.createOnAnswerHandler(actions, onRefuse);
|
||||
|
||||
const onAnswer = (answer: string) => {
|
||||
this.removeNotificationByBoard(selectedBoard);
|
||||
|
||||
handleAction(answer);
|
||||
};
|
||||
|
||||
this.messageService
|
||||
.info(info, ...actions.map((action) => action.key))
|
||||
.then(onAnswer);
|
||||
}
|
||||
|
||||
private generatePromptInfoText(
|
||||
candidateName: string,
|
||||
version: string,
|
||||
boardName: string
|
||||
): string {
|
||||
return nls.localize(
|
||||
'arduino/board/installNow',
|
||||
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
|
||||
candidateName,
|
||||
version,
|
||||
boardName
|
||||
);
|
||||
}
|
||||
|
||||
private createPromptActions(
|
||||
candidate: BoardsPackage
|
||||
): AutoInstallPromptActions {
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
|
||||
const actions: AutoInstallPromptActions = [
|
||||
{
|
||||
key: InstallManually,
|
||||
handler: () => {
|
||||
this.boardsManagerFrontendContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) =>
|
||||
widget.refresh({
|
||||
query: candidate.name.toLocaleLowerCase(),
|
||||
type: 'All',
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
isAcceptance: true,
|
||||
key: yes,
|
||||
handler: () => {
|
||||
return Installable.installWithProgress({
|
||||
installable: this.boardsService,
|
||||
item: candidate,
|
||||
messageService: this.messageService,
|
||||
responseService: this.responseService,
|
||||
version: candidate.availableVersions[0],
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private createOnAnswerHandler(
|
||||
actions: AutoInstallPromptActions,
|
||||
onRefuse?: () => void
|
||||
): (answer: string) => void {
|
||||
return (answer) => {
|
||||
const actionToHandle = actions.find((action) => action.key === answer);
|
||||
actionToHandle?.handler();
|
||||
|
||||
if (!actionToHandle?.isAcceptance && onRefuse) {
|
||||
onRefuse();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
injectable,
|
||||
inject,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
@@ -28,7 +32,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
@inject(BoardsConfigDialogProps)
|
||||
protected override readonly props: BoardsConfigDialogProps
|
||||
) {
|
||||
super(props);
|
||||
super({ ...props, maxWidth: 500 });
|
||||
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
@@ -65,14 +69,6 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
const head = document.createElement('div');
|
||||
head.classList.add('head');
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = nls.localize(
|
||||
'arduino/board/configDialogTitle',
|
||||
'Select Other Board & Port'
|
||||
);
|
||||
title.classList.add('title');
|
||||
head.appendChild(title);
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.classList.add('text');
|
||||
head.appendChild(text);
|
||||
|
@@ -113,7 +113,7 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
}),
|
||||
this.props.notificationCenter.onAttachedBoardsChanged((event) =>
|
||||
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
|
||||
this.updatePorts(
|
||||
event.newState.ports,
|
||||
AttachedBoardsChangeEvent.diff(event).detached.ports
|
||||
@@ -126,19 +126,19 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
),
|
||||
this.props.notificationCenter.onPlatformInstalled(() =>
|
||||
this.props.notificationCenter.onPlatformDidInstall(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onPlatformUninstalled(() =>
|
||||
this.props.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onIndexUpdated(() =>
|
||||
this.props.notificationCenter.onIndexDidUpdate(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onDaemonStarted(() =>
|
||||
this.props.notificationCenter.onDaemonDidStart(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onDaemonStopped(() =>
|
||||
this.props.notificationCenter.onDaemonDidStop(() =>
|
||||
this.setState({ searchResults: [] })
|
||||
),
|
||||
this.props.onFilteredTextDidChangeEvent((query) =>
|
||||
@@ -258,14 +258,14 @@ export class BoardsConfig extends React.Component<
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className="body">
|
||||
<>
|
||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
||||
{this.renderContainer(
|
||||
'ports',
|
||||
this.renderPorts.bind(this),
|
||||
this.renderPortsFooter.bind(this)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,7 +306,10 @@ export class BoardsConfig extends React.Component<
|
||||
type="search"
|
||||
value={query}
|
||||
className="theia-input"
|
||||
placeholder="SEARCH BOARD"
|
||||
placeholder={nls.localize(
|
||||
'arduino/board/searchBoard',
|
||||
'Search board'
|
||||
)}
|
||||
onChange={this.updateBoards}
|
||||
ref={this.focusNodeSet}
|
||||
/>
|
||||
@@ -334,27 +337,19 @@ export class BoardsConfig extends React.Component<
|
||||
if (this.state.showAllPorts) {
|
||||
ports = this.state.knownPorts;
|
||||
} else {
|
||||
ports = this.state.knownPorts.filter((port) => {
|
||||
if (port.protocol === 'serial') {
|
||||
return true;
|
||||
}
|
||||
// All other ports with different protocol are
|
||||
// only shown if there is a recognized board
|
||||
// connected
|
||||
for (const board of this.availableBoards) {
|
||||
if (board.port?.address === port.address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ports = this.state.knownPorts.filter(
|
||||
Port.visiblePorts(this.availableBoards)
|
||||
);
|
||||
}
|
||||
return !ports.length ? (
|
||||
<div className="loading noselect">No ports discovered</div>
|
||||
<div className="loading noselect">
|
||||
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ports list">
|
||||
{ports.map((port) => (
|
||||
<Item<Port>
|
||||
key={`${port.id}`}
|
||||
key={`${Port.keyOf(port)}`}
|
||||
item={port}
|
||||
label={Port.toString(port)}
|
||||
selected={Port.sameAs(this.state.selectedPort, port)}
|
||||
@@ -418,53 +413,5 @@ export namespace BoardsConfig {
|
||||
const { name } = selectedBoard;
|
||||
return `${name}${port ? ` at ${port.address}` : ''}`;
|
||||
}
|
||||
|
||||
export function setConfig(
|
||||
config: Config | undefined,
|
||||
urlToAttachTo: URL
|
||||
): URL {
|
||||
const copy = new URL(urlToAttachTo.toString());
|
||||
if (!config) {
|
||||
copy.searchParams.delete('boards-config');
|
||||
return copy;
|
||||
}
|
||||
|
||||
const selectedBoard = config.selectedBoard
|
||||
? {
|
||||
name: config.selectedBoard.name,
|
||||
fqbn: config.selectedBoard.fqbn,
|
||||
}
|
||||
: undefined;
|
||||
const selectedPort = config.selectedPort
|
||||
? {
|
||||
protocol: config.selectedPort.protocol,
|
||||
address: config.selectedPort.address,
|
||||
}
|
||||
: undefined;
|
||||
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
|
||||
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function getConfig(url: URL): Config | undefined {
|
||||
const encoded = url.searchParams.get('boards-config');
|
||||
if (!encoded) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = decodeURIComponent(encoded);
|
||||
const candidate = JSON.parse(raw);
|
||||
if (typeof candidate === 'object') {
|
||||
return candidate;
|
||||
}
|
||||
console.warn(
|
||||
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
|
||||
);
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
console.log(`Could not get board config from URL: ${url}.`, e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
|
||||
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
|
||||
let shouldFireChanged = false;
|
||||
for (const fqbn of item.boards
|
||||
.map(({ fqbn }) => fqbn)
|
||||
|
@@ -1,21 +1,27 @@
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(ListItemRenderer)
|
||||
protected itemRenderer: ListItemRenderer<BoardsPackage>
|
||||
@inject(BoardsService) service: BoardsService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
||||
) {
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
@@ -26,6 +32,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,10 +41,10 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformInstalled(() =>
|
||||
this.notificationCenter.onPlatformDidInstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
this.notificationCenter.onPlatformUninstalled(() =>
|
||||
this.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
]);
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
Command,
|
||||
CommandContribution,
|
||||
CommandRegistry,
|
||||
CommandService,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
@@ -13,16 +18,28 @@ import {
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardWithPackage,
|
||||
BoardUserField,
|
||||
AvailablePorts,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { naturalCompare } from '../../common/utils';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import { StorageWrapper } from '../storage-wrapper';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { Unknown } from '../../common/nls';
|
||||
import {
|
||||
StartupTask,
|
||||
StartupTaskProvider,
|
||||
} from '../../electron-common/startup-task';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
export class BoardsServiceProvider
|
||||
implements
|
||||
FrontendApplicationContribution,
|
||||
StartupTaskProvider,
|
||||
CommandContribution
|
||||
{
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@@ -38,12 +55,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
@inject(NotificationCenter)
|
||||
protected notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected readonly onBoardsConfigChangedEmitter =
|
||||
new Emitter<BoardsConfig.Config>();
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
|
||||
AvailableBoard[]
|
||||
>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
|
||||
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
|
||||
|
||||
/**
|
||||
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
||||
@@ -61,11 +82,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
protected _availablePorts: Port[] = [];
|
||||
protected _availableBoards: AvailableBoard[] = [];
|
||||
|
||||
private lastBoardsConfigOnUpload: BoardsConfig.Config | undefined;
|
||||
private lastAvailablePortsOnUpload: Port[] | undefined;
|
||||
private boardConfigToAutoSelect: BoardsConfig.Config | undefined;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* Unlike `onAttachedBoardsChanged` this event fires when the user modifies the selected board in the IDE.\
|
||||
* This event 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.\
|
||||
* currently selected board.
|
||||
*
|
||||
* This event is also emitted when the board package for the currently selected board was uninstalled.
|
||||
*/
|
||||
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
||||
@@ -73,29 +99,126 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
this.onAvailableBoardsChangedEmitter.event;
|
||||
readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event;
|
||||
|
||||
private readonly _reconciled = new Deferred<void>();
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onAttachedBoardsChanged(
|
||||
this.notificationCenter.onAttachedBoardsDidChange(
|
||||
this.notifyAttachedBoardsChanged.bind(this)
|
||||
);
|
||||
this.notificationCenter.onPlatformInstalled(
|
||||
this.notificationCenter.onPlatformDidInstall(
|
||||
this.notifyPlatformInstalled.bind(this)
|
||||
);
|
||||
this.notificationCenter.onPlatformUninstalled(
|
||||
this.notificationCenter.onPlatformDidUninstall(
|
||||
this.notifyPlatformUninstalled.bind(this)
|
||||
);
|
||||
|
||||
Promise.all([
|
||||
this.boardsService.getAttachedBoards(),
|
||||
this.boardsService.getAvailablePorts(),
|
||||
this.loadState(),
|
||||
]).then(([attachedBoards, availablePorts]) => {
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const [state] = await Promise.all([
|
||||
this.boardsService.getState(),
|
||||
this.loadState(),
|
||||
]);
|
||||
const { boards: attachedBoards, ports: availablePorts } =
|
||||
AvailablePorts.split(state);
|
||||
this._attachedBoards = attachedBoards;
|
||||
this._availablePorts = availablePorts;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
|
||||
await this.reconcileAvailableBoards();
|
||||
|
||||
this.tryReconnect();
|
||||
this._reconciled.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(USE_INHERITED_CONFIG, {
|
||||
execute: (inheritedConfig: BoardsConfig.Config) =>
|
||||
this.inheritedConfig.resolve(inheritedConfig),
|
||||
});
|
||||
}
|
||||
|
||||
get reconciled(): Promise<void> {
|
||||
return this._reconciled.promise;
|
||||
}
|
||||
|
||||
snapshotBoardDiscoveryOnUpload(): void {
|
||||
this.lastBoardsConfigOnUpload = this._boardsConfig;
|
||||
this.lastAvailablePortsOnUpload = this._availablePorts;
|
||||
}
|
||||
|
||||
clearBoardDiscoverySnapshot(): void {
|
||||
this.lastBoardsConfigOnUpload = undefined;
|
||||
this.lastAvailablePortsOnUpload = undefined;
|
||||
}
|
||||
|
||||
private portToAutoSelectCanBeDerived(): boolean {
|
||||
return Boolean(
|
||||
this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload
|
||||
);
|
||||
}
|
||||
|
||||
attemptPostUploadAutoSelect(): void {
|
||||
setTimeout(() => {
|
||||
if (this.portToAutoSelectCanBeDerived()) {
|
||||
this.attemptAutoSelect({
|
||||
ports: this._availablePorts,
|
||||
boards: this._availableBoards,
|
||||
});
|
||||
}
|
||||
}, 2000); // 2 second delay same as IDE 1.8
|
||||
}
|
||||
|
||||
private attemptAutoSelect(
|
||||
newState: AttachedBoardsChangeEvent['newState']
|
||||
): void {
|
||||
this.deriveBoardConfigToAutoSelect(newState);
|
||||
this.tryReconnect();
|
||||
}
|
||||
|
||||
private deriveBoardConfigToAutoSelect(
|
||||
newState: AttachedBoardsChangeEvent['newState']
|
||||
): void {
|
||||
if (!this.portToAutoSelectCanBeDerived()) {
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPorts = this.lastAvailablePortsOnUpload!;
|
||||
const { ports: newPorts, boards: newBoards } = newState;
|
||||
|
||||
const appearedPorts =
|
||||
oldPorts.length > 0
|
||||
? newPorts.filter((newPort: Port) =>
|
||||
oldPorts.every((oldPort: Port) => !Port.sameAs(newPort, oldPort))
|
||||
)
|
||||
: newPorts;
|
||||
|
||||
for (const port of appearedPorts) {
|
||||
const boardOnAppearedPort = newBoards.find((board: Board) =>
|
||||
Port.sameAs(board.port, port)
|
||||
);
|
||||
|
||||
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload!;
|
||||
|
||||
if (
|
||||
boardOnAppearedPort &&
|
||||
lastBoardsConfigOnUpload.selectedBoard &&
|
||||
Board.sameAs(
|
||||
boardOnAppearedPort,
|
||||
lastBoardsConfigOnUpload.selectedBoard
|
||||
)
|
||||
) {
|
||||
this.clearBoardDiscoverySnapshot();
|
||||
|
||||
this.boardConfigToAutoSelect = {
|
||||
selectedBoard: boardOnAppearedPort,
|
||||
selectedPort: port,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected notifyAttachedBoardsChanged(
|
||||
event: AttachedBoardsChangeEvent
|
||||
): void {
|
||||
@@ -104,10 +227,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
this.logger.info(AttachedBoardsChangeEvent.toString(event));
|
||||
this.logger.info('------------------------------------------');
|
||||
}
|
||||
|
||||
this._attachedBoards = event.newState.boards;
|
||||
this._availablePorts = event.newState.ports;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
this.reconcileAvailableBoards().then(() => {
|
||||
const { uploadInProgress } = event;
|
||||
// avoid attempting "auto-selection" while an
|
||||
// upload is in progress
|
||||
if (!uploadInProgress) {
|
||||
this.attemptAutoSelect(event.newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||
@@ -155,7 +286,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
.then(async (answer) => {
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand(
|
||||
ArduinoCommands.OPEN_BOARDS_DIALOG.id,
|
||||
'arduino-open-boards-dialog',
|
||||
selectedBoard.name
|
||||
);
|
||||
}
|
||||
@@ -185,8 +316,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
|
||||
? selectedBoard
|
||||
: this._availableBoards.find((availableBoard) =>
|
||||
Board.sameAs(availableBoard, selectedBoard)
|
||||
);
|
||||
Board.sameAs(availableBoard, selectedBoard)
|
||||
);
|
||||
if (
|
||||
selectedAvailableBoard &&
|
||||
selectedAvailableBoard.selected &&
|
||||
@@ -209,7 +340,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
}
|
||||
}
|
||||
|
||||
protected async tryReconnect(): Promise<boolean> {
|
||||
protected tryReconnect(): boolean {
|
||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||
for (const board of this.availableBoards.filter(
|
||||
({ state }) => state !== AvailableBoard.State.incomplete
|
||||
@@ -223,23 +354,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
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.latestValidBoardsConfig.selectedPort.protocol === board.port?.protocol
|
||||
) {
|
||||
this.boardsConfig = {
|
||||
...this.latestValidBoardsConfig,
|
||||
selectedPort: board.port,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.boardConfigToAutoSelect) return false;
|
||||
|
||||
this.boardsConfig = this.boardConfigToAutoSelect;
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -258,7 +378,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
}
|
||||
|
||||
protected setBoardsConfig(config: BoardsConfig.Config): void {
|
||||
this.logger.info('Board config changed: ', JSON.stringify(config));
|
||||
this.logger.debug('Board config changed: ', JSON.stringify(config));
|
||||
this._boardsConfig = config;
|
||||
this.latestBoardsConfig = this._boardsConfig;
|
||||
if (this.canUploadTo(this._boardsConfig)) {
|
||||
@@ -364,6 +484,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
return this._availableBoards;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Do not use this API, it will be removed. This is a hack to be able to set the missing port `properties` before an upload.
|
||||
*
|
||||
* See: https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236.
|
||||
*/
|
||||
// TODO: remove this API and fix the selected board config store/restore correctly.
|
||||
get availablePorts(): Port[] {
|
||||
return this._availablePorts.slice();
|
||||
}
|
||||
|
||||
async waitUntilAvailable(
|
||||
what: Board & { port: Port },
|
||||
timeout?: number
|
||||
@@ -376,14 +506,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
const timeoutTask =
|
||||
!!timeout && timeout > 0
|
||||
? new Promise<void>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`Timeout after ${timeout} ms.`)),
|
||||
timeout
|
||||
setTimeout(
|
||||
() => reject(new Error(`Timeout after ${timeout} ms.`)),
|
||||
timeout
|
||||
)
|
||||
)
|
||||
)
|
||||
: new Promise<void>(() => {
|
||||
/* never */
|
||||
});
|
||||
/* never */
|
||||
});
|
||||
const waitUntilTask = new Promise<void>((resolve) => {
|
||||
let candidate = find(what, this.availableBoards);
|
||||
if (candidate) {
|
||||
@@ -420,28 +550,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
const currentAvailableBoards = this._availableBoards;
|
||||
const availableBoards: AvailableBoard[] = [];
|
||||
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
|
||||
const availableBoardPorts = availablePorts.filter((port) => {
|
||||
if (port.protocol === 'serial') {
|
||||
// We always show all serial ports, even if there
|
||||
// is no recognized board connected to it
|
||||
return true;
|
||||
}
|
||||
|
||||
// All other ports with different protocol are
|
||||
// only shown if there is a recognized board
|
||||
// connected
|
||||
for (const board of attachedBoards) {
|
||||
if (board.port?.address === port.address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const availableBoardPorts = availablePorts.filter(
|
||||
Port.visiblePorts(attachedBoards)
|
||||
);
|
||||
|
||||
for (const boardPort of availableBoardPorts) {
|
||||
const board = attachedBoards.find(({ port }) =>
|
||||
Port.sameAs(boardPort, port)
|
||||
);
|
||||
// "board" will always be falsey for
|
||||
// port that was originally mapped
|
||||
// to unknown board and then selected
|
||||
// manually by user
|
||||
|
||||
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
|
||||
boardPort
|
||||
);
|
||||
@@ -460,12 +581,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
availableBoard = {
|
||||
...lastSelectedBoard,
|
||||
state: AvailableBoard.State.guessed,
|
||||
selected: BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard),
|
||||
selected:
|
||||
BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard) &&
|
||||
Port.sameAs(boardPort, boardsConfig.selectedPort), // to avoid double selection
|
||||
port: boardPort,
|
||||
};
|
||||
} else {
|
||||
availableBoard = {
|
||||
name: nls.localize('arduino/common/unknown', 'Unknown'),
|
||||
name: Unknown,
|
||||
port: boardPort,
|
||||
state: AvailableBoard.State.incomplete,
|
||||
};
|
||||
@@ -475,8 +598,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
|
||||
if (
|
||||
boardsConfig.selectedBoard &&
|
||||
!availableBoards.some(({ selected }) => selected)
|
||||
availableBoards.every(({ selected }) => !selected)
|
||||
) {
|
||||
let port = boardsConfig.selectedPort
|
||||
// If the selected board has the same port of an unknown board
|
||||
// that is already in availableBoards we might get a duplicate port.
|
||||
// So we remove the one already in the array and add the selected one.
|
||||
@@ -484,11 +608,15 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
(board) => board.port?.address === boardsConfig.selectedPort?.address
|
||||
);
|
||||
if (found >= 0) {
|
||||
// get the "Unknown board port" that we will substitute,
|
||||
// then we can include it in the "availableBoard object"
|
||||
// pushed below; to ensure addressLabel is included
|
||||
port = availableBoards[found].port
|
||||
availableBoards.splice(found, 1);
|
||||
}
|
||||
availableBoards.push({
|
||||
...boardsConfig.selectedBoard,
|
||||
port: boardsConfig.selectedPort,
|
||||
port,
|
||||
selected: true,
|
||||
state: AvailableBoard.State.incomplete,
|
||||
});
|
||||
@@ -500,6 +628,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
for (let i = 0; !hasChanged && i < availableBoards.length; i++) {
|
||||
const [left, right] = [availableBoards[i], currentAvailableBoards[i]];
|
||||
hasChanged =
|
||||
left.fqbn !== right.fqbn ||
|
||||
!!AvailableBoard.compare(left, right) ||
|
||||
left.selected !== right.selected;
|
||||
}
|
||||
@@ -534,8 +663,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
|
||||
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
|
||||
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
|
||||
return `last-selected-board-on-port:${typeof port === 'string' ? port : port.address
|
||||
}`;
|
||||
return `last-selected-board-on-port:${
|
||||
typeof port === 'string' ? port : port.address
|
||||
}`;
|
||||
}
|
||||
|
||||
protected async loadState(): Promise<void> {
|
||||
@@ -552,11 +682,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
let storedLatestBoardsConfig = await this.getData<
|
||||
BoardsConfig.Config | undefined
|
||||
>('latest-boards-config');
|
||||
// Try to get from the URL if it was not persisted.
|
||||
// Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives.
|
||||
if (!storedLatestBoardsConfig) {
|
||||
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(
|
||||
new URL(window.location.href)
|
||||
);
|
||||
storedLatestBoardsConfig = await Promise.race([
|
||||
this.inheritedConfig.promise,
|
||||
new Promise<undefined>((resolve) =>
|
||||
setTimeout(() => resolve(undefined), 2_000)
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (storedLatestBoardsConfig) {
|
||||
this.latestBoardsConfig = storedLatestBoardsConfig;
|
||||
@@ -579,8 +712,31 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
tasks(): StartupTask[] {
|
||||
return [
|
||||
{
|
||||
command: USE_INHERITED_CONFIG.id,
|
||||
args: [this.boardsConfig],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It should be neither visible nor called from outside.
|
||||
*
|
||||
* This service creates a startup task with the current board config and
|
||||
* passes the task to the electron-main process so that the new window
|
||||
* can inherit the boards config state of this service.
|
||||
*
|
||||
* Note that the state is always set, but new windows might ignore it.
|
||||
* For example, the new window already has a valid boards config persisted to the local storage.
|
||||
*/
|
||||
const USE_INHERITED_CONFIG: Command = {
|
||||
id: 'arduino-use-inherited-boards-config',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
|
@@ -3,13 +3,14 @@ import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Port } from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import { OpenBoardsConfig } from '../contributions/open-boards-config';
|
||||
import {
|
||||
BoardsServiceProvider,
|
||||
AvailableBoard,
|
||||
} from './boards-service-provider';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import classNames from 'classnames';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
|
||||
export interface BoardsDropDownListCoords {
|
||||
readonly top: number;
|
||||
@@ -28,10 +29,12 @@ export namespace BoardsDropDown {
|
||||
|
||||
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
protected dropdownElement: HTMLElement;
|
||||
private listRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: BoardsDropDown.Props) {
|
||||
super(props);
|
||||
|
||||
this.listRef = React.createRef();
|
||||
let list = document.getElementById('boards-dropdown-container');
|
||||
if (!list) {
|
||||
list = document.createElement('div');
|
||||
@@ -41,6 +44,12 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidUpdate(prevProps: BoardsDropDown.Props): void {
|
||||
if (prevProps.coords === 'hidden' && this.listRef.current) {
|
||||
this.listRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
|
||||
}
|
||||
@@ -61,21 +70,22 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
position: 'absolute',
|
||||
...coords,
|
||||
}}
|
||||
ref={this.listRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
{items
|
||||
.map(({ name, port, selected, onClick }) => ({
|
||||
label: nls.localize(
|
||||
'arduino/board/boardListItem',
|
||||
'{0} at {1}',
|
||||
name,
|
||||
Port.toString(port)
|
||||
),
|
||||
selected,
|
||||
onClick,
|
||||
}))
|
||||
.map(this.renderItem)}
|
||||
<div className="arduino-boards-dropdown-list--items-container">
|
||||
{items
|
||||
.map(({ name, port, selected, onClick }) => ({
|
||||
boardLabel: name,
|
||||
port,
|
||||
selected,
|
||||
onClick,
|
||||
}))
|
||||
.map(this.renderItem)}
|
||||
</div>
|
||||
<div
|
||||
key={footerLabel}
|
||||
tabIndex={0}
|
||||
className="arduino-boards-dropdown-item arduino-board-dropdown-footer"
|
||||
onClick={() => this.props.openBoardsConfig()}
|
||||
>
|
||||
@@ -86,22 +96,52 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
}
|
||||
|
||||
protected renderItem({
|
||||
label,
|
||||
boardLabel,
|
||||
port,
|
||||
selected,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
boardLabel: string;
|
||||
port: Port;
|
||||
selected?: boolean;
|
||||
onClick: () => void;
|
||||
}): React.ReactNode {
|
||||
const protocolIcon = iconNameFromProtocol(port.protocol);
|
||||
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`}
|
||||
key={`board-item--${boardLabel}-${port.address}`}
|
||||
className={classNames('arduino-boards-dropdown-item', {
|
||||
'arduino-boards-dropdown-item--selected': selected,
|
||||
})}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div>{label}</div>
|
||||
{selected ? <span className="fa fa-check" /> : ''}
|
||||
<div
|
||||
className={classNames(
|
||||
'arduino-boards-dropdown-item--protocol',
|
||||
'fa',
|
||||
protocolIcon
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="arduino-boards-dropdown-item--label"
|
||||
title={`${boardLabel}\n${port.address}`}
|
||||
>
|
||||
<div className="arduino-boards-dropdown-item--board-label noWrapInfo noselect">
|
||||
{boardLabel}
|
||||
</div>
|
||||
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
|
||||
{port.addressLabel}
|
||||
</div>
|
||||
</div>
|
||||
{selected ? <div className="fa fa-check" /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -119,7 +159,7 @@ export class BoardsToolBarItem extends React.Component<
|
||||
constructor(props: BoardsToolBarItem.Props) {
|
||||
super(props);
|
||||
|
||||
const { availableBoards } = props.boardsServiceClient;
|
||||
const { availableBoards } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
availableBoards,
|
||||
coords: 'hidden',
|
||||
@@ -131,8 +171,8 @@ export class BoardsToolBarItem extends React.Component<
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
|
||||
this.setState({ availableBoards })
|
||||
this.props.boardsServiceProvider.onAvailableBoardsChanged(
|
||||
(availableBoards) => this.setState({ availableBoards })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,7 +180,7 @@ export class BoardsToolBarItem extends React.Component<
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
|
||||
protected readonly show = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
const { currentTarget: element } = event;
|
||||
if (element instanceof HTMLElement) {
|
||||
if (this.state.coords === 'hidden') {
|
||||
@@ -163,36 +203,43 @@ export class BoardsToolBarItem extends React.Component<
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { coords, availableBoards } = this.state;
|
||||
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
|
||||
const title = BoardsConfig.Config.toString(boardsConfig, {
|
||||
default: nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
),
|
||||
});
|
||||
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 '';
|
||||
})();
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.props.boardsServiceProvider.boardsConfig;
|
||||
|
||||
const boardLabel =
|
||||
selectedBoard?.name ||
|
||||
nls.localize('arduino/board/selectBoard', 'Select Board');
|
||||
const selectedPortLabel = portLabel(selectedPort?.address);
|
||||
|
||||
const isConnected = Boolean(selectedBoard && selectedPort);
|
||||
const protocolIcon = isConnected
|
||||
? iconNameFromProtocol(selectedPort?.protocol || '')
|
||||
: null;
|
||||
const protocolIconClassNames = classNames(
|
||||
'arduino-boards-toolbar-item--protocol',
|
||||
'fa',
|
||||
protocolIcon
|
||||
);
|
||||
|
||||
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={decorator} />
|
||||
<div className="label noWrapInfo">
|
||||
<div className="noWrapInfo noselect">{title}</div>
|
||||
</div>
|
||||
<span className="fa fa-caret-down caret" />
|
||||
</div>
|
||||
<div
|
||||
className="arduino-boards-toolbar-item-container"
|
||||
title={selectedPortLabel}
|
||||
onClick={this.show}
|
||||
>
|
||||
{protocolIcon && <div className={protocolIconClassNames} />}
|
||||
<div
|
||||
className={classNames(
|
||||
'arduino-boards-toolbar-item--label',
|
||||
'noWrapInfo',
|
||||
'noselect',
|
||||
{ 'arduino-boards-toolbar-item--label-connected': isConnected }
|
||||
)}
|
||||
>
|
||||
{boardLabel}
|
||||
</div>
|
||||
<div className="fa fa-caret-down caret" />
|
||||
</div>
|
||||
<BoardsDropDown
|
||||
coords={coords}
|
||||
@@ -201,17 +248,20 @@ export class BoardsToolBarItem extends React.Component<
|
||||
.map((board) => ({
|
||||
...board,
|
||||
onClick: () => {
|
||||
if (board.state === AvailableBoard.State.incomplete) {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
if (!board.fqbn) {
|
||||
const previousBoardConfig =
|
||||
this.props.boardsServiceProvider.boardsConfig;
|
||||
this.props.boardsServiceProvider.boardsConfig = {
|
||||
selectedPort: board.port,
|
||||
};
|
||||
this.openDialog();
|
||||
this.openDialog(previousBoardConfig);
|
||||
} else {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
this.props.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: board.port,
|
||||
};
|
||||
}
|
||||
this.setState({ coords: 'hidden' });
|
||||
},
|
||||
}))}
|
||||
openBoardsConfig={this.openDialog}
|
||||
@@ -220,14 +270,25 @@ export class BoardsToolBarItem extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
protected openDialog = () => {
|
||||
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
|
||||
this.setState({ coords: 'hidden' });
|
||||
protected openDialog = async (
|
||||
previousBoardConfig?: BoardsConfig.Config
|
||||
): Promise<void> => {
|
||||
const selectedBoardConfig =
|
||||
await this.props.commands.executeCommand<BoardsConfig.Config>(
|
||||
OpenBoardsConfig.Commands.OPEN_DIALOG.id
|
||||
);
|
||||
if (
|
||||
previousBoardConfig &&
|
||||
(!selectedBoardConfig?.selectedPort ||
|
||||
!selectedBoardConfig?.selectedBoard)
|
||||
) {
|
||||
this.props.boardsServiceProvider.boardsConfig = previousBoardConfig;
|
||||
}
|
||||
};
|
||||
}
|
||||
export namespace BoardsToolBarItem {
|
||||
export interface Props {
|
||||
readonly boardsServiceClient: BoardsServiceProvider;
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
@@ -236,3 +297,26 @@ export namespace BoardsToolBarItem {
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function iconNameFromProtocol(protocol: string): string {
|
||||
switch (protocol) {
|
||||
case 'serial':
|
||||
return 'fa-arduino-technology-usb';
|
||||
case 'network':
|
||||
return 'fa-arduino-technology-connection';
|
||||
/*
|
||||
Bluetooth ports are not listed yet from the CLI;
|
||||
Not sure about the naming ('bluetooth'); make sure it's correct before uncommenting the following lines
|
||||
*/
|
||||
// case 'bluetooth':
|
||||
// return 'fa-arduino-technology-bluetooth';
|
||||
default:
|
||||
return 'fa-arduino-technology-3dimensionscube';
|
||||
}
|
||||
}
|
||||
|
||||
function portLabel(portName?: string): string {
|
||||
return portName
|
||||
? nls.localize('arduino/board/portLabel', 'Port: {0}', portName)
|
||||
: nls.localize('arduino/board/disconnected', 'Disconnected');
|
||||
}
|
||||
|
@@ -1,10 +1,16 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import { BoardsPackage } from '../../common/protocol/boards-service';
|
||||
import type {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
BoardsPackage,
|
||||
BoardSearch
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
|
@@ -4,11 +4,8 @@ import URI from '@theia/core/lib/common/uri';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
Installable,
|
||||
LibraryService,
|
||||
ResponseServiceArduino,
|
||||
} from '../../common/protocol';
|
||||
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
@@ -22,8 +19,8 @@ export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceArduino)
|
||||
protected readonly responseService: ResponseServiceArduino;
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
@@ -88,7 +85,7 @@ export class AddZipLibrary extends SketchContribution {
|
||||
|
||||
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
||||
try {
|
||||
await Installable.doWithProgress({
|
||||
await ExecuteWithProgress.doWithProgress({
|
||||
messageService: this.messageService,
|
||||
progressText:
|
||||
nls.localize('arduino/common/processing', 'Processing') +
|
||||
|
@@ -101,8 +101,8 @@ PID: ${PID}`;
|
||||
}
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onPlatformInstalled(() => this.updateMenus());
|
||||
this.notificationCenter.onPlatformUninstalled(() => this.updateMenus());
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus());
|
||||
this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus());
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
|
||||
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
|
||||
this.updateMenus()
|
||||
@@ -288,7 +288,7 @@ PID: ${PID}`;
|
||||
for (let i = 0; i < sortedIDs.length; i++) {
|
||||
const portID = sortedIDs[i];
|
||||
const [port, boards] = ports[portID];
|
||||
let label = `${port.address}`;
|
||||
let label = `${port.addressLabel}`;
|
||||
if (boards.length) {
|
||||
const boardsList = boards.map((board) => board.name).join(', ');
|
||||
label = `${label} (${boardsList})`;
|
||||
@@ -331,7 +331,7 @@ PID: ${PID}`;
|
||||
}
|
||||
};
|
||||
|
||||
const grouped = AvailablePorts.byProtocol(availablePorts);
|
||||
const grouped = AvailablePorts.groupByProtocol(availablePorts);
|
||||
let protocolOrder = 100;
|
||||
// We first show serial and network ports, then all the rest
|
||||
['serial', 'network'].forEach((protocol) => {
|
||||
|
@@ -1,23 +1,16 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
CoreServiceContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
CoreServiceContribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class BurnBootloader extends CoreServiceContribution {
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
||||
execute: () => this.burnBootloader(),
|
||||
@@ -35,32 +28,20 @@ export class BurnBootloader extends CoreServiceContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async burnBootloader(): Promise<void> {
|
||||
private async burnBootloader(): Promise<void> {
|
||||
this.clearVisibleNotification();
|
||||
const options = await this.options();
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const port = boardsConfig.selectedPort;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(
|
||||
boardsConfig.selectedBoard?.fqbn
|
||||
),
|
||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
|
||||
const board = {
|
||||
...boardsConfig.selectedBoard,
|
||||
name: boardsConfig.selectedBoard?.name || '',
|
||||
fqbn,
|
||||
};
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
await this.coreService.burnBootloader({
|
||||
board,
|
||||
programmer,
|
||||
port,
|
||||
verify,
|
||||
verbose,
|
||||
await this.doWithProgress({
|
||||
progressText: nls.localize(
|
||||
'arduino/bootloader/burningBootloader',
|
||||
'Burning bootloader...'
|
||||
),
|
||||
task: (progressId, coreService) =>
|
||||
coreService.burnBootloader({
|
||||
...options,
|
||||
progressId,
|
||||
}),
|
||||
});
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
@@ -75,6 +56,27 @@ export class BurnBootloader extends CoreServiceContribution {
|
||||
this.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async options(): Promise<CoreService.Options.Bootloader> {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const port = boardsConfig.selectedPort;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(
|
||||
boardsConfig.selectedBoard?.fqbn
|
||||
),
|
||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
return {
|
||||
fqbn,
|
||||
programmer,
|
||||
port,
|
||||
verify,
|
||||
verbose,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BurnBootloader {
|
||||
|
@@ -0,0 +1,68 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
IDEUpdater,
|
||||
SKIP_IDE_VERSION,
|
||||
} from '../../common/protocol/ide-updater';
|
||||
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class CheckForIDEUpdates extends Contribution {
|
||||
@inject(IDEUpdater)
|
||||
private readonly updater: IDEUpdater;
|
||||
|
||||
@inject(IDEUpdaterDialog)
|
||||
private readonly updaterDialog: IDEUpdaterDialog;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorage: LocalStorageService;
|
||||
|
||||
override onStart(): void {
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, newValue, oldValue }) => {
|
||||
if (newValue !== oldValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.ide.updateChannel':
|
||||
case 'arduino.ide.updateBaseUrl':
|
||||
this.updater.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (!checkForUpdates) {
|
||||
return;
|
||||
}
|
||||
this.updater
|
||||
.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
)
|
||||
.then(() => this.updater.checkForUpdates(true))
|
||||
.then(async (updateInfo) => {
|
||||
if (!updateInfo) return;
|
||||
const versionToSkip = await this.localStorage.getData<string>(
|
||||
SKIP_IDE_VERSION
|
||||
);
|
||||
if (versionToSkip === updateInfo.version) return;
|
||||
this.updaterDialog.open(updateInfo);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/ide-updater/errorCheckingForUpdates',
|
||||
'Error while checking for Arduino IDE updates.\n{0}',
|
||||
e.message
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,221 @@
|
||||
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { InstallManually, Later } from '../../common/nls';
|
||||
import {
|
||||
ArduinoComponent,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
LibraryPackage,
|
||||
LibraryService,
|
||||
ResponseServiceClient,
|
||||
Searchable,
|
||||
} from '../../common/protocol';
|
||||
import { Installable } from '../../common/protocol/installable';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import type { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
|
||||
const NoUpdates = nls.localize(
|
||||
'arduino/checkForUpdates/noUpdates',
|
||||
'There are no recent updates available.'
|
||||
);
|
||||
const PromptUpdateBoards = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateBoards',
|
||||
'Updates are available for some of your boards.'
|
||||
);
|
||||
const PromptUpdateLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateLibraries',
|
||||
'Updates are available for some of your libraries.'
|
||||
);
|
||||
const UpdatingBoards = nls.localize(
|
||||
'arduino/checkForUpdates/updatingBoards',
|
||||
'Updating boards...'
|
||||
);
|
||||
const UpdatingLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/updatingLibraries',
|
||||
'Updating libraries...'
|
||||
);
|
||||
const InstallAll = nls.localize(
|
||||
'arduino/checkForUpdates/installAll',
|
||||
'Install All'
|
||||
);
|
||||
|
||||
interface Task<T extends ArduinoComponent> {
|
||||
readonly run: () => Promise<void>;
|
||||
readonly item: T;
|
||||
}
|
||||
|
||||
const Updatable = { type: 'Updatable' } as const;
|
||||
|
||||
@injectable()
|
||||
export class CheckForUpdates extends Contribution {
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowService: WindowServiceExt;
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
private readonly boardsContribution: BoardsListWidgetFrontendContribution;
|
||||
@inject(LibraryListWidgetFrontendContribution)
|
||||
private readonly librariesContribution: LibraryListWidgetFrontendContribution;
|
||||
|
||||
override registerCommands(register: CommandRegistry): void {
|
||||
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
||||
execute: () => this.checkForUpdates(false),
|
||||
});
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (checkForUpdates) {
|
||||
this.windowService.isFirstWindow().then((firstWindow) => {
|
||||
if (firstWindow) {
|
||||
this.checkForUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForUpdates(silent = true) {
|
||||
const [boardsPackages, libraryPackages] = await Promise.all([
|
||||
this.boardsService.search(Updatable),
|
||||
this.libraryService.search(Updatable),
|
||||
]);
|
||||
this.promptUpdateBoards(boardsPackages);
|
||||
this.promptUpdateLibraries(libraryPackages);
|
||||
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
||||
this.messageService.info(NoUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private promptUpdateBoards(items: BoardsPackage[]): void {
|
||||
this.prompt({
|
||||
items,
|
||||
installable: this.boardsService,
|
||||
viewContribution: this.boardsContribution,
|
||||
viewSearchOptions: { query: '', ...Updatable },
|
||||
promptMessage: PromptUpdateBoards,
|
||||
updatingMessage: UpdatingBoards,
|
||||
});
|
||||
}
|
||||
|
||||
private promptUpdateLibraries(items: LibraryPackage[]): void {
|
||||
this.prompt({
|
||||
items,
|
||||
installable: this.libraryService,
|
||||
viewContribution: this.librariesContribution,
|
||||
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
|
||||
promptMessage: PromptUpdateLibraries,
|
||||
updatingMessage: UpdatingLibraries,
|
||||
});
|
||||
}
|
||||
|
||||
private prompt<
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
>(options: {
|
||||
items: T[];
|
||||
installable: Installable<T>;
|
||||
viewContribution: AbstractViewContribution<ListWidget<T, S>>;
|
||||
viewSearchOptions: S;
|
||||
promptMessage: string;
|
||||
updatingMessage: string;
|
||||
}): void {
|
||||
const {
|
||||
items,
|
||||
installable,
|
||||
viewContribution,
|
||||
promptMessage: message,
|
||||
viewSearchOptions,
|
||||
updatingMessage,
|
||||
} = options;
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
this.messageService
|
||||
.info(message, Later, InstallManually, InstallAll)
|
||||
.then((answer) => {
|
||||
if (answer === InstallAll) {
|
||||
const tasks = items.map((item) =>
|
||||
this.createInstallTask(item, installable)
|
||||
);
|
||||
this.executeTasks(updatingMessage, tasks);
|
||||
} else if (answer === InstallManually) {
|
||||
viewContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) => widget.refresh(viewSearchOptions));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async executeTasks(
|
||||
message: string,
|
||||
tasks: Task<ArduinoComponent>[]
|
||||
): Promise<void> {
|
||||
if (tasks.length) {
|
||||
return ExecuteWithProgress.withProgress(
|
||||
message,
|
||||
this.messageService,
|
||||
async (progress) => {
|
||||
try {
|
||||
const total = tasks.length;
|
||||
let count = 0;
|
||||
for (const { run, item } of tasks) {
|
||||
try {
|
||||
await run(); // runs update sequentially. // TODO: is parallel update desired?
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.messageService.error(
|
||||
`Failed to update ${item.name}. ${err}`
|
||||
);
|
||||
} finally {
|
||||
progress.report({ work: { total, done: ++count } });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
progress.cancel();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private createInstallTask<T extends ArduinoComponent>(
|
||||
item: T,
|
||||
installable: Installable<T>
|
||||
): Task<T> {
|
||||
const latestVersion = item.availableVersions[0];
|
||||
return {
|
||||
item,
|
||||
run: () =>
|
||||
Installable.installWithProgress({
|
||||
installable,
|
||||
item,
|
||||
version: latestVersion,
|
||||
messageService: this.messageService,
|
||||
responseService: this.responseService,
|
||||
keepOutput: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
export namespace CheckForUpdates {
|
||||
export namespace Commands {
|
||||
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-check-for-updates',
|
||||
label: 'Check for Arduino Updates',
|
||||
category: 'Arduino',
|
||||
},
|
||||
'arduino/checkForUpdates/checkForUpdates'
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,9 +1,14 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import type { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import type {
|
||||
FrontendApplication,
|
||||
OnWillStopAction,
|
||||
} from '@theia/core/lib/browser/frontend-application';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -11,27 +16,48 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
/**
|
||||
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
|
||||
*/
|
||||
@injectable()
|
||||
export class Close extends SketchContribution {
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
private shell: ApplicationShell | undefined;
|
||||
|
||||
protected shell: ApplicationShell;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
override onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Close.Commands.CLOSE, {
|
||||
execute: () => remote.getCurrentWindow().close()
|
||||
execute: () => {
|
||||
// Close current editor if closeable.
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.title.closable) {
|
||||
currentEditor.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shell) {
|
||||
// Close current widget from the main area if possible.
|
||||
const { currentWidget } = this.shell;
|
||||
if (currentWidget) {
|
||||
const currentWidgetInMain = toArray(
|
||||
this.shell.mainPanel.widgets()
|
||||
).find((widget) => widget === currentWidget);
|
||||
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
||||
return currentWidgetInMain.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
return remote.getCurrentWindow().close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,6 +76,123 @@ export class Close extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
// `FrontendApplicationContribution#onWillStop`
|
||||
onWillStop(): OnWillStopAction {
|
||||
return {
|
||||
reason: 'save-sketch',
|
||||
action: () => {
|
||||
return this.showSaveSketchDialog();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If returns with `true`, IDE2 will close. Otherwise, it won't.
|
||||
*/
|
||||
private async showSaveSketchDialog(): Promise<boolean> {
|
||||
const sketch = await this.isCurrentSketchTemp();
|
||||
if (!sketch) {
|
||||
// Normal close workflow: if there are dirty editors prompt the user.
|
||||
if (!this.shell) {
|
||||
console.error(
|
||||
`Could not get the application shell. Something went wrong.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (this.shell.canSaveAll()) {
|
||||
const prompt = await this.prompt(false);
|
||||
switch (prompt) {
|
||||
case Prompt.DoNotSave:
|
||||
return true;
|
||||
case Prompt.Cancel:
|
||||
return false;
|
||||
case Prompt.Save: {
|
||||
await this.shell.saveAll();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected prompt: ${prompt}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If non of the sketch files were ever touched, do not prompt the save dialog. (#1274)
|
||||
const wereTouched = await Promise.all(
|
||||
Sketch.uris(sketch).map((uri) => this.wasTouched(uri))
|
||||
);
|
||||
if (wereTouched.every((wasTouched) => !Boolean(wasTouched))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prompt = await this.prompt(true);
|
||||
switch (prompt) {
|
||||
case Prompt.DoNotSave:
|
||||
return true;
|
||||
case Prompt.Cancel:
|
||||
return false;
|
||||
case Prompt.Save: {
|
||||
// If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI.
|
||||
const result = await this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: false,
|
||||
wipeOriginal: true,
|
||||
markAsRecentlyOpened: true,
|
||||
}
|
||||
);
|
||||
return !!result;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected prompt: ${prompt}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async prompt(isTemp: boolean): Promise<Prompt> {
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message: nls.localize(
|
||||
'arduino/sketch/saveSketch',
|
||||
'Save your sketch to open it again later.'
|
||||
),
|
||||
title: nls.localize(
|
||||
'theia/core/quitTitle',
|
||||
'Are you sure you want to quit?'
|
||||
),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localizeByDefault("Don't Save"),
|
||||
Dialog.CANCEL,
|
||||
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
|
||||
],
|
||||
defaultId: 2, // `Save`/`Save As...` button index is the default.
|
||||
}
|
||||
);
|
||||
switch (response) {
|
||||
case 0:
|
||||
return Prompt.DoNotSave;
|
||||
case 1:
|
||||
return Prompt.Cancel;
|
||||
case 2:
|
||||
return Prompt.Save;
|
||||
default:
|
||||
throw new Error(`Unexpected response: ${response}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async isCurrentSketchTemp(): Promise<false | Sketch> {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
const isTemp = await this.sketchService.isTemp(currentSketch);
|
||||
if (isTemp) {
|
||||
return currentSketch;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
|
||||
*/
|
||||
@@ -59,13 +202,23 @@ export class Close extends SketchContribution {
|
||||
const { editor } = editorWidget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||
if (Number.isInteger(versionId) && versionId! > 1) {
|
||||
if (this.isInteger(versionId) && versionId > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isInteger(arg: unknown): arg is number {
|
||||
return Number.isInteger(arg);
|
||||
}
|
||||
}
|
||||
|
||||
enum Prompt {
|
||||
Save,
|
||||
DoNotSave,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
export namespace Close {
|
||||
|
@@ -4,11 +4,13 @@ import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
Emitter,
|
||||
MaybeArray,
|
||||
MaybePromise,
|
||||
nls,
|
||||
notEmpty,
|
||||
} from '@theia/core';
|
||||
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
||||
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
@@ -28,17 +30,15 @@ import * as monaco from '@theia/monaco-editor-core';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||
import { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||
import { CoreError } from '../../common/protocol/core-service';
|
||||
import {
|
||||
ArduinoPreferences,
|
||||
ErrorRevealStrategy,
|
||||
} from '../arduino-preferences';
|
||||
import { InoSelector } from '../ino-selectors';
|
||||
import { fullRange } from '../utils/monaco';
|
||||
import { ErrorRevealStrategy } from '../arduino-preferences';
|
||||
import { ArduinoOutputSelector, InoSelector } from '../selectors';
|
||||
import { Contribution } from './contribution';
|
||||
import { CoreErrorHandler } from './core-error-handler';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
|
||||
interface ErrorDecoration {
|
||||
interface ErrorDecorationRef {
|
||||
/**
|
||||
* This is the unique ID of the decoration given by `monaco`.
|
||||
*/
|
||||
@@ -48,72 +48,89 @@ interface ErrorDecoration {
|
||||
*/
|
||||
readonly uri: string;
|
||||
}
|
||||
namespace ErrorDecoration {
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
|
||||
): Promise<monaco.Range | undefined>;
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider: MonacoEditor
|
||||
): monaco.Range | undefined;
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider:
|
||||
| ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||
| MonacoEditor
|
||||
): MaybePromise<monaco.Range | undefined> {
|
||||
if (editorProvider instanceof MonacoEditor) {
|
||||
const control = editorProvider.getControl();
|
||||
const model = control.getModel();
|
||||
if (model) {
|
||||
return control
|
||||
.getDecorationsInRange(fullRange(model))
|
||||
?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||
}
|
||||
return undefined;
|
||||
export namespace ErrorDecorationRef {
|
||||
export function is(arg: unknown): arg is ErrorDecorationRef {
|
||||
if (typeof arg === 'object') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = arg as any;
|
||||
return (
|
||||
'uri' in object &&
|
||||
typeof object['uri'] === 'string' &&
|
||||
'id' in object &&
|
||||
typeof object['id'] === 'string'
|
||||
);
|
||||
}
|
||||
return editorProvider(uri).then((editor) => {
|
||||
if (editor) {
|
||||
return rangeOf({ id, uri }, editor);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// export async function rangeOf(
|
||||
// { id, uri }: ErrorDecoration,
|
||||
// editorProvider:
|
||||
// | ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||
// | MonacoEditor
|
||||
// ): Promise<monaco.Range | undefined> {
|
||||
// const editor =
|
||||
// editorProvider instanceof MonacoEditor
|
||||
// ? editorProvider
|
||||
// : await editorProvider(uri);
|
||||
// if (editor) {
|
||||
// const control = editor.getControl();
|
||||
// const model = control.getModel();
|
||||
// if (model) {
|
||||
// return control
|
||||
// .getDecorationsInRange(fullRange(model))
|
||||
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||
// }
|
||||
// }
|
||||
// return undefined;
|
||||
// }
|
||||
export function sameAs(
|
||||
left: ErrorDecoration,
|
||||
right: ErrorDecoration
|
||||
left: ErrorDecorationRef,
|
||||
right: ErrorDecorationRef
|
||||
): boolean {
|
||||
return left.id === right.id && left.uri === right.uri;
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorDecoration extends ErrorDecorationRef {
|
||||
/**
|
||||
* The range of the error location the error in the compiler output from the CLI.
|
||||
*/
|
||||
readonly rangesInOutput: monaco.Range[];
|
||||
}
|
||||
namespace ErrorDecoration {
|
||||
export function rangeOf(
|
||||
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||
decorations: ErrorDecoration
|
||||
): monaco.Range | undefined;
|
||||
export function rangeOf(
|
||||
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||
decorations: ErrorDecoration[]
|
||||
): (monaco.Range | undefined)[];
|
||||
export function rangeOf(
|
||||
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||
decorations: ErrorDecoration | ErrorDecoration[]
|
||||
): MaybePromise<MaybeArray<monaco.Range | undefined>> {
|
||||
if (editorOrModel) {
|
||||
const allDecorations = getAllDecorations(editorOrModel);
|
||||
if (allDecorations) {
|
||||
if (Array.isArray(decorations)) {
|
||||
return decorations.map(({ id: decorationId }) =>
|
||||
findRangeOf(decorationId, allDecorations)
|
||||
);
|
||||
} else {
|
||||
return findRangeOf(decorations.id, allDecorations);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.isArray(decorations)
|
||||
? decorations.map(() => undefined)
|
||||
: undefined;
|
||||
}
|
||||
function findRangeOf(
|
||||
decorationId: string,
|
||||
allDecorations: { id: string; range?: monaco.Range }[]
|
||||
): monaco.Range | undefined {
|
||||
return allDecorations.find(
|
||||
({ id: candidateId }) => candidateId === decorationId
|
||||
)?.range;
|
||||
}
|
||||
function getAllDecorations(
|
||||
editorOrModel: MonacoEditor | ITextModel
|
||||
): { id: string; range?: monaco.Range }[] {
|
||||
if (editorOrModel instanceof MonacoEditor) {
|
||||
const model = editorOrModel.getControl().getModel();
|
||||
if (!model) {
|
||||
return [];
|
||||
}
|
||||
return model.getAllDecorations();
|
||||
}
|
||||
return editorOrModel.getAllDecorations();
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CompilerErrors
|
||||
extends Contribution
|
||||
implements monaco.languages.CodeLensProvider
|
||||
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
|
||||
{
|
||||
@inject(EditorManager)
|
||||
private readonly editorManager: EditorManager;
|
||||
@@ -122,13 +139,13 @@ export class CompilerErrors
|
||||
private readonly p2m: ProtocolToMonacoConverter;
|
||||
|
||||
@inject(MonacoToProtocolConverter)
|
||||
private readonly mp2: MonacoToProtocolConverter;
|
||||
private readonly m2p: MonacoToProtocolConverter;
|
||||
|
||||
@inject(CoreErrorHandler)
|
||||
private readonly coreErrorHandler: CoreErrorHandler;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly preferences: ArduinoPreferences;
|
||||
private revealStrategy = ErrorRevealStrategy.Default;
|
||||
private experimental = false;
|
||||
|
||||
private readonly errors: ErrorDecoration[] = [];
|
||||
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
||||
@@ -137,8 +154,8 @@ export class CompilerErrors
|
||||
this.currentErrorDidChangEmitter.event;
|
||||
private readonly toDisposeOnCompilerErrorDidChange =
|
||||
new DisposableCollection();
|
||||
|
||||
private shell: ApplicationShell | undefined;
|
||||
private revealStrategy = ErrorRevealStrategy.Default;
|
||||
private currentError: ErrorDecoration | undefined;
|
||||
private get currentErrorIndex(): number {
|
||||
const current = this.currentError;
|
||||
@@ -146,46 +163,75 @@ export class CompilerErrors
|
||||
return -1;
|
||||
}
|
||||
return this.errors.findIndex((error) =>
|
||||
ErrorDecoration.sameAs(error, current)
|
||||
ErrorDecorationRef.sameAs(error, current)
|
||||
);
|
||||
}
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
||||
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
|
||||
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
||||
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
|
||||
this.handleCompilerErrorsDidChange(errors)
|
||||
);
|
||||
this.onCurrentErrorDidChange(async (error) => {
|
||||
const range = await ErrorDecoration.rangeOf(error, (uri) =>
|
||||
this.monacoEditor(uri)
|
||||
);
|
||||
if (!range) {
|
||||
const monacoEditor = await this.monacoEditor(error.uri);
|
||||
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||
if (!monacoRange) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Could not find range of decoration: ${error.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const range = this.m2p.asRange(monacoRange);
|
||||
const editor = await this.revealLocationInEditor({
|
||||
uri: error.uri,
|
||||
range: this.mp2.asRange(range),
|
||||
range,
|
||||
});
|
||||
if (!editor) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Failed to mark error ${error.id} as the current one.`
|
||||
);
|
||||
} else {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (monacoEditor) {
|
||||
monacoEditor.cursor = range.start;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override onReady(): MaybePromise<void> {
|
||||
this.preferences.ready.then(() => {
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.compile.revealRange') {
|
||||
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||
? newValue
|
||||
: ErrorRevealStrategy.Default;
|
||||
this.experimental = Boolean(
|
||||
this.preferences['arduino.compile.experimental']
|
||||
);
|
||||
const strategy = this.preferences['arduino.compile.revealRange'];
|
||||
this.revealStrategy = ErrorRevealStrategy.is(strategy)
|
||||
? strategy
|
||||
: ErrorRevealStrategy.Default;
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, newValue, oldValue }) => {
|
||||
if (newValue === oldValue) {
|
||||
return;
|
||||
}
|
||||
switch (preferenceName) {
|
||||
case 'arduino.compile.revealRange': {
|
||||
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||
? newValue
|
||||
: ErrorRevealStrategy.Default;
|
||||
return;
|
||||
}
|
||||
case 'arduino.compile.experimental': {
|
||||
this.experimental = Boolean(newValue);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,9 +248,13 @@ export class CompilerErrors
|
||||
}
|
||||
const nextError =
|
||||
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
||||
this.markAsCurrentError(nextError);
|
||||
return this.markAsCurrentError(nextError, {
|
||||
forceReselect: true,
|
||||
reveal: true,
|
||||
});
|
||||
},
|
||||
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||
isEnabled: () =>
|
||||
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||
});
|
||||
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
||||
execute: () => {
|
||||
@@ -218,9 +268,24 @@ export class CompilerErrors
|
||||
}
|
||||
const previousError =
|
||||
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
||||
this.markAsCurrentError(previousError);
|
||||
return this.markAsCurrentError(previousError, {
|
||||
forceReselect: true,
|
||||
reveal: true,
|
||||
});
|
||||
},
|
||||
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||
isEnabled: () =>
|
||||
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||
});
|
||||
registry.registerCommand(CompilerErrors.Commands.MARK_AS_CURRENT, {
|
||||
execute: (arg: unknown) => {
|
||||
if (ErrorDecorationRef.is(arg)) {
|
||||
return this.markAsCurrentError(
|
||||
{ id: arg.id, uri: new URI(arg.uri).toString() }, // Make sure the URI fragments are encoded. On Windows, `C:` is encoded as `C%3A`.
|
||||
{ forceReselect: true, reveal: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
isEnabled: () => !!this.errors.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,13 +300,13 @@ export class CompilerErrors
|
||||
): Promise<monaco.languages.CodeLensList> {
|
||||
const lenses: monaco.languages.CodeLens[] = [];
|
||||
if (
|
||||
this.experimental &&
|
||||
this.currentError &&
|
||||
this.currentError.uri === model.uri.toString() &&
|
||||
this.errors.length > 1
|
||||
) {
|
||||
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
|
||||
this.monacoEditor(uri)
|
||||
);
|
||||
const monacoEditor = await this.monacoEditor(model.uri);
|
||||
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
|
||||
if (range) {
|
||||
lenses.push(
|
||||
{
|
||||
@@ -274,14 +339,81 @@ export class CompilerErrors
|
||||
};
|
||||
}
|
||||
|
||||
async provideLinks(
|
||||
model: monaco.editor.ITextModel,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.ILinksList> {
|
||||
const links: monaco.languages.ILink[] = [];
|
||||
if (
|
||||
model.uri.scheme === OutputUri.SCHEME &&
|
||||
model.uri.path === '/Arduino'
|
||||
) {
|
||||
links.push(
|
||||
...this.errors
|
||||
.filter((decoration) => !!decoration.rangesInOutput.length)
|
||||
.map(({ rangesInOutput, id, uri }) =>
|
||||
rangesInOutput.map(
|
||||
(range) =>
|
||||
<monaco.languages.ILink>{
|
||||
range,
|
||||
url: monaco.Uri.parse(`command://`).with({
|
||||
query: JSON.stringify({ id, uri }),
|
||||
path: CompilerErrors.Commands.MARK_AS_CURRENT.id,
|
||||
}),
|
||||
tooltip: nls.localize(
|
||||
'arduino/editor/revealError',
|
||||
'Reveal Error'
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
);
|
||||
} else {
|
||||
console.warn('unexpected URI: ' + model.uri.toString());
|
||||
}
|
||||
return { links };
|
||||
}
|
||||
|
||||
async resolveLink(
|
||||
link: monaco.languages.ILink,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.ILink | undefined> {
|
||||
if (!this.experimental) {
|
||||
return undefined;
|
||||
}
|
||||
const { url } = link;
|
||||
if (url) {
|
||||
const candidateUri = new URI(
|
||||
typeof url === 'string' ? url : url.toString()
|
||||
);
|
||||
const candidateId = candidateUri.path.toString();
|
||||
const error = this.errors.find((error) => error.id === candidateId);
|
||||
if (error) {
|
||||
const monacoEditor = await this.monacoEditor(error.uri);
|
||||
const range = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||
if (range) {
|
||||
return {
|
||||
range,
|
||||
url: monaco.Uri.parse(error.uri),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async handleCompilerErrorsDidChange(
|
||||
errors: CoreError.Compiler[]
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<void> {
|
||||
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||
const compilerErrorsPerResource = this.groupByResource(
|
||||
await this.filter(errors)
|
||||
const groupedErrors = this.groupBy(
|
||||
errors,
|
||||
(error: CoreError.ErrorLocation) => error.location.uri
|
||||
);
|
||||
const decorations = await this.decorateEditors(compilerErrorsPerResource);
|
||||
const decorations = await this.decorateEditors(groupedErrors);
|
||||
this.errors.push(...decorations.errors);
|
||||
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
||||
Disposable.create(() => (this.errors.length = 0)),
|
||||
@@ -289,17 +421,17 @@ export class CompilerErrors
|
||||
...(await Promise.all([
|
||||
decorations.dispose,
|
||||
this.trackEditors(
|
||||
compilerErrorsPerResource,
|
||||
groupedErrors,
|
||||
(editor) =>
|
||||
editor.editor.onSelectionChanged((selection) =>
|
||||
editor.onSelectionChanged((selection) =>
|
||||
this.handleSelectionChange(editor, selection)
|
||||
),
|
||||
(editor) =>
|
||||
editor.onDidDispose(() =>
|
||||
this.handleEditorDidDispose(editor.editor.uri.toString())
|
||||
editor.onDispose(() =>
|
||||
this.handleEditorDidDispose(editor.uri.toString())
|
||||
),
|
||||
(editor) =>
|
||||
editor.editor.onDocumentContentChanged((event) =>
|
||||
editor.onDocumentContentChanged((event) =>
|
||||
this.handleDocumentContentChange(editor, event)
|
||||
)
|
||||
),
|
||||
@@ -307,26 +439,15 @@ export class CompilerErrors
|
||||
]);
|
||||
const currentError = this.errors[0];
|
||||
if (currentError) {
|
||||
await this.markAsCurrentError(currentError);
|
||||
await this.markAsCurrentError(currentError, {
|
||||
forceReselect: true,
|
||||
reveal: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async filter(
|
||||
errors: CoreError.Compiler[]
|
||||
): Promise<CoreError.Compiler[]> {
|
||||
if (!errors.length) {
|
||||
return [];
|
||||
}
|
||||
await this.preferences.ready;
|
||||
if (this.preferences['arduino.compile.experimental']) {
|
||||
return errors;
|
||||
}
|
||||
// Always shows maximum one error; hence the code lens navigation is unavailable.
|
||||
return [errors[0]];
|
||||
}
|
||||
|
||||
private async decorateEditors(
|
||||
errors: Map<string, CoreError.Compiler[]>
|
||||
errors: Map<string, CoreError.ErrorLocation[]>
|
||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||
const composite = await Promise.all(
|
||||
[...errors.entries()].map(([uri, errors]) =>
|
||||
@@ -346,13 +467,13 @@ export class CompilerErrors
|
||||
|
||||
private async decorateEditor(
|
||||
uri: string,
|
||||
errors: CoreError.Compiler[]
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||
const editor = await this.monacoEditor(uri);
|
||||
if (!editor) {
|
||||
return { dispose: Disposable.NULL, errors: [] };
|
||||
}
|
||||
const oldDecorations = editor.editor.deltaDecorations({
|
||||
const oldDecorations = editor.deltaDecorations({
|
||||
oldDecorations: [],
|
||||
newDecorations: errors.map((error) =>
|
||||
this.compilerErrorDecoration(error.location.range)
|
||||
@@ -361,13 +482,19 @@ export class CompilerErrors
|
||||
return {
|
||||
dispose: Disposable.create(() => {
|
||||
if (editor) {
|
||||
editor.editor.deltaDecorations({
|
||||
editor.deltaDecorations({
|
||||
oldDecorations,
|
||||
newDecorations: [],
|
||||
});
|
||||
}
|
||||
}),
|
||||
errors: oldDecorations.map((id) => ({ id, uri })),
|
||||
errors: oldDecorations.map((id, index) => ({
|
||||
id,
|
||||
uri,
|
||||
rangesInOutput: errors[index].rangesInOutput.map((range) =>
|
||||
this.p2m.asRange(range)
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -377,7 +504,7 @@ export class CompilerErrors
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'compiler-error',
|
||||
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -385,11 +512,10 @@ export class CompilerErrors
|
||||
/**
|
||||
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
|
||||
*/
|
||||
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (!monacoEditor) {
|
||||
return;
|
||||
}
|
||||
private handleSelectionChange(
|
||||
monacoEditor: MonacoEditor,
|
||||
selection: Range
|
||||
): void {
|
||||
const uri = monacoEditor.uri.toString();
|
||||
const monacoSelection = this.p2m.asRange(selection);
|
||||
console.log(
|
||||
@@ -424,12 +550,13 @@ export class CompilerErrors
|
||||
console.trace('No match');
|
||||
return undefined;
|
||||
};
|
||||
const error = this.errors
|
||||
.filter((error) => error.uri === uri)
|
||||
.map((error) => ({
|
||||
error,
|
||||
range: ErrorDecoration.rangeOf(error, monacoEditor),
|
||||
}))
|
||||
const errorsPerResource = this.errors.filter((error) => error.uri === uri);
|
||||
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||
monacoEditor,
|
||||
errorsPerResource
|
||||
);
|
||||
const error = rangesPerResource
|
||||
.map((range, index) => ({ error: errorsPerResource[index], range }))
|
||||
.map(({ error, range }) => {
|
||||
if (range) {
|
||||
const priority = calculatePriority(range, monacoSelection);
|
||||
@@ -470,66 +597,77 @@ export class CompilerErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* If a document change "destroys" the range of the decoration, the decoration must be removed.
|
||||
* If the text document changes in the line where compiler errors are, the compiler errors will be removed.
|
||||
*/
|
||||
private handleDocumentContentChange(
|
||||
editor: EditorWidget,
|
||||
monacoEditor: MonacoEditor,
|
||||
event: TextDocumentChangeEvent
|
||||
): void {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (!monacoEditor) {
|
||||
return;
|
||||
}
|
||||
// A decoration location can be "destroyed", hence should be deleted when:
|
||||
// - deleting range (start != end AND text is empty)
|
||||
// - inserting text into range (start != end AND text is not empty)
|
||||
// Filter unrelated delta changes to spare the CPU.
|
||||
const relevantChanges = event.contentChanges.filter(
|
||||
({ range: { start, end } }) =>
|
||||
start.line !== end.line || start.character !== end.character
|
||||
const errorsPerResource = this.errors.filter(
|
||||
(error) => error.uri === event.document.uri
|
||||
);
|
||||
if (!relevantChanges.length) {
|
||||
return;
|
||||
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
|
||||
const doc = event.document;
|
||||
if (doc instanceof MonacoEditorModel) {
|
||||
editorOrModel = doc.textEditorModel;
|
||||
}
|
||||
|
||||
const resolvedMarkers = this.errors
|
||||
.filter((error) => error.uri === event.document.uri)
|
||||
.map((error, index) => {
|
||||
const range = ErrorDecoration.rangeOf(error, monacoEditor);
|
||||
if (range) {
|
||||
return { error, range, index };
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(notEmpty);
|
||||
|
||||
const decorationIdsToRemove = relevantChanges
|
||||
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||
editorOrModel,
|
||||
errorsPerResource
|
||||
);
|
||||
const resolvedDecorations = rangesPerResource.map((range, index) => ({
|
||||
error: errorsPerResource[index],
|
||||
range,
|
||||
}));
|
||||
const decoratorsToRemove = event.contentChanges
|
||||
.map(({ range }) => this.p2m.asRange(range))
|
||||
.map((changeRange) =>
|
||||
resolvedMarkers.filter(({ range: decorationRange }) =>
|
||||
changeRange.containsRange(decorationRange)
|
||||
)
|
||||
.map((changedRange) =>
|
||||
resolvedDecorations
|
||||
.filter(({ range: decorationRange }) => {
|
||||
if (!decorationRange) {
|
||||
return false;
|
||||
}
|
||||
const affects =
|
||||
changedRange.startLineNumber <= decorationRange.startLineNumber &&
|
||||
changedRange.endLineNumber >= decorationRange.endLineNumber;
|
||||
console.log(
|
||||
'compiler-errors',
|
||||
`decoration range: ${decorationRange.toString()}, change range: ${changedRange.toString()}, affects: ${affects}`
|
||||
);
|
||||
return affects;
|
||||
})
|
||||
.map(({ error }) => {
|
||||
const index = this.errors.findIndex((candidate) =>
|
||||
ErrorDecorationRef.sameAs(candidate, error)
|
||||
);
|
||||
return index !== -1 ? { error, index } : undefined;
|
||||
})
|
||||
.filter(notEmpty)
|
||||
)
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
.map(({ error, index }) => {
|
||||
this.errors.splice(index, 1);
|
||||
return error.id;
|
||||
});
|
||||
if (!decorationIdsToRemove.length) {
|
||||
return;
|
||||
.sort((left, right) => left.index - right.index); // highest index last
|
||||
|
||||
if (decoratorsToRemove.length) {
|
||||
let i = decoratorsToRemove.length;
|
||||
while (i--) {
|
||||
this.errors.splice(decoratorsToRemove[i].index, 1);
|
||||
}
|
||||
monacoEditor.getControl().deltaDecorations(
|
||||
decoratorsToRemove.map(({ error }) => error.id),
|
||||
[]
|
||||
);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
|
||||
private async trackEditors(
|
||||
errors: Map<string, CoreError.Compiler[]>,
|
||||
...track: ((editor: EditorWidget) => Disposable)[]
|
||||
errors: Map<string, CoreError.ErrorLocation[]>,
|
||||
...track: ((editor: MonacoEditor) => Disposable)[]
|
||||
): Promise<Disposable> {
|
||||
return new DisposableCollection(
|
||||
...(await Promise.all(
|
||||
Array.from(errors.keys()).map(async (uri) => {
|
||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||
const editor = await this.monacoEditor(uri);
|
||||
if (!editor) {
|
||||
return Disposable.NULL;
|
||||
}
|
||||
@@ -539,15 +677,18 @@ export class CompilerErrors
|
||||
);
|
||||
}
|
||||
|
||||
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
|
||||
private async markAsCurrentError(
|
||||
ref: ErrorDecorationRef,
|
||||
options?: { forceReselect?: boolean; reveal?: boolean }
|
||||
): Promise<void> {
|
||||
const index = this.errors.findIndex((candidate) =>
|
||||
ErrorDecoration.sameAs(candidate, error)
|
||||
ErrorDecorationRef.sameAs(candidate, ref)
|
||||
);
|
||||
if (index < 0) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Failed to mark error ${
|
||||
error.id
|
||||
ref.id
|
||||
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||
({ id }) => id
|
||||
)}`
|
||||
@@ -556,15 +697,18 @@ export class CompilerErrors
|
||||
}
|
||||
const newError = this.errors[index];
|
||||
if (
|
||||
options?.forceReselect ||
|
||||
!this.currentError ||
|
||||
!ErrorDecoration.sameAs(this.currentError, newError)
|
||||
!ErrorDecorationRef.sameAs(this.currentError, newError)
|
||||
) {
|
||||
this.currentError = this.errors[index];
|
||||
console.log(
|
||||
'compiler-errors',
|
||||
`Current error changed to ${this.currentError.id}`
|
||||
);
|
||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||
if (options?.reveal) {
|
||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||
}
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
}
|
||||
@@ -599,32 +743,33 @@ export class CompilerErrors
|
||||
}
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`could not found editor widget for URI: ${uri}`
|
||||
`could not find editor widget for URI: ${uri}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private groupByResource(
|
||||
errors: CoreError.Compiler[]
|
||||
): Map<string, CoreError.Compiler[]> {
|
||||
return errors.reduce((acc, curr) => {
|
||||
const {
|
||||
location: { uri },
|
||||
} = curr;
|
||||
let errors = acc.get(uri);
|
||||
if (!errors) {
|
||||
errors = [];
|
||||
acc.set(uri, errors);
|
||||
private groupBy<K, V>(
|
||||
elements: V[],
|
||||
extractKey: (element: V) => K
|
||||
): Map<K, V[]> {
|
||||
return elements.reduce((acc, curr) => {
|
||||
const key = extractKey(curr);
|
||||
let values = acc.get(key);
|
||||
if (!values) {
|
||||
values = [];
|
||||
acc.set(key, values);
|
||||
}
|
||||
errors.push(curr);
|
||||
values.push(curr);
|
||||
return acc;
|
||||
}, new Map<string, CoreError.Compiler[]>());
|
||||
}, new Map<K, V[]>());
|
||||
}
|
||||
|
||||
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
|
||||
private monacoEditor(
|
||||
uriOrWidget: string | EditorWidget
|
||||
uri: string | monaco.Uri
|
||||
): Promise<MonacoEditor | undefined>;
|
||||
private monacoEditor(
|
||||
uriOrWidget: string | monaco.Uri | EditorWidget
|
||||
): MaybePromise<MonacoEditor | undefined> {
|
||||
if (uriOrWidget instanceof EditorWidget) {
|
||||
const editor = uriOrWidget.editor;
|
||||
@@ -652,5 +797,8 @@ export namespace CompilerErrors {
|
||||
export const PREVIOUS_ERROR: Command = {
|
||||
id: 'arduino-editor-previous-error',
|
||||
};
|
||||
export const MARK_AS_CURRENT: Command = {
|
||||
id: 'arduino-editor-mark-as-current-error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,6 @@ import {
|
||||
CommandContribution,
|
||||
CommandService,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
import { SettingsService } from '../dialogs/settings/settings';
|
||||
import {
|
||||
CurrentSketch,
|
||||
@@ -50,13 +49,18 @@ import {
|
||||
Sketch,
|
||||
CoreService,
|
||||
CoreError,
|
||||
ResponseServiceClient,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { CoreErrorHandler } from './core-error-handler';
|
||||
import { nls } from '@theia/core';
|
||||
import { OutputChannelManager } from '../theia/output/output-channel';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
|
||||
export {
|
||||
Command,
|
||||
@@ -90,15 +94,15 @@ export abstract class Contribution
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(SettingsService)
|
||||
protected readonly settingsService: SettingsService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly preferences: ArduinoPreferences;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@@ -146,9 +150,6 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly preferences: ArduinoPreferences;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@@ -171,18 +172,39 @@ export abstract class SketchContribution extends Contribution {
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CoreServiceContribution extends SketchContribution {
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
export abstract class CoreServiceContribution extends SketchContribution {
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(CoreErrorHandler)
|
||||
protected readonly coreErrorHandler: CoreErrorHandler;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(CoreService)
|
||||
private readonly coreService: CoreService;
|
||||
|
||||
@inject(ClipboardService)
|
||||
private readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(NotificationManager)
|
||||
private readonly notificationManager: NotificationManager;
|
||||
|
||||
/**
|
||||
* This is the internal (Theia) ID of the notification that is currently visible.
|
||||
* It's stored here as a field to be able to close it before executing any new core command (such as verify, upload, etc.)
|
||||
*/
|
||||
private visibleNotificationId: string | undefined;
|
||||
|
||||
protected clearVisibleNotification(): void {
|
||||
if (this.visibleNotificationId) {
|
||||
this.notificationManager.clear(this.visibleNotificationId);
|
||||
this.visibleNotificationId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected handleError(error: unknown): void {
|
||||
this.coreErrorHandler.tryHandle(error);
|
||||
this.tryToastErrorMessage(error);
|
||||
}
|
||||
|
||||
@@ -200,10 +222,17 @@ export class CoreServiceContribution extends SketchContribution {
|
||||
} catch {}
|
||||
}
|
||||
if (message) {
|
||||
if (message.includes('Missing FQBN (Fully Qualified Board Name)')) {
|
||||
message = nls.localize(
|
||||
'arduino/coreContribution/noBoardSelected',
|
||||
'No board selected. Please select your Arduino board from the Tools > Board menu.'
|
||||
);
|
||||
}
|
||||
const copyAction = nls.localize(
|
||||
'arduino/coreContribution/copyError',
|
||||
'Copy error messages'
|
||||
);
|
||||
this.visibleNotificationId = this.notificationId(message, copyAction);
|
||||
this.messageService.error(message, copyAction).then(async (action) => {
|
||||
if (action === copyAction) {
|
||||
const content = await this.outputChannelManager.contentOfChannel(
|
||||
@@ -218,6 +247,33 @@ export class CoreServiceContribution extends SketchContribution {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async doWithProgress<T>(options: {
|
||||
progressText: string;
|
||||
keepOutput?: boolean;
|
||||
task: (progressId: string, coreService: CoreService) => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const { progressText, keepOutput, task } = options;
|
||||
this.outputChannelManager
|
||||
.getChannel('Arduino')
|
||||
.show({ preserveFocus: true });
|
||||
const result = await ExecuteWithProgress.doWithProgress({
|
||||
messageService: this.messageService,
|
||||
responseService: this.responseService,
|
||||
progressText,
|
||||
run: ({ progressId }) => task(progressId, this.coreService),
|
||||
keepOutput,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private notificationId(message: string, ...actions: string[]): string {
|
||||
return this.notificationManager.getMessageId({
|
||||
text: message,
|
||||
actions,
|
||||
type: MessageType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Contribution {
|
||||
|
@@ -4,29 +4,29 @@ import { CoreError } from '../../common/protocol/core-service';
|
||||
|
||||
@injectable()
|
||||
export class CoreErrorHandler {
|
||||
private readonly compilerErrors: CoreError.Compiler[] = [];
|
||||
private readonly errors: CoreError.ErrorLocation[] = [];
|
||||
private readonly compilerErrorsDidChangeEmitter = new Emitter<
|
||||
CoreError.Compiler[]
|
||||
CoreError.ErrorLocation[]
|
||||
>();
|
||||
|
||||
tryHandle(error: unknown): void {
|
||||
if (CoreError.is(error)) {
|
||||
this.compilerErrors.length = 0;
|
||||
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
|
||||
this.errors.length = 0;
|
||||
this.errors.push(...error.data);
|
||||
this.fireCompilerErrorsDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.compilerErrors.length = 0;
|
||||
this.errors.length = 0;
|
||||
this.fireCompilerErrorsDidChange();
|
||||
}
|
||||
|
||||
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
|
||||
get onCompilerErrorsDidChange(): Event<CoreError.ErrorLocation[]> {
|
||||
return this.compilerErrorsDidChangeEmitter.event;
|
||||
}
|
||||
|
||||
private fireCompilerErrorsDidChange(): void {
|
||||
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
|
||||
this.compilerErrorsDidChangeEmitter.fire(this.errors.slice());
|
||||
}
|
||||
}
|
||||
|
41
arduino-ide-extension/src/browser/contributions/daemon.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { nls } from '@theia/core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { ArduinoDaemon } from '../../common/protocol';
|
||||
import { Contribution, Command, CommandRegistry } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class Daemon extends Contribution {
|
||||
@inject(ArduinoDaemon)
|
||||
private readonly daemon: ArduinoDaemon;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Daemon.Commands.START_DAEMON, {
|
||||
execute: () => this.daemon.start(),
|
||||
});
|
||||
registry.registerCommand(Daemon.Commands.STOP_DAEMON, {
|
||||
execute: () => this.daemon.stop(),
|
||||
});
|
||||
registry.registerCommand(Daemon.Commands.RESTART_DAEMON, {
|
||||
execute: () => this.daemon.restart(),
|
||||
});
|
||||
}
|
||||
}
|
||||
export namespace Daemon {
|
||||
export namespace Commands {
|
||||
export const START_DAEMON: Command = {
|
||||
id: 'arduino-start-daemon',
|
||||
label: nls.localize('arduino/daemon/start', 'Start Daemon'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const STOP_DAEMON: Command = {
|
||||
id: 'arduino-stop-daemon',
|
||||
label: nls.localize('arduino/daemon/stop', 'Stop Daemon'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const RESTART_DAEMON: Command = {
|
||||
id: 'arduino-restart-daemon',
|
||||
label: nls.localize('arduino/daemon/restart', 'Restart Daemon'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
@@ -12,46 +12,53 @@ import {
|
||||
SketchContribution,
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { MaybePromise, nls } from '@theia/core/lib/common';
|
||||
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
|
||||
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
|
||||
@injectable()
|
||||
export class Debug extends SketchContribution {
|
||||
@inject(HostedPluginSupport)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
private readonly hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected readonly executableService: ExecutableService;
|
||||
private readonly executableService: ExecutableService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
private readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
/**
|
||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||
*/
|
||||
protected _disabledMessages?: string = nls.localize(
|
||||
private _disabledMessages?: string = nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
); // Initial pessimism.
|
||||
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
protected onDisabledMessageDidChange =
|
||||
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
private onDisabledMessageDidChange =
|
||||
this.disabledMessageDidChangeEmitter.event;
|
||||
|
||||
protected get disabledMessage(): string | undefined {
|
||||
private get disabledMessage(): string | undefined {
|
||||
return this._disabledMessages;
|
||||
}
|
||||
protected set disabledMessage(message: string | undefined) {
|
||||
private set disabledMessage(message: string | undefined) {
|
||||
this._disabledMessages = message;
|
||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||
}
|
||||
|
||||
protected readonly debugToolbarItem = {
|
||||
private readonly debugToolbarItem = {
|
||||
id: Debug.Commands.START_DEBUGGING.id,
|
||||
command: Debug.Commands.START_DEBUGGING.id,
|
||||
tooltip: `${
|
||||
@@ -83,8 +90,8 @@ export class Debug extends SketchContribution {
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
this.refreshState(selectedBoard)
|
||||
);
|
||||
this.notificationCenter.onPlatformInstalled(() => this.refreshState());
|
||||
this.notificationCenter.onPlatformUninstalled(() => this.refreshState());
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
|
||||
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
|
||||
}
|
||||
|
||||
override onReady(): MaybePromise<void> {
|
||||
@@ -98,12 +105,27 @@ export class Debug extends SketchContribution {
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: () => !this.disabledMessage,
|
||||
});
|
||||
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
|
||||
execute: () => this.toggleCompileForDebug(),
|
||||
isToggled: () => this.compileForDebug,
|
||||
});
|
||||
registry.registerCommand(Debug.Commands.IS_OPTIMIZE_FOR_DEBUG, {
|
||||
execute: () => this.compileForDebug,
|
||||
});
|
||||
}
|
||||
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem(this.debugToolbarItem);
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.id,
|
||||
label: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.label,
|
||||
order: '5',
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshState(
|
||||
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||
.selectedBoard
|
||||
@@ -145,7 +167,7 @@ export class Debug extends SketchContribution {
|
||||
}
|
||||
}
|
||||
|
||||
protected async startDebug(
|
||||
private async startDebug(
|
||||
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||
.selectedBoard
|
||||
): Promise<void> {
|
||||
@@ -183,8 +205,19 @@ export class Debug extends SketchContribution {
|
||||
};
|
||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||
}
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
const value = window.localStorage.getItem(COMPILE_FOR_DEBUG_KEY);
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
async toggleCompileForDebug(): Promise<void> {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
export namespace Debug {
|
||||
export namespace Commands {
|
||||
export const START_DEBUGGING = Command.toLocalizedCommand(
|
||||
@@ -195,5 +228,16 @@ export namespace Debug {
|
||||
},
|
||||
'vscode/debug.contribution/startDebuggingHelp'
|
||||
);
|
||||
export const TOGGLE_OPTIMIZE_FOR_DEBUG = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-toggle-optimize-for-debug',
|
||||
label: 'Optimize for Debugging',
|
||||
category: 'Arduino',
|
||||
},
|
||||
'arduino/debug/optimizeForDebugging'
|
||||
);
|
||||
export const IS_OPTIMIZE_FOR_DEBUG: Command = {
|
||||
id: 'arduino-is-optimize-for-debug',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class DeleteSketch extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
|
||||
execute: (uri: string) => this.deleteSketch(uri),
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteSketch(uri: string): Promise<void> {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
if (!sketch) {
|
||||
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
|
||||
return;
|
||||
}
|
||||
return this.sketchService.deleteSketch(sketch);
|
||||
}
|
||||
|
||||
private async loadSketch(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
export namespace DeleteSketch {
|
||||
export namespace Commands {
|
||||
export const DELETE_SKETCH: Command = {
|
||||
id: 'arduino-delete-sketch',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
|
||||
import {
|
||||
Contribution,
|
||||
@@ -20,13 +19,10 @@ import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/edit
|
||||
@injectable()
|
||||
export class EditContributions extends Contribution {
|
||||
@inject(MonacoEditorService)
|
||||
protected readonly codeEditorService: MonacoEditorService;
|
||||
private readonly codeEditorService: MonacoEditorService;
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferences: PreferenceService;
|
||||
private readonly clipboardService: ClipboardService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
|
||||
@@ -145,6 +141,11 @@ ${value}
|
||||
label: nls.localize('arduino/editor/decreaseIndent', 'Decrease Indent'),
|
||||
order: '2',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'),
|
||||
order: '3',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
@@ -252,10 +253,13 @@ ${value}
|
||||
});
|
||||
}
|
||||
|
||||
protected async current(): Promise<ICodeEditor | StandaloneCodeEditor | undefined> {
|
||||
protected async current(): Promise<
|
||||
ICodeEditor | StandaloneCodeEditor | undefined
|
||||
> {
|
||||
return (
|
||||
this.codeEditorService.getFocusedCodeEditor() ||
|
||||
this.codeEditorService.getActiveCodeEditor() || undefined
|
||||
this.codeEditorService.getActiveCodeEditor() ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -19,10 +19,11 @@ import {
|
||||
SketchContribution,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@@ -150,10 +151,13 @@ export abstract class Examples extends SketchContribution {
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
return this.commandService
|
||||
.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch)
|
||||
.then((result) => {
|
||||
const name = new URI(uri).path.base;
|
||||
this.sketchService.markAsRecentlyOpened({ name, sourceUri: uri }); // no await
|
||||
return result;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -202,8 +206,8 @@ export class LibraryExamples extends Examples {
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onLibraryInstalled(() => this.register());
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.register());
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.register());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
|
@@ -0,0 +1,102 @@
|
||||
import { LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BoardsService,
|
||||
LibraryLocation,
|
||||
LibraryService,
|
||||
} from '../../common/protocol';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class FirstStartupInstaller extends Contribution {
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorageService: LocalStorageService;
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
const isFirstStartup = !(await this.localStorageService.getData(
|
||||
FirstStartupInstaller.INIT_LIBS_AND_PACKAGES
|
||||
));
|
||||
if (isFirstStartup) {
|
||||
const avrPackage = await this.boardsService.getBoardPackage({
|
||||
id: 'arduino:avr',
|
||||
});
|
||||
const builtInLibrary = (
|
||||
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
|
||||
)[0];
|
||||
|
||||
let avrPackageError: Error | undefined;
|
||||
let builtInLibraryError: Error | undefined;
|
||||
|
||||
if (avrPackage) {
|
||||
try {
|
||||
await this.boardsService.install({
|
||||
item: avrPackage,
|
||||
noOverwrite: true, // We don't want to automatically replace custom platforms the user might already have in place
|
||||
});
|
||||
} catch (e) {
|
||||
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/ffe4232b359fcfa87238d68acf1c3b64a1621f14#diff-10ffbdde46838dd9caa881fd1f2a5326a49f8061f6cfd7c9d430b4875a6b6895R62
|
||||
if (
|
||||
e.message.includes(
|
||||
`Platform ${avrPackage.id}@${avrPackage.installedVersion} already installed`
|
||||
)
|
||||
) {
|
||||
// If arduino:avr installation fails because it's already installed we don't want to retry on next start-up
|
||||
console.error(e);
|
||||
} else {
|
||||
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
|
||||
avrPackageError = e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
avrPackageError = new Error('Could not find platform.');
|
||||
}
|
||||
|
||||
if (builtInLibrary) {
|
||||
try {
|
||||
await this.libraryService.install({
|
||||
item: builtInLibrary,
|
||||
installDependencies: true,
|
||||
noOverwrite: true, // We don't want to automatically replace custom libraries the user might already have in place
|
||||
installLocation: LibraryLocation.BUILTIN,
|
||||
});
|
||||
} catch (e) {
|
||||
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/2ea3608453b17b1157f8a1dc892af2e13e40f4f0#diff-1de7569144d4e260f8dde0e0d00a4e2a218c57966d583da1687a70d518986649R95
|
||||
if (/Library (.*) is already installed/.test(e.message)) {
|
||||
// If Arduino_BuiltIn installation fails because it's already installed we don't want to retry on next start-up
|
||||
console.log('error installing core', e);
|
||||
} else {
|
||||
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
|
||||
builtInLibraryError = e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builtInLibraryError = new Error('Could not find library');
|
||||
}
|
||||
|
||||
if (avrPackageError) {
|
||||
this.messageService.error(
|
||||
`Could not install Arduino AVR platform: ${avrPackageError}`
|
||||
);
|
||||
}
|
||||
if (builtInLibraryError) {
|
||||
this.messageService.error(
|
||||
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!avrPackageError && !builtInLibraryError) {
|
||||
await this.localStorageService.setData(
|
||||
FirstStartupInstaller.INIT_LIBS_AND_PACKAGES,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export namespace FirstStartupInstaller {
|
||||
export const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
|
||||
}
|
@@ -2,8 +2,7 @@ import { MaybePromise } from '@theia/core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { Formatter } from '../../common/protocol/formatter';
|
||||
import { InoSelector } from '../ino-selectors';
|
||||
import { fullRange } from '../utils/monaco';
|
||||
import { InoSelector } from '../selectors';
|
||||
import { Contribution, URI } from './contribution';
|
||||
|
||||
@injectable()
|
||||
@@ -40,7 +39,7 @@ export class Format
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.TextEdit[]> {
|
||||
const range = fullRange(model);
|
||||
const range = model.getFullModelRange();
|
||||
const text = await this.format(model, range, options);
|
||||
return [{ range, text }];
|
||||
}
|
||||
|
@@ -41,7 +41,9 @@ export class Help extends Contribution {
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.ENVIRONMENT,
|
||||
createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
|
||||
createOpenHandler(
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2'
|
||||
)
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.TROUBLESHOOTING,
|
||||
|
@@ -49,8 +49,8 @@ export class IncludeLibrary extends SketchContribution {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryUninstalled(() =>
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,71 @@
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { ProgressService } from '@theia/core/lib/common/progress-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { ProgressMessage } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class IndexesUpdateProgress extends Contribution {
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(ProgressService)
|
||||
private readonly progressService: ProgressService;
|
||||
private currentProgress:
|
||||
| (Progress & Readonly<{ progressId: string }>)
|
||||
| undefined;
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onIndexWillUpdate((progressId) =>
|
||||
this.getOrCreateProgress(progressId)
|
||||
);
|
||||
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
|
||||
this.getOrCreateProgress(progress).then((delegate) =>
|
||||
delegate.report(progress)
|
||||
);
|
||||
});
|
||||
this.notificationCenter.onIndexDidUpdate((progressId) => {
|
||||
this.cancelProgress(progressId);
|
||||
});
|
||||
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {
|
||||
this.cancelProgress(progressId);
|
||||
this.messageService.error(message);
|
||||
});
|
||||
}
|
||||
|
||||
private async getOrCreateProgress(
|
||||
progressOrId: ProgressMessage | string
|
||||
): Promise<Progress & { progressId: string }> {
|
||||
const progressId = ProgressMessage.is(progressOrId)
|
||||
? progressOrId.progressId
|
||||
: progressOrId;
|
||||
if (this.currentProgress?.progressId === progressId) {
|
||||
return this.currentProgress;
|
||||
}
|
||||
if (this.currentProgress) {
|
||||
this.currentProgress.cancel();
|
||||
}
|
||||
this.currentProgress = undefined;
|
||||
const progress = await this.progressService.showProgress({
|
||||
text: '',
|
||||
options: { location: 'notification' },
|
||||
});
|
||||
if (ProgressMessage.is(progressOrId)) {
|
||||
progress.report(progressOrId);
|
||||
}
|
||||
this.currentProgress = { ...progress, progressId };
|
||||
return this.currentProgress;
|
||||
}
|
||||
|
||||
private cancelProgress(progressId: string) {
|
||||
if (this.currentProgress) {
|
||||
if (this.currentProgress.progressId !== progressId) {
|
||||
console.warn(
|
||||
`Mismatching progress IDs. Expected ${progressId}, got ${this.currentProgress.progressId}. Canceling anyway.`
|
||||
);
|
||||
}
|
||||
this.currentProgress.cancel();
|
||||
this.currentProgress = undefined;
|
||||
}
|
||||
}
|
||||
}
|
159
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
ArduinoDaemon,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
} from '../../common/protocol';
|
||||
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class InoLanguage extends SketchContribution {
|
||||
@inject(HostedPluginEvents)
|
||||
private readonly hostedPluginEvents: HostedPluginEvents;
|
||||
|
||||
@inject(ExecutableService)
|
||||
private readonly executableService: ExecutableService;
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
private readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
private languageServerFqbn?: string;
|
||||
private languageServerStartMutex = new Mutex();
|
||||
|
||||
override onReady(): void {
|
||||
const start = (
|
||||
{ selectedBoard }: BoardsConfig.Config,
|
||||
forceStart = false
|
||||
) => {
|
||||
if (selectedBoard) {
|
||||
const { name, fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
this.startLanguageServer(fqbn, name, forceStart);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(start);
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig)
|
||||
);
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
);
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, oldValue, newValue }) => {
|
||||
if (oldValue !== newValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
case 'arduino.language.realTimeDiagnostics':
|
||||
start(this.boardsServiceProvider.boardsConfig, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
start(this.boardsServiceProvider.boardsConfig);
|
||||
}
|
||||
|
||||
private async startLanguageServer(
|
||||
fqbn: string,
|
||||
name: string | undefined,
|
||||
forceStart = false
|
||||
): Promise<void> {
|
||||
const port = await this.daemon.tryGetPort();
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
const release = await this.languageServerStartMutex.acquire();
|
||||
try {
|
||||
await this.hostedPluginEvents.didStart;
|
||||
const details = await this.boardsService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
// Core is not installed for the selected board.
|
||||
console.info(
|
||||
`Could not start language server for ${fqbn}. The core is not installed for the board.`
|
||||
);
|
||||
if (this.languageServerFqbn) {
|
||||
try {
|
||||
await this.commandService.executeCommand(
|
||||
'arduino.languageserver.stop'
|
||||
);
|
||||
console.info(
|
||||
`Stopped language server process for ${this.languageServerFqbn}.`
|
||||
);
|
||||
this.languageServerFqbn = undefined;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to start language server process for ${this.languageServerFqbn}`,
|
||||
e
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!forceStart && fqbn === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbn}`);
|
||||
const log = this.preferences.get('arduino.language.log');
|
||||
const realTimeDiagnostics = this.preferences.get(
|
||||
'arduino.language.realTimeDiagnostics'
|
||||
);
|
||||
let currentSketchPath: string | undefined = undefined;
|
||||
if (log) {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
currentSketchPath = await this.fileService.fsPath(
|
||||
new URI(currentSketch.uri)
|
||||
);
|
||||
}
|
||||
}
|
||||
const { clangdUri, lsUri } = await this.executableService.list();
|
||||
const [clangdPath, lsPath] = await Promise.all([
|
||||
this.fileService.fsPath(new URI(clangdUri)),
|
||||
this.fileService.fsPath(new URI(lsUri)),
|
||||
]);
|
||||
|
||||
this.languageServerFqbn = await Promise.race([
|
||||
new Promise<undefined>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`Timeout after ${20_000} ms.`)),
|
||||
20_000
|
||||
)
|
||||
),
|
||||
this.commandService.executeCommand<string>(
|
||||
'arduino.languageserver.start',
|
||||
{
|
||||
lsPath,
|
||||
cliDaemonAddr: `localhost:${port}`,
|
||||
clangdPath,
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
cliDaemonInstance: '1',
|
||||
board: {
|
||||
fqbn,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
realTimeDiagnostics,
|
||||
silentOutput: true,
|
||||
}
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,7 +9,6 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
@@ -40,15 +39,6 @@ export class NewSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
|
||||
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
|
||||
tooltip: nls.localize('arduino/sketch/new', 'New'),
|
||||
priority: 3,
|
||||
});
|
||||
}
|
||||
|
||||
async newSketch(): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
|
@@ -0,0 +1,32 @@
|
||||
import { CommandRegistry } from '@theia/core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsConfigDialog } from '../boards/boards-config-dialog';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { Contribution, Command } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class OpenBoardsConfig extends Contribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsConfigDialog)
|
||||
private readonly boardsConfigDialog: BoardsConfigDialog;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
|
||||
execute: async (query?: string | undefined) => {
|
||||
const boardsConfig = await this.boardsConfigDialog.open(query);
|
||||
if (boardsConfig) {
|
||||
return (this.boardsServiceProvider.boardsConfig = boardsConfig);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
export namespace OpenBoardsConfig {
|
||||
export namespace Commands {
|
||||
export const OPEN_DIALOG: Command = {
|
||||
id: 'arduino-open-boards-dialog',
|
||||
};
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ExampleRef } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class OpenRecentSketch extends SketchContribution {
|
||||
@@ -36,7 +37,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
this.refreshMenu(sketches)
|
||||
);
|
||||
}
|
||||
@@ -55,26 +56,30 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
);
|
||||
}
|
||||
|
||||
private refreshMenu(sketches: Sketch[]): void {
|
||||
private refreshMenu(sketches: (Sketch | ExampleRef)[]): void {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
protected register(sketches: (Sketch | ExampleRef)[]): void {
|
||||
const order = 0;
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const uri = Sketch.is(sketch) ? sketch.uri : sketch.sourceUri;
|
||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
execute: async () => {
|
||||
const toOpen = Sketch.is(sketch)
|
||||
? sketch
|
||||
: await this.sketchService.cloneExample(sketch.sourceUri);
|
||||
this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
),
|
||||
toOpen
|
||||
);
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(
|
||||
@@ -86,7 +91,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeRegister.set(
|
||||
sketch.uri,
|
||||
uri,
|
||||
new DisposableCollection(
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
|
@@ -0,0 +1,108 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { Later } from '../../common/nls';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchFiles extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
|
||||
execute: (uri: URI) => this.openSketchFiles(uri),
|
||||
});
|
||||
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
|
||||
execute: (
|
||||
uri: string,
|
||||
forceOpen?: boolean,
|
||||
options?: EditorOpenerOptions
|
||||
) => {
|
||||
this.ensureOpened(uri, forceOpen, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async openSketchFiles(uri: URI): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(uri.toString());
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
||||
await this.ensureOpened(uri);
|
||||
}
|
||||
if (mainFileUri.endsWith('.pde')) {
|
||||
const message = nls.localize(
|
||||
'arduino/common/oldFormat',
|
||||
"The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?",
|
||||
sketch.name
|
||||
);
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
this.messageService.info(message, Later, yes).then((answer) => {
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.openFallbackSketch();
|
||||
} else {
|
||||
console.error(err);
|
||||
const message =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === 'string'
|
||||
? err
|
||||
: String(err);
|
||||
this.messageService.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async openFallbackSketch(): Promise<void> {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||
}
|
||||
|
||||
private async ensureOpened(
|
||||
uri: string,
|
||||
forceOpen = false,
|
||||
options?: EditorOpenerOptions
|
||||
): Promise<unknown> {
|
||||
const widget = this.editorManager.all.find(
|
||||
(widget) => widget.editor.uri.toString() === uri
|
||||
);
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(
|
||||
new URI(uri),
|
||||
options ?? {
|
||||
mode: 'reveal',
|
||||
preview: false,
|
||||
counter: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
export namespace OpenSketchFiles {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH_FILES: Command = {
|
||||
id: 'arduino-open-sketch-files',
|
||||
};
|
||||
export const ENSURE_OPENED: Command = {
|
||||
id: 'arduino-ensure-opened',
|
||||
};
|
||||
}
|
||||
}
|
@@ -16,7 +16,6 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
@@ -131,15 +130,6 @@ export class OpenSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
||||
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
||||
tooltip: nls.localize('vscode/dialogMainService/open', 'Open'),
|
||||
priority: 4,
|
||||
});
|
||||
}
|
||||
|
||||
async openSketch(
|
||||
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
||||
): Promise<void> {
|
||||
|
@@ -12,21 +12,19 @@ import {
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly applicationShell: ApplicationShell;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||
@@ -57,6 +55,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
execOnlyIfTemp,
|
||||
openAfterMove,
|
||||
wipeOriginal,
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
@@ -77,15 +76,11 @@ export class SaveAsSketch extends SketchContribution {
|
||||
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 defaultUri = sketchDirUri.resolve(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
@@ -106,21 +101,23 @@ export class SaveAsSketch extends SketchContribution {
|
||||
});
|
||||
if (workspaceUri) {
|
||||
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||
}
|
||||
if (workspaceUri && openAfterMove) {
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
try {
|
||||
await this.fileService.delete(new URI(sketch.uri), {
|
||||
recursive: true,
|
||||
});
|
||||
} catch {
|
||||
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
|
||||
}
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchService.markAsRecentlyOpened(workspaceUri);
|
||||
}
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (workspaceUri && openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
this.workspaceService.open(new URI(workspaceUri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [sketch.uri],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), options);
|
||||
}
|
||||
return !!workspaceUri;
|
||||
}
|
||||
@@ -174,12 +171,14 @@ export namespace SaveAsSketch {
|
||||
* Ignored if `openAfterMove` is `false`.
|
||||
*/
|
||||
readonly wipeOriginal?: boolean;
|
||||
readonly markAsRecentlyOpened?: boolean;
|
||||
}
|
||||
export namespace Options {
|
||||
export const DEFAULT: Options = {
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false,
|
||||
markAsRecentlyOpened: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
@@ -43,15 +42,6 @@ export class SaveSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
|
||||
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
|
||||
tooltip: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||
priority: 5,
|
||||
});
|
||||
}
|
||||
|
||||
async saveSketch(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
|
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
StatusBar,
|
||||
StatusBarAlignment,
|
||||
} from '@theia/core/lib/browser/status-bar/status-bar';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class SelectedBoard extends Contribution {
|
||||
@inject(StatusBar)
|
||||
private readonly statusBar: StatusBar;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
override onStart(): void {
|
||||
this.boardsServiceProvider.onBoardsConfigChanged((config) =>
|
||||
this.update(config)
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.update(this.boardsServiceProvider.boardsConfig);
|
||||
}
|
||||
|
||||
private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void {
|
||||
this.statusBar.setElement('arduino-selected-board', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedBoard
|
||||
? `$(microchip) ${selectedBoard.name}`
|
||||
: `$(close) ${nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
)}`,
|
||||
className: 'arduino-selected-board',
|
||||
});
|
||||
if (selectedBoard) {
|
||||
this.statusBar.setElement('arduino-selected-port', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedPort
|
||||
? nls.localize(
|
||||
'arduino/common/selectedOn',
|
||||
'on {0}',
|
||||
selectedPort.address
|
||||
)
|
||||
: nls.localize('arduino/common/notConnected', '[not connected]'),
|
||||
className: 'arduino-selected-port',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -49,7 +49,10 @@ export class Settings extends SketchContribution {
|
||||
) + '...',
|
||||
order: '0',
|
||||
});
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__ADVANCED_SUBMENU,
|
||||
nls.localize('arduino/menu/advanced', 'Advanced')
|
||||
);
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
|
@@ -276,7 +276,7 @@ export namespace SketchControl {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch-control--toolbar',
|
||||
iconClass: 'fa fa-caret-down',
|
||||
iconClass: 'fa fa-arduino-sketch-tabs-menu',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,65 @@
|
||||
import { SaveableWidget } from '@theia/core/lib/browser/saveable';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { Sketch, SketchContribution } from './contribution';
|
||||
import { OpenSketchFiles } from './open-sketch-files';
|
||||
|
||||
@injectable()
|
||||
export class SketchFilesTracker extends SketchContribution {
|
||||
@inject(FileSystemFrontendContribution)
|
||||
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
|
||||
private readonly toDisposeOnStop = new DisposableCollection();
|
||||
|
||||
override onStart(): void {
|
||||
this.fileSystemFrontendContribution.onDidChangeEditorFile(
|
||||
({ type, editor }) => {
|
||||
if (type === FileChangeType.DELETED) {
|
||||
const editorWidget = editor;
|
||||
if (SaveableWidget.is(editorWidget)) {
|
||||
editorWidget.closeWithoutSaving();
|
||||
} else {
|
||||
editorWidget.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.sketchServiceClient.currentSketch().then(async (sketch) => {
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
this.toDisposeOnStop.push(
|
||||
this.fileService.onDidFilesChange(async (event) => {
|
||||
for (const { type, resource } of event.changes) {
|
||||
if (
|
||||
type === FileChangeType.ADDED &&
|
||||
resource.parent.toString() === sketch.uri
|
||||
) {
|
||||
const reloadedSketch = await this.sketchService.loadSketch(
|
||||
sketch.uri
|
||||
);
|
||||
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
||||
this.commandService.executeCommand(
|
||||
OpenSketchFiles.Commands.ENSURE_OPENED.id,
|
||||
resource.toString(),
|
||||
true,
|
||||
{
|
||||
mode: 'open',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDisposeOnStop.dispose();
|
||||
}
|
||||
}
|
@@ -5,7 +5,11 @@ import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Examples } from './examples';
|
||||
import { SketchContainer } from '../../common/protocol';
|
||||
import {
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@@ -24,15 +28,14 @@ export class Sketchbook extends Examples {
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
});
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.mainMenuManager.update();
|
||||
@@ -59,11 +62,24 @@ export class Sketchbook extends Examples {
|
||||
protected override createHandler(uri: string): CommandHandler {
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.loadSketch(uri);
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
let sketch: SketchRef | undefined = undefined;
|
||||
try {
|
||||
sketch = await this.sketchService.loadSketch(uri);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// To handle the following:
|
||||
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
|
||||
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
|
||||
this.messageService.error(err.message);
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
if (sketch) {
|
||||
await this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import type { IpcRendererEvent } from '@theia/core/electron-shared/electron';
|
||||
import { ipcRenderer } from '@theia/core/electron-shared/electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class StartupTasks extends Contribution {
|
||||
override onReady(): void {
|
||||
ipcRenderer.once(
|
||||
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
|
||||
(_: IpcRendererEvent, args: unknown) => {
|
||||
console.debug(
|
||||
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
|
||||
args
|
||||
)}`
|
||||
);
|
||||
if (!StartupTask.has(args)) {
|
||||
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
|
||||
return;
|
||||
}
|
||||
const tasks = args.tasks;
|
||||
if (tasks.length) {
|
||||
console.log(`Executing startup tasks:`);
|
||||
tasks.forEach(({ command, args = [] }) => {
|
||||
console.log(
|
||||
` - '${command}' ${
|
||||
args.length ? `, args: ${JSON.stringify(args)}` : ''
|
||||
}`
|
||||
);
|
||||
this.commandService
|
||||
.executeCommand(command, ...args)
|
||||
.catch((err) =>
|
||||
console.error(
|
||||
`Error occurred when executing the startup task '${command}'${
|
||||
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
|
||||
}.`,
|
||||
err
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
const { id } = remote.getCurrentWindow();
|
||||
console.debug(
|
||||
`Signalling app ready event to the electron main process. Sender ID: ${id}.`
|
||||
);
|
||||
ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id));
|
||||
}
|
||||
}
|
@@ -1,58 +1,49 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { BoardUserField, CoreService } from '../../common/protocol';
|
||||
import { BoardUserField, CoreService, Port } from '../../common/protocol';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } 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 {
|
||||
CoreServiceContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
CoreServiceContribution,
|
||||
} from './contribution';
|
||||
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import type { VerifySketchParams } from './verify-sketch';
|
||||
|
||||
@injectable()
|
||||
export class UploadSketch extends CoreServiceContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
protected readonly userFieldsDialog: UserFieldsDialog;
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
protected cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private boardRequiresUserFields = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
|
||||
protected uploadInProgress = false;
|
||||
protected boardRequiresUserFields = false;
|
||||
|
||||
protected readonly menuActionsDisposables = new DisposableCollection();
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private uploadInProgress = false;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceClientImpl.selectedBoardUserFields();
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return '';
|
||||
@@ -70,13 +61,14 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
|
||||
if (
|
||||
this.boardRequiresUserFields &&
|
||||
key &&
|
||||
!this.cachedUserFields.has(key)
|
||||
) {
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
await this.boardsServiceClientImpl.selectedBoardUserFields()
|
||||
await this.boardsServiceProvider.selectedBoardUserFields()
|
||||
).map((f) => ({ ...f }));
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
@@ -98,8 +90,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
cached ??
|
||||
(await this.boardsServiceClientImpl.selectedBoardUserFields())
|
||||
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
|
||||
).map((f) => ({ ...f }));
|
||||
|
||||
const result = await this.userFieldsDialog.open();
|
||||
@@ -130,7 +121,6 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
@@ -153,7 +143,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
{ order: '2' }
|
||||
)
|
||||
)
|
||||
@@ -193,46 +183,44 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
}
|
||||
|
||||
async uploadSketch(usingProgrammer = false): Promise<void> {
|
||||
// even with buttons disabled, better to double check if an upload is already in progress
|
||||
if (this.uploadInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// toggle the toolbar button and menu item state.
|
||||
// uploadInProgress will be set to false whether the upload fails or not
|
||||
this.uploadInProgress = true;
|
||||
this.coreErrorHandler.reset();
|
||||
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
|
||||
this.onDidChangeEmitter.fire();
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
|
||||
await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(
|
||||
boardsConfig.selectedBoard?.fqbn
|
||||
),
|
||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
this.sourceOverride(),
|
||||
]);
|
||||
this.clearVisibleNotification();
|
||||
|
||||
const board = {
|
||||
...boardsConfig.selectedBoard,
|
||||
name: boardsConfig.selectedBoard?.name || '',
|
||||
fqbn,
|
||||
};
|
||||
let options: CoreService.Upload.Options | undefined = undefined;
|
||||
const optimizeForDebug = this.editorMode.compileForDebug;
|
||||
const { selectedPort } = boardsConfig;
|
||||
const port = selectedPort;
|
||||
const userFields =
|
||||
this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
|
||||
if (userFields.length === 0 && this.boardRequiresUserFields) {
|
||||
const verifyOptions =
|
||||
await this.commandService.executeCommand<CoreService.Options.Compile>(
|
||||
'arduino-verify-sketch',
|
||||
<VerifySketchParams>{
|
||||
exportBinaries: false,
|
||||
silent: true,
|
||||
}
|
||||
);
|
||||
if (!verifyOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadOptions = await this.uploadOptions(
|
||||
usingProgrammer,
|
||||
verifyOptions
|
||||
);
|
||||
if (!uploadOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (
|
||||
uploadOptions.userFields.length === 0 &&
|
||||
this.boardRequiresUserFields
|
||||
) {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
@@ -242,37 +230,13 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (usingProgrammer) {
|
||||
const programmer = selectedProgrammer;
|
||||
options = {
|
||||
sketch,
|
||||
board,
|
||||
optimizeForDebug,
|
||||
programmer,
|
||||
port,
|
||||
verbose,
|
||||
verify,
|
||||
sourceOverride,
|
||||
userFields,
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
sketch,
|
||||
board,
|
||||
optimizeForDebug,
|
||||
port,
|
||||
verbose,
|
||||
verify,
|
||||
sourceOverride,
|
||||
userFields,
|
||||
};
|
||||
}
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
if (usingProgrammer) {
|
||||
await this.coreService.uploadUsingProgrammer(options);
|
||||
} else {
|
||||
await this.coreService.upload(options);
|
||||
}
|
||||
await this.doWithProgress({
|
||||
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
|
||||
task: (progressId, coreService) =>
|
||||
coreService.upload({ ...uploadOptions, progressId }),
|
||||
keepOutput: true,
|
||||
});
|
||||
|
||||
this.messageService.info(
|
||||
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
|
||||
{ timeout: 3000 }
|
||||
@@ -281,9 +245,78 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
this.handleError(e);
|
||||
} finally {
|
||||
this.uploadInProgress = false;
|
||||
this.boardsServiceProvider.attemptPostUploadAutoSelect();
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadOptions(
|
||||
usingProgrammer: boolean,
|
||||
verifyOptions: CoreService.Options.Compile
|
||||
): Promise<CoreService.Options.Upload | undefined> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return undefined;
|
||||
}
|
||||
const userFields = this.userFields();
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
verifyOptions.fqbn, // already decorated FQBN
|
||||
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
|
||||
return {
|
||||
sketch,
|
||||
fqbn,
|
||||
...(usingProgrammer && { programmer }),
|
||||
port,
|
||||
verbose,
|
||||
verify,
|
||||
userFields,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740)
|
||||
* This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236).
|
||||
*
|
||||
* Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`.
|
||||
* This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`.
|
||||
*/
|
||||
private maybeUpdatePortProperties(port: Port | undefined): Port | undefined {
|
||||
if (port) {
|
||||
const key = Port.keyOf(port);
|
||||
for (const candidate of this.boardsServiceProvider.availablePorts) {
|
||||
if (key === Port.keyOf(candidate) && candidate.properties) {
|
||||
return {
|
||||
...port,
|
||||
properties: deepClone(candidate.properties),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
private userFields(): BoardUserField[] {
|
||||
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
|
||||
*/
|
||||
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const [vendor, arch, id] = fqbn.split(':');
|
||||
return `${vendor}:${arch}:${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UploadSketch {
|
||||
@@ -291,7 +324,7 @@ export namespace UploadSketch {
|
||||
export const UPLOAD_SKETCH: Command = {
|
||||
id: 'arduino-upload-sketch',
|
||||
};
|
||||
export const UPLOAD_WITH_CONFIGURATION: Command = {
|
||||
export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = {
|
||||
id: 'arduino-upload-with-configuration-sketch',
|
||||
label: nls.localize(
|
||||
'arduino/sketch/configureAndUpload',
|
||||
|
@@ -2,8 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
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 {
|
||||
CoreServiceContribution,
|
||||
Command,
|
||||
@@ -14,27 +12,36 @@ import {
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { CoreErrorHandler } from './core-error-handler';
|
||||
|
||||
export interface VerifySketchParams {
|
||||
/**
|
||||
* Same as `CoreService.Options.Compile#exportBinaries`
|
||||
*/
|
||||
readonly exportBinaries?: boolean;
|
||||
/**
|
||||
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
|
||||
*/
|
||||
readonly silent?: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class VerifySketch extends CoreServiceContribution {
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
@inject(CoreErrorHandler)
|
||||
private readonly coreErrorHandler: CoreErrorHandler;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
|
||||
protected verifyInProgress = false;
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private verifyInProgress = false;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
|
||||
execute: () => this.verifySketch(),
|
||||
execute: (params?: VerifySketchParams) => this.verifySketch(params),
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
|
||||
execute: () => this.verifySketch(true),
|
||||
execute: () => this.verifySketch({ exportBinaries: true }),
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
|
||||
@@ -84,57 +91,88 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async verifySketch(exportBinaries?: boolean): Promise<void> {
|
||||
// even with buttons disabled, better to double check if a verify is already in progress
|
||||
protected override handleError(error: unknown): void {
|
||||
this.coreErrorHandler.tryHandle(error);
|
||||
super.handleError(error);
|
||||
}
|
||||
|
||||
private async verifySketch(
|
||||
params?: VerifySketchParams
|
||||
): Promise<CoreService.Options.Compile | undefined> {
|
||||
if (this.verifyInProgress) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// toggle the toolbar button and menu item state.
|
||||
// verifyInProgress will be set to false whether the compilation fails or not
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.verifyInProgress = true;
|
||||
if (!params?.silent) {
|
||||
this.verifyInProgress = true;
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
this.clearVisibleNotification();
|
||||
this.coreErrorHandler.reset();
|
||||
this.onDidChangeEmitter.fire();
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const [fqbn, sourceOverride] = await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(
|
||||
boardsConfig.selectedBoard?.fqbn
|
||||
|
||||
const options = await this.options(params?.exportBinaries);
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this.doWithProgress({
|
||||
progressText: nls.localize(
|
||||
'arduino/sketch/compile',
|
||||
'Compiling sketch...'
|
||||
),
|
||||
this.sourceOverride(),
|
||||
]);
|
||||
const board = {
|
||||
...boardsConfig.selectedBoard,
|
||||
name: boardsConfig.selectedBoard?.name || '',
|
||||
fqbn,
|
||||
};
|
||||
const verbose = this.preferences.get('arduino.compile.verbose');
|
||||
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
await this.coreService.compile({
|
||||
sketch,
|
||||
board,
|
||||
optimizeForDebug: this.editorMode.compileForDebug,
|
||||
verbose,
|
||||
exportBinaries,
|
||||
sourceOverride,
|
||||
compilerWarnings,
|
||||
task: (progressId, coreService) =>
|
||||
coreService.compile({
|
||||
...options,
|
||||
progressId,
|
||||
}),
|
||||
});
|
||||
this.messageService.info(
|
||||
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
// Returns with the used options for the compilation
|
||||
// so that follow-up tasks (such as upload) can reuse the compiled code.
|
||||
// Note that the `fqbn` is already decorated with the board settings, if any.
|
||||
return options;
|
||||
} catch (e) {
|
||||
this.handleError(e);
|
||||
return undefined;
|
||||
} finally {
|
||||
this.verifyInProgress = false;
|
||||
this.onDidChangeEmitter.fire();
|
||||
if (!params?.silent) {
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async options(
|
||||
exportBinaries?: boolean
|
||||
): Promise<CoreService.Options.Compile | undefined> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return undefined;
|
||||
}
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([
|
||||
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
||||
this.sourceOverride(),
|
||||
this.commandService.executeCommand<boolean>(
|
||||
'arduino-is-optimize-for-debug'
|
||||
),
|
||||
]);
|
||||
const verbose = this.preferences.get('arduino.compile.verbose');
|
||||
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
||||
return {
|
||||
sketch,
|
||||
fqbn,
|
||||
optimizeForDebug: Boolean(optimizeForDebug),
|
||||
verbose,
|
||||
exportBinaries,
|
||||
sourceOverride,
|
||||
compilerWarnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace VerifySketch {
|
||||
|
@@ -507,7 +507,8 @@ export class CreateApi {
|
||||
}
|
||||
|
||||
private domain(apiVersion = 'v2'): string {
|
||||
const endpoint = this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint'];
|
||||
const endpoint =
|
||||
this.arduinoPreferences['arduino.cloud.sketchSyncEndpoint'];
|
||||
return `${endpoint}/${apiVersion}`;
|
||||
}
|
||||
|
||||
|
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"tokenColors": [
|
||||
{
|
||||
"settings": {
|
||||
"foreground": "#434f54"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Comments",
|
||||
"scope": "comment",
|
||||
"settings": {
|
||||
"foreground": "#95a5a6cc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Keywords Attributes",
|
||||
"scope": [
|
||||
"storage",
|
||||
"support",
|
||||
"string.quoted.single.c"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#00979D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "literal",
|
||||
"scope": [
|
||||
"meta.function.c",
|
||||
"entity.name.function",
|
||||
"meta.function-call.c",
|
||||
"variable.other"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#D35400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "punctuation",
|
||||
"scope": [
|
||||
"punctuation.section",
|
||||
"meta.function-call.c",
|
||||
"meta.block.c",
|
||||
"meta.function.c",
|
||||
"entity.name.function.preprocessor.c",
|
||||
"meta.preprocessor.macro.c",
|
||||
"variable",
|
||||
"variable.name"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#434f54"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constants",
|
||||
"scope": [
|
||||
"string.quoted.double",
|
||||
"constant"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#005C5F"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "meta keywords",
|
||||
"scope": [
|
||||
"keyword.control",
|
||||
"meta.preprocessor.c"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#728E00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "numeric preprocessor",
|
||||
"scope": [
|
||||
"meta.preprocessor.macro.c",
|
||||
"constant.numeric.preprocessor.c",
|
||||
"meta.preprocessor.c"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#434f54"
|
||||
}
|
||||
}
|
||||
],
|
||||
"colors": {
|
||||
"list.highlightForeground": "#005c5f",
|
||||
"list.activeSelectionForeground": "#424242",
|
||||
"list.activeSelectionBackground": "#DAE3E3",
|
||||
"list.inactiveSelectionForeground": "#424242",
|
||||
"list.inactiveSelectionBackground": "#DAE3E3",
|
||||
"list.hoverBackground": "#ECF1F1",
|
||||
"progressBar.background": "#005c5f",
|
||||
"editor.background": "#ffffff",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editor.foreground": "#434f54",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#ffcb00",
|
||||
"editorWidget.background": "#F7F9F9",
|
||||
"focusBorder": "#7fcbcd99",
|
||||
"menubar.selectionBackground": "#ffffff",
|
||||
"menubar.selectionForeground": "#212121",
|
||||
"menu.selectionBackground": "#dae3e3",
|
||||
"menu.selectionForeground": "#212121",
|
||||
"editorGroupHeader.tabsBackground": "#f7f9f9",
|
||||
"button.background": "#7fcbcd",
|
||||
"titleBar.activeBackground": "#005c5f",
|
||||
"titleBar.activeForeground": "#ffffff",
|
||||
"terminal.background": "#000000",
|
||||
"terminal.foreground": "#e0e0e0",
|
||||
"dropdown.border": "#ececec",
|
||||
"dropdown.background": "#ececec",
|
||||
"activityBar.background": "#ececec",
|
||||
"activityBar.foreground": "#616161",
|
||||
"statusBar.background": "#005c5f",
|
||||
"secondaryButton.background": "#b5c8c9",
|
||||
"secondaryButton.foreground": "#ececec",
|
||||
"secondaryButton.hoverBackground": "#dae3e3",
|
||||
"arduino.branding.primary": "#00979d",
|
||||
"arduino.branding.secondary": "#b5c8c9",
|
||||
"arduino.foreground": "#edf1f1",
|
||||
"arduino.output.foreground": "#FFFFFF",
|
||||
"arduino.output.background": "#000000"
|
||||
},
|
||||
"type": "light",
|
||||
"name": "Arduino"
|
||||
}
|
@@ -4,18 +4,20 @@
|
||||
"colors": {
|
||||
"list.highlightForeground": "#0ca1a6",
|
||||
"list.activeSelectionForeground": "#dae3e3",
|
||||
"list.activeSelectionBackground": "#434f54",
|
||||
"list.activeSelectionBackground": "#0ca1a64d",
|
||||
"list.inactiveSelectionForeground": "#dae3e3",
|
||||
"list.inactiveSelectionBackground": "#434f54",
|
||||
"list.hoverBackground": "#1f272a",
|
||||
"list.activeSelectionIconForeground": "#0ca1a6",
|
||||
"progressBar.background": "#005c5f",
|
||||
"editor.background": "#1f272a",
|
||||
"editor.foreground": "#dae3e3",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#f1c40f",
|
||||
"editor.selectionBackground": "#00818480",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editorWidget.background": "#171e21",
|
||||
"editorWidget.foreground": "#dae3e3",
|
||||
"focusBorder": "#dae3e3",
|
||||
"menubar.selectionBackground": "#ffffff",
|
||||
"menubar.selectionForeground": "#212121",
|
||||
@@ -23,10 +25,12 @@
|
||||
"menu.selectionForeground": "#212121",
|
||||
"editorGroupHeader.tabsBackground": "#171e21",
|
||||
"button.background": "#0ca1a6",
|
||||
"button.foreground": "#101618",
|
||||
"button.hoverBackground": "#7fcbcd",
|
||||
"titleBar.activeBackground": "#171e21",
|
||||
"titleBar.activeForeground": "#dae3e3",
|
||||
"terminal.background": "#000000",
|
||||
"terminal.foreground": "#e0e0e0",
|
||||
"terminal.foreground": "#ffffff",
|
||||
"dropdown.border": "#7fcbcd",
|
||||
"dropdown.background": "#2c353a",
|
||||
"dropdown.foreground": "#dae3e3",
|
||||
@@ -37,20 +41,33 @@
|
||||
"statusBar.background": "#171e21",
|
||||
"secondaryButton.background": "#ff000000",
|
||||
"secondaryButton.foreground": "#dae3e3",
|
||||
"secondaryButton.hoverBackground": "#434f54",
|
||||
"secondaryButton.hoverBackground": "#ffffff1a",
|
||||
"arduino.branding.primary": "#0ca1a6",
|
||||
"arduino.branding.secondary": "#b5c8c9",
|
||||
"arduino.foreground": "#edf1f1",
|
||||
"arduino.output.foreground": "#ffffff",
|
||||
"arduino.output.background": "#000000",
|
||||
"arduino.toolbar.hoverBackground": "#dae3e3",
|
||||
"arduino.toolbar.button.hoverBackground": "#dae3e3",
|
||||
"arduino.toolbar.button.secondary.label": "#dae3e3",
|
||||
"arduino.toolbar.button.secondary.hoverBackground": "#dae3e366",
|
||||
"arduino.toolbar.button.background": "#0ca1a6",
|
||||
"arduino.toolbar.dropdown.border": "#7fcbcd",
|
||||
"arduino.toolbar.dropdown.borderActive": "#0ca1a6",
|
||||
"arduino.toolbar.dropdown.background": "#2c353a",
|
||||
"arduino.toolbar.dropdown.label": "#dae3e3",
|
||||
"arduino.toolbar.dropdown.iconSelected": "#3fae98",
|
||||
"arduino.toolbar.dropdown.option.backgroundHover": "#374146",
|
||||
"arduino.toolbar.dropdown.option.backgroundSelected": "#4e5b61",
|
||||
"arduino.toolbar.toggleBackground": "#f1c40f",
|
||||
"sideBar.background": "#101618",
|
||||
"sideBar.foreground": "#dae3e3",
|
||||
"input.background": "#000000",
|
||||
"foreground": "#dae3e3",
|
||||
"settings.headerForeground": "#dae3e3",
|
||||
"tree.indentGuidesStroke": "#374146",
|
||||
"tab.unfocusedActiveForeground": "#dae3e3",
|
||||
"tab.inactiveBackground": "#171e21"
|
||||
"tab.inactiveBackground": "#171e21",
|
||||
"textLink.foreground": "#0ca1a6"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
@@ -146,4 +163,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -4,30 +4,34 @@
|
||||
"colors": {
|
||||
"list.highlightForeground": "#008184",
|
||||
"list.activeSelectionForeground": "#4e5b61",
|
||||
"list.activeSelectionBackground": "#dae3e3",
|
||||
"list.activeSelectionBackground": "#00818433",
|
||||
"list.inactiveSelectionForeground": "#4e5b61",
|
||||
"list.inactiveSelectionBackground": "#dae3e3",
|
||||
"list.hoverBackground": "#ecf1f1",
|
||||
"list.activeSelectionIconForeground": "#008184",
|
||||
"progressBar.background": "#005c5f",
|
||||
"editor.background": "#ffffff",
|
||||
"editor.foreground": "#4e5b61",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#f1c40f",
|
||||
"editor.selectionBackground": "#7fcbcdb3",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editorWidget.background": "#f7f9f9",
|
||||
"editorWidget.foreground": "#4e5b61",
|
||||
"focusBorder": "#7fcbcd",
|
||||
"menubar.selectionBackground": "#ffffff",
|
||||
"menubar.selectionForeground": "#212121",
|
||||
"menu.selectionBackground": "#dae3e3",
|
||||
"menu.selectionForeground": "#212121",
|
||||
"editorGroupHeader.tabsBackground": "#ecf1f1",
|
||||
"button.background": "#7fcbcd",
|
||||
"button.background": "#008184",
|
||||
"button.foreground": "#f7f9f9",
|
||||
"button.hoverBackground": "#005C5F",
|
||||
"titleBar.activeBackground": "#006d70",
|
||||
"titleBar.activeForeground": "#f7f9f9",
|
||||
"terminal.background": "#000000",
|
||||
"terminal.foreground": "#e0e0e0",
|
||||
"dropdown.border": "#f7f9f9",
|
||||
"terminal.foreground": "#ffffff",
|
||||
"dropdown.border": "#dae3e3",
|
||||
"dropdown.background": "#ffffff",
|
||||
"dropdown.foreground": "#4e5b61",
|
||||
"activityBar.background": "#ecf1f1",
|
||||
@@ -37,20 +41,33 @@
|
||||
"statusBar.background": "#006d70",
|
||||
"secondaryButton.background": "#ff000000",
|
||||
"secondaryButton.foreground": "#008184",
|
||||
"secondaryButton.hoverBackground": "#dae3e3",
|
||||
"secondaryButton.hoverBackground": "#005c5f1a",
|
||||
"arduino.branding.primary": "#008184",
|
||||
"arduino.branding.secondary": "#b5c8c9",
|
||||
"arduino.foreground": "#edf1f1",
|
||||
"arduino.output.foreground": "#ffffff",
|
||||
"arduino.output.background": "#000000",
|
||||
"arduino.toolbar.hoverBackground": "#f7f9f9",
|
||||
"arduino.toolbar.button.hoverBackground": "#f7f9f9",
|
||||
"arduino.toolbar.button.secondary.label": "#dae3e3",
|
||||
"arduino.toolbar.button.secondary.hoverBackground": "#dae3e366",
|
||||
"arduino.toolbar.button.background": "#7fcbcd",
|
||||
"arduino.toolbar.dropdown.border": "#dae3e3",
|
||||
"arduino.toolbar.dropdown.borderActive": "#7fcbcd",
|
||||
"arduino.toolbar.dropdown.background": "#ffffff",
|
||||
"arduino.toolbar.dropdown.label": "#4e5b61",
|
||||
"arduino.toolbar.dropdown.iconSelected": "#1da086",
|
||||
"arduino.toolbar.dropdown.option.backgroundHover": "#ecf1f1",
|
||||
"arduino.toolbar.dropdown.option.backgroundSelected": "#dae3e3",
|
||||
"arduino.toolbar.toggleBackground": "#f1c40f",
|
||||
"sideBar.background": "#f7f9f9",
|
||||
"sideBar.foreground": "#4e5b61",
|
||||
"input.background": "#ffffff",
|
||||
"foreground": "#4e5b61",
|
||||
"settings.headerForeground": "#4e5b61",
|
||||
"tree.indentGuidesStroke": "#dae3e3",
|
||||
"tab.unfocusedActiveForeground": "#4e5b61",
|
||||
"tab.inactiveBackground": "#ecf1f1"
|
||||
"tab.inactiveBackground": "#ecf1f1",
|
||||
"textLink.foreground": "#008184"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
@@ -146,4 +163,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -94,7 +94,7 @@ export const CertificateUploaderComponent = ({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="theia-button primary add-cert-btn"
|
||||
className="theia-button secondary add-cert-btn"
|
||||
onClick={() => {
|
||||
showAdd ? setShowAdd(false) : setShowAdd(true);
|
||||
}}
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { AbstractDialog } from '../../theia/dialogs/dialogs';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
@@ -19,6 +23,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { certificateList, sanifyCertString } from './utils';
|
||||
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
|
||||
@injectable()
|
||||
export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
@@ -37,6 +42,9 @@ export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected certificates: string[] = [];
|
||||
protected updatableFqbns: string[] = [];
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
@@ -66,10 +74,12 @@ export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
}
|
||||
});
|
||||
|
||||
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
|
||||
this.updatableFqbns = fqbns;
|
||||
this.update();
|
||||
});
|
||||
this.appStateService.reachedState('ready').then(() =>
|
||||
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
|
||||
this.updatableFqbns = fqbns;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
|
||||
this.availableBoards = availableBoards;
|
||||
@@ -147,6 +157,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
|
||||
'Upload SSL Root Certificates'
|
||||
),
|
||||
});
|
||||
this.node.id = 'certificate-uploader-dialog-container';
|
||||
this.contentNode.classList.add('certificate-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
@@ -101,6 +101,7 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
protected override readonly props: UploadFirmwareDialogProps
|
||||
) {
|
||||
super({ title: UploadFirmware.Commands.OPEN.label || '' });
|
||||
this.node.id = 'firmware-uploader-dialog-container';
|
||||
this.contentNode.classList.add('firmware-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { shell } from 'electron';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
@@ -7,36 +6,32 @@ import ReactMarkdown from 'react-markdown';
|
||||
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
|
||||
import ProgressBar from '../../components/ProgressBar';
|
||||
|
||||
export type IDEUpdaterComponentProps = {
|
||||
updateInfo: UpdateInfo;
|
||||
windowService: WindowService;
|
||||
export interface UpdateProgress {
|
||||
progressInfo?: ProgressInfo | undefined;
|
||||
downloadFinished?: boolean;
|
||||
downloadStarted?: boolean;
|
||||
progress?: ProgressInfo;
|
||||
error?: Error;
|
||||
onDownload: () => void;
|
||||
onClose: () => void;
|
||||
onSkipVersion: () => void;
|
||||
onCloseAndInstall: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDEUpdaterComponentProps {
|
||||
updateInfo: UpdateInfo;
|
||||
updateProgress: UpdateProgress;
|
||||
}
|
||||
|
||||
export const IDEUpdaterComponent = ({
|
||||
updateInfo: { version, releaseNotes },
|
||||
downloadStarted = false,
|
||||
downloadFinished = false,
|
||||
windowService,
|
||||
progress,
|
||||
error,
|
||||
onDownload,
|
||||
onClose,
|
||||
onSkipVersion,
|
||||
onCloseAndInstall,
|
||||
updateInfo,
|
||||
updateProgress: {
|
||||
downloadStarted = false,
|
||||
downloadFinished = false,
|
||||
progressInfo,
|
||||
error,
|
||||
},
|
||||
}: IDEUpdaterComponentProps): React.ReactElement => {
|
||||
const changelogDivRef = React.useRef() as React.MutableRefObject<
|
||||
HTMLDivElement
|
||||
>;
|
||||
const { version, releaseNotes } = updateInfo;
|
||||
const changelogDivRef =
|
||||
React.useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
React.useEffect(() => {
|
||||
if (!!releaseNotes) {
|
||||
if (!!releaseNotes && changelogDivRef.current) {
|
||||
let changelog: string;
|
||||
if (typeof releaseNotes === 'string') changelog = releaseNotes;
|
||||
else
|
||||
@@ -58,12 +53,7 @@ export const IDEUpdaterComponent = ({
|
||||
changelogDivRef.current
|
||||
);
|
||||
}
|
||||
}, [releaseNotes]);
|
||||
const closeButton = (
|
||||
<button onClick={onClose} type="button" className="theia-button secondary">
|
||||
{nls.localize('arduino/ide-updater/notNowButton', 'Not now')}
|
||||
</button>
|
||||
);
|
||||
}, [updateInfo]);
|
||||
|
||||
const DownloadCompleted: () => React.ReactElement = () => (
|
||||
<div className="ide-updater-dialog--downloaded">
|
||||
@@ -80,19 +70,6 @@ export const IDEUpdaterComponent = ({
|
||||
'Close the software and install the update on your machine.'
|
||||
)}
|
||||
</div>
|
||||
<div className="buttons-container">
|
||||
{closeButton}
|
||||
<button
|
||||
onClick={onCloseAndInstall}
|
||||
type="button"
|
||||
className="theia-button close-and-install"
|
||||
>
|
||||
{nls.localize(
|
||||
'arduino/ide-updater/closeAndInstallButton',
|
||||
'Close and Install'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,7 +81,7 @@ export const IDEUpdaterComponent = ({
|
||||
'Downloading the latest version of the Arduino IDE.'
|
||||
)}
|
||||
</div>
|
||||
<ProgressBar percent={progress?.percent} showPercentage />
|
||||
<ProgressBar percent={progressInfo?.percent} showPercentage />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -130,46 +107,14 @@ export const IDEUpdaterComponent = ({
|
||||
)}
|
||||
</div>
|
||||
{releaseNotes && (
|
||||
<div className="dialogRow">
|
||||
<div className="changelog-container" ref={changelogDivRef} />
|
||||
<div className="dialogRow changelog-container">
|
||||
<div className="changelog" ref={changelogDivRef} />
|
||||
</div>
|
||||
)}
|
||||
<div className="buttons-container">
|
||||
<button
|
||||
onClick={onSkipVersion}
|
||||
type="button"
|
||||
className="theia-button secondary skip-version"
|
||||
>
|
||||
{nls.localize(
|
||||
'arduino/ide-updater/skipVersionButton',
|
||||
'Skip Version'
|
||||
)}
|
||||
</button>
|
||||
<div className="push"></div>
|
||||
{closeButton}
|
||||
<button
|
||||
onClick={onDownload}
|
||||
type="button"
|
||||
className="theia-button primary"
|
||||
>
|
||||
{nls.localize('arduino/ide-updater/downloadButton', 'Download')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const onGoToDownloadClick = (
|
||||
event: React.SyntheticEvent<HTMLAnchorElement, Event>
|
||||
) => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
event.nativeEvent.preventDefault();
|
||||
windowService.openNewWindow(target.href, { external: true });
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const GoToDownloadPage: () => React.ReactElement = () => (
|
||||
<div className="ide-updater-dialog--go-to-download-page">
|
||||
<div>
|
||||
@@ -178,19 +123,6 @@ export const IDEUpdaterComponent = ({
|
||||
"An update for the Arduino IDE is available, but we're not able to download and install it automatically. Please go to the download page and download the latest version from there."
|
||||
)}
|
||||
</div>
|
||||
<div className="buttons-container">
|
||||
{closeButton}
|
||||
<a
|
||||
className="theia-button primary"
|
||||
href="https://www.arduino.cc/en/software#experimental-software"
|
||||
onClick={onGoToDownloadClick}
|
||||
>
|
||||
{nls.localize(
|
||||
'arduino/ide-updater/goToDownloadButton',
|
||||
'Go To Download'
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@@ -1,113 +1,56 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { AbstractDialog } from '../../theia/dialogs/dialogs';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { nls } from '@theia/core';
|
||||
import { IDEUpdaterComponent } from './ide-updater-component';
|
||||
|
||||
import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component';
|
||||
import {
|
||||
IDEUpdater,
|
||||
IDEUpdaterClient,
|
||||
ProgressInfo,
|
||||
SKIP_IDE_VERSION,
|
||||
UpdateInfo,
|
||||
} from '../../../common/protocol/ide-updater';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { SKIP_IDE_VERSION } from '../../arduino-frontend-contribution';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
|
||||
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
|
||||
|
||||
@injectable()
|
||||
export class IDEUpdaterDialogWidget extends ReactWidget {
|
||||
protected isOpen = new Object();
|
||||
updateInfo: UpdateInfo;
|
||||
progressInfo: ProgressInfo | undefined;
|
||||
error: Error | undefined;
|
||||
downloadFinished: boolean;
|
||||
downloadStarted: boolean;
|
||||
onClose: () => void;
|
||||
private _updateInfo: UpdateInfo;
|
||||
private _updateProgress: UpdateProgress = {};
|
||||
|
||||
@inject(IDEUpdater)
|
||||
protected readonly updater: IDEUpdater;
|
||||
|
||||
@inject(IDEUpdaterClient)
|
||||
protected readonly updaterClient: IDEUpdaterClient;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected readonly localStorageService: LocalStorageService;
|
||||
|
||||
@inject(WindowService)
|
||||
protected windowService: WindowService;
|
||||
|
||||
init(updateInfo: UpdateInfo, onClose: () => void): void {
|
||||
this.updateInfo = updateInfo;
|
||||
this.progressInfo = undefined;
|
||||
this.error = undefined;
|
||||
this.downloadStarted = false;
|
||||
this.downloadFinished = false;
|
||||
this.onClose = onClose;
|
||||
|
||||
this.updaterClient.onError((e) => {
|
||||
this.error = e;
|
||||
this.update();
|
||||
});
|
||||
this.updaterClient.onDownloadProgressChanged((e) => {
|
||||
this.progressInfo = e;
|
||||
this.update();
|
||||
});
|
||||
this.updaterClient.onDownloadFinished((e) => {
|
||||
this.downloadFinished = true;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
async onSkipVersion(): Promise<void> {
|
||||
this.localStorageService.setData<string>(
|
||||
SKIP_IDE_VERSION,
|
||||
this.updateInfo.version
|
||||
);
|
||||
this.close();
|
||||
}
|
||||
|
||||
override close(): void {
|
||||
super.close();
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
onDispose(): void {
|
||||
if (this.downloadStarted && !this.downloadFinished)
|
||||
this.updater.stopDownload();
|
||||
}
|
||||
|
||||
async onDownload(): Promise<void> {
|
||||
this.progressInfo = undefined;
|
||||
this.downloadStarted = true;
|
||||
this.error = undefined;
|
||||
this.updater.downloadUpdate();
|
||||
setUpdateInfo(updateInfo: UpdateInfo): void {
|
||||
this._updateInfo = updateInfo;
|
||||
this.update();
|
||||
}
|
||||
|
||||
onCloseAndInstall(): void {
|
||||
this.updater.quitAndInstall();
|
||||
mergeUpdateProgress(updateProgress: UpdateProgress): void {
|
||||
this._updateProgress = { ...this._updateProgress, ...updateProgress };
|
||||
this.update();
|
||||
}
|
||||
|
||||
get updateInfo(): UpdateInfo {
|
||||
return this._updateInfo;
|
||||
}
|
||||
|
||||
get updateProgress(): UpdateProgress {
|
||||
return this._updateProgress;
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return !!this.updateInfo ? (
|
||||
<form>
|
||||
<IDEUpdaterComponent
|
||||
updateInfo={this.updateInfo}
|
||||
windowService={this.windowService}
|
||||
downloadStarted={this.downloadStarted}
|
||||
downloadFinished={this.downloadFinished}
|
||||
progress={this.progressInfo}
|
||||
error={this.error}
|
||||
onClose={this.close.bind(this)}
|
||||
onSkipVersion={this.onSkipVersion.bind(this)}
|
||||
onDownload={this.onDownload.bind(this)}
|
||||
onCloseAndInstall={this.onCloseAndInstall.bind(this)}
|
||||
/>
|
||||
</form>
|
||||
return !!this._updateInfo ? (
|
||||
<IDEUpdaterComponent
|
||||
updateInfo={this._updateInfo}
|
||||
updateProgress={this._updateProgress}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +61,19 @@ export class IDEUpdaterDialogProps extends DialogProps {}
|
||||
@injectable()
|
||||
export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
@inject(IDEUpdaterDialogWidget)
|
||||
protected readonly widget: IDEUpdaterDialogWidget;
|
||||
private readonly widget: IDEUpdaterDialogWidget;
|
||||
|
||||
@inject(IDEUpdater)
|
||||
private readonly updater: IDEUpdater;
|
||||
|
||||
@inject(IDEUpdaterClient)
|
||||
private readonly updaterClient: IDEUpdaterClient;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorageService: LocalStorageService;
|
||||
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
constructor(
|
||||
@inject(IDEUpdaterDialogProps)
|
||||
@@ -130,10 +85,26 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
'Software Update'
|
||||
),
|
||||
});
|
||||
this.node.id = 'ide-updater-dialog-container';
|
||||
this.contentNode.classList.add('ide-updater-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.updaterClient.onUpdaterDidFail((error) => {
|
||||
this.appendErrorButtons();
|
||||
this.widget.mergeUpdateProgress({ error });
|
||||
});
|
||||
this.updaterClient.onDownloadProgressDidChange((progressInfo) => {
|
||||
this.widget.mergeUpdateProgress({ progressInfo });
|
||||
});
|
||||
this.updaterClient.onDownloadDidFinish(() => {
|
||||
this.appendInstallButtons();
|
||||
this.widget.mergeUpdateProgress({ downloadFinished: true });
|
||||
});
|
||||
}
|
||||
|
||||
get value(): UpdateInfo {
|
||||
return this.widget.updateInfo;
|
||||
}
|
||||
@@ -143,24 +114,123 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
this.appendInitialButtons();
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
private clearButtons(): void {
|
||||
while (this.controlPanel.firstChild) {
|
||||
this.controlPanel.removeChild(this.controlPanel.firstChild);
|
||||
}
|
||||
this.closeButton = undefined;
|
||||
}
|
||||
|
||||
private appendNotNowButton(): void {
|
||||
this.appendCloseButton(
|
||||
nls.localize('arduino/ide-updater/notNowButton', 'Not now')
|
||||
);
|
||||
if (this.closeButton) {
|
||||
this.addCloseAction(this.closeButton, 'click');
|
||||
}
|
||||
}
|
||||
|
||||
private appendInitialButtons(): void {
|
||||
this.clearButtons();
|
||||
|
||||
const skipVersionButton = this.createButton(
|
||||
nls.localize('arduino/ide-updater/skipVersionButton', 'Skip Version')
|
||||
);
|
||||
skipVersionButton.classList.add('secondary');
|
||||
skipVersionButton.classList.add('skip-version-button');
|
||||
this.addAction(skipVersionButton, this.skipVersion.bind(this), 'click');
|
||||
this.controlPanel.appendChild(skipVersionButton);
|
||||
|
||||
this.appendNotNowButton();
|
||||
|
||||
const downloadButton = this.createButton(
|
||||
nls.localize('arduino/ide-updater/downloadButton', 'Download')
|
||||
);
|
||||
this.addAction(downloadButton, this.startDownload.bind(this), 'click');
|
||||
this.controlPanel.appendChild(downloadButton);
|
||||
downloadButton.focus();
|
||||
}
|
||||
|
||||
private appendInstallButtons(): void {
|
||||
this.clearButtons();
|
||||
this.appendNotNowButton();
|
||||
|
||||
const closeAndInstallButton = this.createButton(
|
||||
nls.localize(
|
||||
'arduino/ide-updater/closeAndInstallButton',
|
||||
'Close and Install'
|
||||
)
|
||||
);
|
||||
this.addAction(
|
||||
closeAndInstallButton,
|
||||
this.closeAndInstall.bind(this),
|
||||
'click'
|
||||
);
|
||||
this.controlPanel.appendChild(closeAndInstallButton);
|
||||
closeAndInstallButton.focus();
|
||||
}
|
||||
|
||||
private appendErrorButtons(): void {
|
||||
this.clearButtons();
|
||||
this.appendNotNowButton();
|
||||
|
||||
const goToDownloadPageButton = this.createButton(
|
||||
nls.localize('arduino/ide-updater/goToDownloadButton', 'Go To Download')
|
||||
);
|
||||
this.addAction(
|
||||
goToDownloadPageButton,
|
||||
this.openDownloadPage.bind(this),
|
||||
'click'
|
||||
);
|
||||
this.controlPanel.appendChild(goToDownloadPageButton);
|
||||
goToDownloadPageButton.focus();
|
||||
}
|
||||
|
||||
private openDownloadPage(): void {
|
||||
this.windowService.openNewWindow(DOWNLOAD_PAGE_URL, { external: true });
|
||||
this.close();
|
||||
}
|
||||
|
||||
private skipVersion(): void {
|
||||
this.localStorageService.setData<string>(
|
||||
SKIP_IDE_VERSION,
|
||||
this.widget.updateInfo.version
|
||||
);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private startDownload(): void {
|
||||
this.widget.mergeUpdateProgress({
|
||||
downloadStarted: true,
|
||||
});
|
||||
this.clearButtons();
|
||||
this.updater.downloadUpdate();
|
||||
}
|
||||
|
||||
private closeAndInstall() {
|
||||
this.updater.quitAndInstall();
|
||||
this.close();
|
||||
}
|
||||
|
||||
override async open(
|
||||
data: UpdateInfo | undefined = undefined
|
||||
): Promise<UpdateInfo | undefined> {
|
||||
if (data && data.version) {
|
||||
this.widget.init(data, this.close.bind(this));
|
||||
this.widget.mergeUpdateProgress({
|
||||
progressInfo: undefined,
|
||||
downloadStarted: false,
|
||||
downloadFinished: false,
|
||||
error: undefined,
|
||||
});
|
||||
this.widget.setUpdateInfo(data);
|
||||
return super.open();
|
||||
}
|
||||
}
|
||||
|
||||
protected override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
@@ -168,6 +238,12 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
|
||||
override close(): void {
|
||||
this.widget.dispose();
|
||||
if (
|
||||
this.widget.updateProgress?.downloadStarted &&
|
||||
!this.widget.updateProgress?.downloadFinished
|
||||
) {
|
||||
this.updater.stopDownload();
|
||||
}
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,15 @@ import {
|
||||
AsyncLocalizationProvider,
|
||||
LanguageInfo,
|
||||
} from '@theia/core/lib/common/i18n/localization';
|
||||
import SettingsStepInput from './settings-step-input';
|
||||
|
||||
const maxScale = 280;
|
||||
const minScale = -60;
|
||||
const scaleStep = 20;
|
||||
|
||||
const maxFontSize = 72;
|
||||
const minFontSize = 0;
|
||||
const fontSizeStep = 2;
|
||||
export class SettingsComponent extends React.Component<
|
||||
SettingsComponent.Props,
|
||||
SettingsComponent.State
|
||||
@@ -88,6 +96,8 @@ export class SettingsComponent extends React.Component<
|
||||
}
|
||||
|
||||
protected renderSettings(): React.ReactNode {
|
||||
const scalePercentage = 100 + this.state.interfaceScale * 20;
|
||||
|
||||
return (
|
||||
<div className="content noselect">
|
||||
{nls.localize(
|
||||
@@ -119,7 +129,7 @@ export class SettingsComponent extends React.Component<
|
||||
'Show files inside Sketches'
|
||||
)}
|
||||
</label>
|
||||
<div className="flex-line">
|
||||
<div className="column-container">
|
||||
<div className="column">
|
||||
<div className="flex-line">
|
||||
{nls.localize(
|
||||
@@ -160,14 +170,13 @@ export class SettingsComponent extends React.Component<
|
||||
</div>
|
||||
<div className="column">
|
||||
<div className="flex-line">
|
||||
<input
|
||||
className="theia-input small"
|
||||
type="number"
|
||||
step={1}
|
||||
pattern="[0-9]+"
|
||||
onKeyDown={this.numbersOnlyKeyDown}
|
||||
<SettingsStepInput
|
||||
value={this.state.editorFontSize}
|
||||
onChange={this.editorFontSizeDidChange}
|
||||
setSettingsStateValue={this.setFontSize}
|
||||
step={fontSizeStep}
|
||||
maxValue={maxFontSize}
|
||||
minValue={minFontSize}
|
||||
classNames={{ input: 'theia-input small' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-line">
|
||||
@@ -179,26 +188,22 @@ export class SettingsComponent extends React.Component<
|
||||
/>
|
||||
{nls.localize('arduino/preferences/automatic', 'Automatic')}
|
||||
</label>
|
||||
<input
|
||||
className="theia-input small with-margin"
|
||||
type="number"
|
||||
step={20}
|
||||
pattern="[0-9]+"
|
||||
onKeyDown={this.noopKeyDown}
|
||||
value={100 + this.state.interfaceScale * 20}
|
||||
onChange={this.interfaceScaleDidChange}
|
||||
/>
|
||||
%
|
||||
<div>
|
||||
<SettingsStepInput
|
||||
value={scalePercentage}
|
||||
setSettingsStateValue={this.setInterfaceScale}
|
||||
step={scaleStep}
|
||||
maxValue={maxScale}
|
||||
minValue={minScale}
|
||||
unitOfMeasure="%"
|
||||
classNames={{ input: 'theia-input small with-margin' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-line">
|
||||
<select
|
||||
className="theia-select"
|
||||
value={
|
||||
ThemeService.get()
|
||||
.getThemes()
|
||||
.find(({ id }) => id === this.state.themeId)?.label ||
|
||||
nls.localize('arduino/common/unknown', 'Unknown')
|
||||
}
|
||||
value={ThemeService.get().getCurrentTheme().label}
|
||||
onChange={this.themeDidChange}
|
||||
>
|
||||
{ThemeService.get()
|
||||
@@ -516,13 +521,8 @@ export class SettingsComponent extends React.Component<
|
||||
}
|
||||
};
|
||||
|
||||
protected editorFontSizeDidChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
): void => {
|
||||
const { value } = event.target;
|
||||
if (value) {
|
||||
this.setState({ editorFontSize: parseInt(value, 10) });
|
||||
}
|
||||
private setFontSize = (editorFontSize: number) => {
|
||||
this.setState({ editorFontSize });
|
||||
};
|
||||
|
||||
protected rawAdditionalUrlsValueDidChange = (
|
||||
@@ -539,18 +539,10 @@ export class SettingsComponent extends React.Component<
|
||||
this.setState({ autoScaleInterface: event.target.checked });
|
||||
};
|
||||
|
||||
protected interfaceScaleDidChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
): void => {
|
||||
const { value } = event.target;
|
||||
const percentage = parseInt(value, 10);
|
||||
if (isNaN(percentage)) {
|
||||
return;
|
||||
}
|
||||
private setInterfaceScale = (percentage: number) => {
|
||||
const interfaceScale = (percentage - 100) / 20;
|
||||
if (!isNaN(interfaceScale)) {
|
||||
this.setState({ interfaceScale });
|
||||
}
|
||||
|
||||
this.setState({ interfaceScale });
|
||||
};
|
||||
|
||||
protected verifyAfterUploadDidChange = (
|
||||
@@ -596,6 +588,9 @@ export class SettingsComponent extends React.Component<
|
||||
const theme = ThemeService.get().getThemes()[selectedIndex];
|
||||
if (theme) {
|
||||
this.setState({ themeId: theme.id });
|
||||
if (ThemeService.get().getCurrentTheme().id !== theme.id) {
|
||||
ThemeService.get().setCurrentTheme(theme.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
injectable,
|
||||
inject,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { DialogError, ReactWidget } from '@theia/core/lib/browser';
|
||||
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser';
|
||||
import { DialogError, DialogProps, ReactWidget } from '@theia/core/lib/browser';
|
||||
import { Settings, SettingsService } from './settings';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
@@ -12,6 +15,8 @@ import { nls } from '@theia/core/lib/common';
|
||||
import { SettingsComponent } from './settings-component';
|
||||
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
|
||||
import { AdditionalUrls } from '../../../common/protocol';
|
||||
import { AbstractDialog } from '../../theia/dialogs/dialogs';
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
|
||||
@injectable()
|
||||
export class SettingsWidget extends ReactWidget {
|
||||
@@ -59,6 +64,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
|
||||
protected override readonly props: SettingsDialogProps
|
||||
) {
|
||||
super(props);
|
||||
this.node.id = 'arduino-settings-dialog-container';
|
||||
this.contentNode.classList.add('arduino-settings-dialog');
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
@@ -73,7 +79,9 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
|
||||
);
|
||||
}
|
||||
|
||||
protected override async isValid(settings: Promise<Settings>): Promise<DialogError> {
|
||||
protected override async isValid(
|
||||
settings: Promise<Settings>
|
||||
): Promise<DialogError> {
|
||||
const result = await this.settingsService.validate(settings);
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
@@ -111,6 +119,17 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
|
||||
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
override async open(): Promise<Promise<Settings> | undefined> {
|
||||
const themeIdBeforeOpen = ThemeService.get().getCurrentTheme().id;
|
||||
const result = await super.open();
|
||||
if (!result) {
|
||||
if (ThemeService.get().getCurrentTheme().id !== themeIdBeforeOpen) {
|
||||
ThemeService.get().setCurrentTheme(themeIdBeforeOpen);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
|
||||
|
@@ -0,0 +1,102 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface SettingsStepInputProps {
|
||||
value: number;
|
||||
setSettingsStateValue: (value: number) => void;
|
||||
step: number;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
unitOfMeasure?: string;
|
||||
classNames?: { [key: string]: string };
|
||||
}
|
||||
|
||||
const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
props: SettingsStepInputProps
|
||||
) => {
|
||||
const {
|
||||
value,
|
||||
setSettingsStateValue,
|
||||
step,
|
||||
maxValue,
|
||||
minValue,
|
||||
unitOfMeasure,
|
||||
classNames,
|
||||
} = props;
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number => {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
const onStep = (
|
||||
roundingOperation: 'ceil' | 'floor',
|
||||
stepOperation: (a: number, b: number) => number
|
||||
): void => {
|
||||
const valueRoundedToScale = Math[roundingOperation](value / step) * step;
|
||||
const calculatedValue =
|
||||
valueRoundedToScale === value
|
||||
? stepOperation(value, step)
|
||||
: valueRoundedToScale;
|
||||
const newValue = clamp(calculatedValue, minValue, maxValue);
|
||||
|
||||
setSettingsStateValue(newValue);
|
||||
};
|
||||
|
||||
const onStepUp = (): void => {
|
||||
onStep('ceil', (a: number, b: number) => a + b);
|
||||
};
|
||||
|
||||
const onStepDown = (): void => {
|
||||
onStep('floor', (a: number, b: number) => a - b);
|
||||
};
|
||||
|
||||
const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { value: eventValue } = event.target;
|
||||
|
||||
if (eventValue === '') {
|
||||
setSettingsStateValue(0);
|
||||
}
|
||||
|
||||
const number = Number(eventValue);
|
||||
|
||||
if (!isNaN(number) && number !== value) {
|
||||
const newValue = clamp(number, minValue, maxValue);
|
||||
|
||||
setSettingsStateValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const upDisabled = value >= maxValue;
|
||||
const downDisabled = value <= minValue;
|
||||
|
||||
return (
|
||||
<div className="settings-step-input-container">
|
||||
<input
|
||||
className={classnames('settings-step-input-element', classNames?.input)}
|
||||
value={value.toString()}
|
||||
onChange={onUserInput}
|
||||
type="number"
|
||||
pattern="[0-9]+"
|
||||
/>
|
||||
<div className="settings-step-input-buttons-container">
|
||||
<button
|
||||
className="settings-step-input-button settings-step-input-up-button"
|
||||
disabled={upDisabled}
|
||||
onClick={onStepUp}
|
||||
>
|
||||
▾
|
||||
</button>
|
||||
<button
|
||||
className="settings-step-input-button"
|
||||
disabled={downDisabled}
|
||||
onClick={onStepDown}
|
||||
>
|
||||
▾
|
||||
</button>
|
||||
</div>
|
||||
{unitOfMeasure && `${unitOfMeasure}`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsStepInput;
|
@@ -111,9 +111,11 @@ export class SettingsService {
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
const settings = await this.loadSettings();
|
||||
this._settings = deepClone(settings);
|
||||
this.ready.resolve();
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const settings = await this.loadSettings();
|
||||
this._settings = deepClone(settings);
|
||||
this.ready.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadSettings(): Promise<Settings> {
|
||||
@@ -139,7 +141,10 @@ export class SettingsService {
|
||||
this.preferenceService.get<number>(FONT_SIZE_SETTING, 12),
|
||||
this.preferenceService.get<string>(
|
||||
'workbench.colorTheme',
|
||||
'arduino-theme'
|
||||
window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'arduino-theme-dark'
|
||||
: 'arduino-theme'
|
||||
),
|
||||
this.preferenceService.get<Settings.AutoSave>(
|
||||
AUTO_SAVE_SETTING,
|
||||
|
@@ -65,7 +65,11 @@ export const UserFieldsComponent = ({
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
value={field.value}
|
||||
className="theia-input"
|
||||
placeholder={'Enter ' + field.label}
|
||||
placeholder={nls.localize(
|
||||
'arduino/userFields/enterField',
|
||||
'Enter {0}',
|
||||
field.label
|
||||
)}
|
||||
onChange={updateUserField(index)}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,37 +0,0 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
FrontendApplicationContribution,
|
||||
FrontendApplication,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MainMenuManager } from '../common/main-menu-manager';
|
||||
|
||||
@injectable()
|
||||
export class EditorMode implements FrontendApplicationContribution {
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
protected app: FrontendApplication;
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
const value = window.localStorage.getItem(EditorMode.COMPILE_FOR_DEBUG_KEY);
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
async toggleCompileForDebug(): Promise<void> {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(
|
||||
EditorMode.COMPILE_FOR_DEBUG_KEY,
|
||||
String(newState)
|
||||
);
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
export namespace EditorMode {
|
||||
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<svg width="25" height="20" viewBox="0 0 25 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.125 14C20.125 14.2321 20.0328 14.4546 19.8687 14.6187C19.7046 14.7828 19.4821 14.875 19.25 14.875H10.5C10.2679 14.875 10.0454 14.7828 9.88128 14.6187C9.71719 14.4546 9.625 14.2321 9.625 14C9.625 13.7679 9.71719 13.5454 9.88128 13.3813C10.0454 13.2172 10.2679 13.125 10.5 13.125H19.25C19.4821 13.125 19.7046 13.2172 19.8687 13.3813C20.0328 13.5454 20.125 13.7679 20.125 14Z" fill="black"/>
|
||||
<path d="M24.2462 6.37875L22.75 4.89125V0.875C22.75 0.642936 22.6578 0.420376 22.4937 0.256282C22.3296 0.0921874 22.1071 0 21.875 0H3.5C3.26794 0 3.04538 0.0921874 2.88128 0.256282C2.71719 0.420376 2.625 0.642936 2.625 0.875V3.5H0.875C0.642936 3.5 0.420376 3.59219 0.256282 3.75628C0.0921872 3.92038 0 4.14294 0 4.375V8.75C0 8.98206 0.0921872 9.20462 0.256282 9.36872C0.420376 9.53281 0.642936 9.625 0.875 9.625H2.625V18.375C2.625 18.6071 2.71719 18.8296 2.88128 18.9937C3.04538 19.1578 3.26794 19.25 3.5 19.25H21.875C21.9902 19.2507 22.1043 19.2286 22.2109 19.1851C22.3175 19.1415 22.4145 19.0773 22.4962 18.9962L24.2462 17.2462C24.3273 17.1645 24.3915 17.0675 24.4351 16.9609C24.4786 16.8543 24.5007 16.7402 24.5 16.625V7C24.5007 6.88484 24.4786 6.77069 24.4351 6.66408C24.3915 6.55747 24.3273 6.46051 24.2462 6.37875ZM1.75 7.875V5.25H6.125V7.875H1.75ZM22.75 16.2663L21.5163 17.5H4.375V9.625H7C7.23206 9.625 7.45462 9.53281 7.61872 9.36872C7.78281 9.20462 7.875 8.98206 7.875 8.75V4.375C7.875 4.14294 7.78281 3.92038 7.61872 3.75628C7.45462 3.59219 7.23206 3.5 7 3.5H4.375V1.75H21V5.25C20.9993 5.36516 21.0214 5.47931 21.0649 5.58592C21.1085 5.69253 21.1727 5.78949 21.2538 5.87125L22.75 7.35875V16.2663Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
3
arduino-ide-extension/src/browser/icons/monitor.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1858 13.92C8.73525 13.92 7.38744 13.4748 6.27173 12.7138L4.24581 14.7397C3.89434 15.0912 3.3245 15.0912 2.97303 14.7397C2.62156 14.3882 2.62156 13.8184 2.97303 13.4669L4.92544 11.5145C3.86685 10.2933 3.2258 8.70059 3.2258 6.95998C3.2258 3.11999 6.3458 0 10.1858 0C14.0258 0 17.1458 3.11999 17.1458 6.95998C17.1458 10.8 14.0258 13.92 10.1858 13.92ZM10.1858 2.4C7.6658 2.4 5.6258 4.43999 5.6258 6.95998C5.6258 9.47998 7.6658 11.52 10.1858 11.52C12.6938 11.52 14.7458 9.47998 14.7458 6.95998C14.7458 4.45199 12.7058 2.4 10.1858 2.4ZM17.9619 7.47017C17.9619 8.02365 18.4106 8.47233 18.9641 8.47233C19.5176 8.47233 19.9662 8.02365 19.9662 7.47017C19.9662 6.91669 19.5176 6.468 18.9641 6.468C18.4106 6.468 17.9619 6.91669 17.9619 7.47017ZM1.001 8.47246C0.448162 8.47246 0 8.0243 0 7.47147C0 6.91863 0.448162 6.47047 1.001 6.47047C1.55383 6.47047 2.002 6.91863 2.002 7.47147C2.002 8.0243 1.55383 8.47246 1.001 8.47246ZM22.7923 8.47233C22.2389 8.47233 21.7902 8.02365 21.7902 7.47017C21.7902 6.91669 22.2389 6.468 22.7923 6.468C23.3458 6.468 23.7945 6.91669 23.7945 7.47017C23.7945 8.02365 23.3458 8.47233 22.7923 8.47233ZM10.1978 8.23198C9.49529 8.23198 8.9258 7.66249 8.9258 6.95999C8.9258 6.25748 9.49529 5.68799 10.1978 5.68799C10.9003 5.68799 11.4698 6.25748 11.4698 6.95999C11.4698 7.66249 10.9003 8.23198 10.1978 8.23198Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
arduino-ide-extension/src/browser/icons/plotter.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="19" viewBox="0 0 16 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 18.0019C5.62402 18.0019 5.27637 17.79 5.10547 17.4492L2.38184 12.0019H1C0.44727 12.0019 0 11.5546 0 11.0019C0 10.4492 0.44727 10.0019 1 10.0019H3C3.37891 10.0019 3.72461 10.2158 3.89453 10.5546L5.68555 14.1367L9.03028 0.759723C9.13868 0.322223 9.52637 0.0126525 9.97657 0.00191246C10.4209 -0.0273875 10.8281 0.283163 10.958 0.714803L13.7442 10.0019H15C15.5527 10.0019 16 10.4492 16 11.0019C16 11.5546 15.5527 12.0019 15 12.0019H13C12.5586 12.0019 12.169 11.7119 12.042 11.289L10.0879 4.77437L6.96974 17.2441C6.86818 17.6504 6.5254 17.9502 6.10939 17.9961C6.07228 18 6.03615 18.0019 6.00001 18.0019H6Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 732 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="13" height="3" viewBox="0 0 13 3" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.1875 2.25C6.80882 2.25 7.3125 1.74632 7.3125 1.125C7.3125 0.50368 6.80882 0 6.1875 0C5.56618 0 5.0625 0.50368 5.0625 1.125C5.0625 1.74632 5.56618 2.25 6.1875 2.25Z" fill="black"/>
|
||||
<path d="M11.25 2.25C11.8713 2.25 12.375 1.74632 12.375 1.125C12.375 0.50368 11.8713 0 11.25 0C10.6287 0 10.125 0.50368 10.125 1.125C10.125 1.74632 10.6287 2.25 11.25 2.25Z" fill="black"/>
|
||||
<path d="M1.125 2.25C1.74632 2.25 2.25 1.74632 2.25 1.125C2.25 0.50368 1.74632 0 1.125 0C0.50368 0 0 0.50368 0 1.125C0 1.74632 0.50368 2.25 1.125 2.25Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 650 B |
3
arduino-ide-extension/src/browser/icons/upload.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="15" viewBox="0 0 17 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.71 7.71253L9.70996 13.7125C9.51001 13.9025 9.26001 14.0026 9 14.0026C8.73999 14.0026 8.48999 13.9025 8.29004 13.7125C7.90003 13.3226 7.90003 12.6825 8.29004 12.2926L12.5901 8.00257H1C0.44995 8.00257 0 7.55261 0 7.00256C0 6.45251 0.44995 6.00256 1 6.00256H12.5901L8.29004 1.71253C7.90003 1.32264 7.90003 0.682495 8.29004 0.292605C8.67993 -0.097535 9.32007 -0.097535 9.70996 0.292605L15.71 6.2926C16.1 6.68249 16.1 7.32264 15.71 7.71253V7.71253Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 576 B |
3
arduino-ide-extension/src/browser/icons/verify.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 11C5.74414 11 5.48827 10.9023 5.29296 10.707L0.292965 5.70702C-0.097655 5.3164 -0.097655 4.68358 0.292965 4.29296C0.683585 3.90234 1.31641 3.90234 1.70703 4.29296L6 8.58593L14.293 0.292965C14.6836 -0.097655 15.3164 -0.097655 15.707 0.292965C16.0976 0.683585 16.0976 1.31641 15.707 1.70703L6.70703 10.707C6.51172 10.9023 6.25586 11 6 11Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 468 B |
@@ -5,36 +5,43 @@ import { IDEUpdaterClient } from '../../common/protocol/ide-updater';
|
||||
|
||||
@injectable()
|
||||
export class IDEUpdaterClientImpl implements IDEUpdaterClient {
|
||||
protected readonly onErrorEmitter = new Emitter<Error>();
|
||||
protected readonly onCheckingForUpdateEmitter = new Emitter<void>();
|
||||
protected readonly onUpdateAvailableEmitter = new Emitter<UpdateInfo>();
|
||||
protected readonly onUpdateNotAvailableEmitter = new Emitter<UpdateInfo>();
|
||||
protected readonly onDownloadProgressEmitter = new Emitter<ProgressInfo>();
|
||||
protected readonly onDownloadFinishedEmitter = new Emitter<UpdateInfo>();
|
||||
protected readonly onUpdaterDidFailEmitter = new Emitter<Error>();
|
||||
protected readonly onUpdaterDidCheckForUpdateEmitter = new Emitter<void>();
|
||||
protected readonly onUpdaterDidFindUpdateAvailableEmitter =
|
||||
new Emitter<UpdateInfo>();
|
||||
protected readonly onUpdaterDidNotFindUpdateAvailableEmitter =
|
||||
new Emitter<UpdateInfo>();
|
||||
protected readonly onDownloadProgressDidChangeEmitter =
|
||||
new Emitter<ProgressInfo>();
|
||||
protected readonly onDownloadDidFinishEmitter = new Emitter<UpdateInfo>();
|
||||
|
||||
readonly onError = this.onErrorEmitter.event;
|
||||
readonly onCheckingForUpdate = this.onCheckingForUpdateEmitter.event;
|
||||
readonly onUpdateAvailable = this.onUpdateAvailableEmitter.event;
|
||||
readonly onUpdateNotAvailable = this.onUpdateNotAvailableEmitter.event;
|
||||
readonly onDownloadProgressChanged = this.onDownloadProgressEmitter.event;
|
||||
readonly onDownloadFinished = this.onDownloadFinishedEmitter.event;
|
||||
readonly onUpdaterDidFail = this.onUpdaterDidFailEmitter.event;
|
||||
readonly onUpdaterDidCheckForUpdate =
|
||||
this.onUpdaterDidCheckForUpdateEmitter.event;
|
||||
readonly onUpdaterDidFindUpdateAvailable =
|
||||
this.onUpdaterDidFindUpdateAvailableEmitter.event;
|
||||
readonly onUpdaterDidNotFindUpdateAvailable =
|
||||
this.onUpdaterDidNotFindUpdateAvailableEmitter.event;
|
||||
readonly onDownloadProgressDidChange =
|
||||
this.onDownloadProgressDidChangeEmitter.event;
|
||||
readonly onDownloadDidFinish = this.onDownloadDidFinishEmitter.event;
|
||||
|
||||
notifyError(message: Error): void {
|
||||
this.onErrorEmitter.fire(message);
|
||||
notifyUpdaterFailed(message: Error): void {
|
||||
this.onUpdaterDidFailEmitter.fire(message);
|
||||
}
|
||||
notifyCheckingForUpdate(message: void): void {
|
||||
this.onCheckingForUpdateEmitter.fire(message);
|
||||
notifyCheckedForUpdate(message: void): void {
|
||||
this.onUpdaterDidCheckForUpdateEmitter.fire(message);
|
||||
}
|
||||
notifyUpdateAvailable(message: UpdateInfo): void {
|
||||
this.onUpdateAvailableEmitter.fire(message);
|
||||
notifyUpdateAvailableFound(message: UpdateInfo): void {
|
||||
this.onUpdaterDidFindUpdateAvailableEmitter.fire(message);
|
||||
}
|
||||
notifyUpdateNotAvailable(message: UpdateInfo): void {
|
||||
this.onUpdateNotAvailableEmitter.fire(message);
|
||||
notifyUpdateAvailableNotFound(message: UpdateInfo): void {
|
||||
this.onUpdaterDidNotFindUpdateAvailableEmitter.fire(message);
|
||||
}
|
||||
notifyDownloadProgressChanged(message: ProgressInfo): void {
|
||||
this.onDownloadProgressEmitter.fire(message);
|
||||
this.onDownloadProgressDidChangeEmitter.fire(message);
|
||||
}
|
||||
notifyDownloadFinished(message: UpdateInfo): void {
|
||||
this.onDownloadFinishedEmitter.fire(message);
|
||||
this.onDownloadDidFinishEmitter.fire(message);
|
||||
}
|
||||
}
|
||||
|
@@ -52,9 +52,12 @@ export class IDEUpdaterCommands implements CommandContribution {
|
||||
}
|
||||
}
|
||||
export namespace IDEUpdaterCommands {
|
||||
export const CHECK_FOR_UPDATES: Command = {
|
||||
id: 'arduino-ide-check-for-updates',
|
||||
category: 'Arduino',
|
||||
label: 'Check for Arduino IDE updates',
|
||||
};
|
||||
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-check-for-ide-updates',
|
||||
label: 'Check for Arduino IDE Updates',
|
||||
category: 'Arduino',
|
||||
},
|
||||
'arduino/ide-updater/checkForUpdates'
|
||||
);
|
||||
}
|
||||
|
@@ -1,19 +1,28 @@
|
||||
import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
injectable,
|
||||
postConstruct,
|
||||
inject,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import {
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
LibraryService,
|
||||
} from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
export class LibraryListWidget extends ListWidget<
|
||||
LibraryPackage,
|
||||
LibrarySearch
|
||||
> {
|
||||
static WIDGET_ID = 'library-list-widget';
|
||||
static WIDGET_LABEL = nls.localize(
|
||||
'arduino/library/title',
|
||||
@@ -21,9 +30,9 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
);
|
||||
|
||||
constructor(
|
||||
@inject(LibraryService) protected service: LibraryService,
|
||||
@inject(ListItemRenderer)
|
||||
protected itemRenderer: ListItemRenderer<LibraryPackage>
|
||||
@inject(LibraryService) private service: LibraryService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
|
||||
@inject(LibraryFilterRenderer) filterRenderer: LibraryFilterRenderer
|
||||
) {
|
||||
super({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
@@ -34,6 +43,8 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
itemLabel: (item: LibraryPackage) => item.name,
|
||||
itemDeprecated: (item: LibraryPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,8 +52,10 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onLibraryUninstalled(() =>
|
||||
this.notificationCenter.onLibraryDidInstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
]);
|
||||
|
@@ -145,7 +145,10 @@ export class MonitorManagerProxyClientImpl
|
||||
if (
|
||||
selectedBoard?.fqbn !==
|
||||
this.lastConnectedBoard?.selectedBoard?.fqbn ||
|
||||
selectedPort?.id !== this.lastConnectedBoard?.selectedPort?.id
|
||||
Port.keyOf(selectedPort) !==
|
||||
(this.lastConnectedBoard.selectedPort
|
||||
? Port.keyOf(this.lastConnectedBoard.selectedPort)
|
||||
: undefined)
|
||||
) {
|
||||
this.onMonitorShouldResetEmitter.fire(null);
|
||||
this.lastConnectedBoard = {
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
LibraryPackage,
|
||||
Config,
|
||||
Sketch,
|
||||
ProgressMessage,
|
||||
} from '../common/protocol';
|
||||
import {
|
||||
FrontendApplicationStateService,
|
||||
@@ -33,25 +34,32 @@ export class NotificationCenter
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected readonly indexUpdatedEmitter = new Emitter<void>();
|
||||
protected readonly daemonStartedEmitter = new Emitter<string>();
|
||||
protected readonly daemonStoppedEmitter = new Emitter<void>();
|
||||
protected readonly configChangedEmitter = new Emitter<{
|
||||
protected readonly indexDidUpdateEmitter = new Emitter<string>();
|
||||
protected readonly indexWillUpdateEmitter = new Emitter<string>();
|
||||
protected readonly indexUpdateDidProgressEmitter =
|
||||
new Emitter<ProgressMessage>();
|
||||
protected readonly indexUpdateDidFailEmitter = new Emitter<{
|
||||
progressId: string;
|
||||
message: string;
|
||||
}>();
|
||||
protected readonly daemonDidStartEmitter = new Emitter<string>();
|
||||
protected readonly daemonDidStopEmitter = new Emitter<void>();
|
||||
protected readonly configDidChangeEmitter = new Emitter<{
|
||||
config: Config | undefined;
|
||||
}>();
|
||||
protected readonly platformInstalledEmitter = new Emitter<{
|
||||
protected readonly platformDidInstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
protected readonly platformUninstalledEmitter = new Emitter<{
|
||||
protected readonly platformDidUninstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
protected readonly libraryInstalledEmitter = new Emitter<{
|
||||
protected readonly libraryDidInstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
}>();
|
||||
protected readonly libraryUninstalledEmitter = new Emitter<{
|
||||
protected readonly libraryDidUninstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
}>();
|
||||
protected readonly attachedBoardsChangedEmitter =
|
||||
protected readonly attachedBoardsDidChangeEmitter =
|
||||
new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly recentSketchesChangedEmitter = new Emitter<{
|
||||
sketches: Sketch[];
|
||||
@@ -60,27 +68,34 @@ export class NotificationCenter
|
||||
new Emitter<FrontendApplicationState>();
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.indexUpdatedEmitter,
|
||||
this.daemonStartedEmitter,
|
||||
this.daemonStoppedEmitter,
|
||||
this.configChangedEmitter,
|
||||
this.platformInstalledEmitter,
|
||||
this.platformUninstalledEmitter,
|
||||
this.libraryInstalledEmitter,
|
||||
this.libraryUninstalledEmitter,
|
||||
this.attachedBoardsChangedEmitter
|
||||
this.indexWillUpdateEmitter,
|
||||
this.indexUpdateDidProgressEmitter,
|
||||
this.indexDidUpdateEmitter,
|
||||
this.indexUpdateDidFailEmitter,
|
||||
this.daemonDidStartEmitter,
|
||||
this.daemonDidStopEmitter,
|
||||
this.configDidChangeEmitter,
|
||||
this.platformDidInstallEmitter,
|
||||
this.platformDidUninstallEmitter,
|
||||
this.libraryDidInstallEmitter,
|
||||
this.libraryDidUninstallEmitter,
|
||||
this.attachedBoardsDidChangeEmitter
|
||||
);
|
||||
|
||||
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;
|
||||
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
|
||||
readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
|
||||
readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
|
||||
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
|
||||
readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event;
|
||||
readonly onDaemonDidStart = this.daemonDidStartEmitter.event;
|
||||
readonly onDaemonDidStop = this.daemonDidStopEmitter.event;
|
||||
readonly onConfigDidChange = this.configDidChangeEmitter.event;
|
||||
readonly onPlatformDidInstall = this.platformDidInstallEmitter.event;
|
||||
readonly onPlatformDidUninstall = this.platformDidUninstallEmitter.event;
|
||||
readonly onLibraryDidInstall = this.libraryDidInstallEmitter.event;
|
||||
readonly onLibraryDidUninstall = this.libraryDidUninstallEmitter.event;
|
||||
readonly onAttachedBoardsDidChange =
|
||||
this.attachedBoardsDidChangeEmitter.event;
|
||||
readonly onRecentSketchesDidChange = this.recentSketchesChangedEmitter.event;
|
||||
readonly onAppStateDidChange = this.onAppStateDidChangeEmitter.event;
|
||||
|
||||
@postConstruct()
|
||||
@@ -97,43 +112,61 @@ export class NotificationCenter
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
notifyIndexUpdated(): void {
|
||||
this.indexUpdatedEmitter.fire();
|
||||
notifyIndexWillUpdate(progressId: string): void {
|
||||
this.indexWillUpdateEmitter.fire(progressId);
|
||||
}
|
||||
|
||||
notifyDaemonStarted(port: string): void {
|
||||
this.daemonStartedEmitter.fire(port);
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
|
||||
this.indexUpdateDidProgressEmitter.fire(progressMessage);
|
||||
}
|
||||
|
||||
notifyDaemonStopped(): void {
|
||||
this.daemonStoppedEmitter.fire();
|
||||
notifyIndexDidUpdate(progressId: string): void {
|
||||
this.indexDidUpdateEmitter.fire(progressId);
|
||||
}
|
||||
|
||||
notifyConfigChanged(event: { config: Config | undefined }): void {
|
||||
this.configChangedEmitter.fire(event);
|
||||
notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}): void {
|
||||
this.indexUpdateDidFailEmitter.fire({ progressId, message });
|
||||
}
|
||||
|
||||
notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||
this.platformInstalledEmitter.fire(event);
|
||||
notifyDaemonDidStart(port: string): void {
|
||||
this.daemonDidStartEmitter.fire(port);
|
||||
}
|
||||
|
||||
notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
|
||||
this.platformUninstalledEmitter.fire(event);
|
||||
notifyDaemonDidStop(): void {
|
||||
this.daemonDidStopEmitter.fire();
|
||||
}
|
||||
|
||||
notifyLibraryInstalled(event: { item: LibraryPackage }): void {
|
||||
this.libraryInstalledEmitter.fire(event);
|
||||
notifyConfigDidChange(event: { config: Config | undefined }): void {
|
||||
this.configDidChangeEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
|
||||
this.libraryUninstalledEmitter.fire(event);
|
||||
notifyPlatformDidInstall(event: { item: BoardsPackage }): void {
|
||||
this.platformDidInstallEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
this.attachedBoardsChangedEmitter.fire(event);
|
||||
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void {
|
||||
this.platformDidUninstallEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
|
||||
notifyLibraryDidInstall(event: { item: LibraryPackage }): void {
|
||||
this.libraryDidInstallEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void {
|
||||
this.libraryDidUninstallEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void {
|
||||
this.attachedBoardsDidChangeEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void {
|
||||
this.recentSketchesChangedEmitter.fire(event);
|
||||
}
|
||||
}
|
||||
|
@@ -7,11 +7,11 @@ import {
|
||||
import {
|
||||
OutputMessage,
|
||||
ProgressMessage,
|
||||
ResponseServiceArduino,
|
||||
ResponseServiceClient,
|
||||
} from '../common/protocol/response-service';
|
||||
|
||||
@injectable()
|
||||
export class ResponseServiceImpl implements ResponseServiceArduino {
|
||||
export class ResponseServiceImpl implements ResponseServiceClient {
|
||||
@inject(OutputChannelManager)
|
||||
private readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ResponseServiceImpl implements ResponseServiceArduino {
|
||||
|
||||
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
|
||||
|
||||
clearArduinoChannel(): void {
|
||||
clearOutput(): void {
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||
/**
|
||||
* Exclusive "ino" document selector for monaco.
|
||||
*/
|
||||
@@ -11,3 +12,11 @@ function selectorOf(
|
||||
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for the `monaco` resource in the Arduino _Output_ channel.
|
||||
*/
|
||||
export const ArduinoOutputSelector: monaco.languages.LanguageSelector = {
|
||||
scheme: OutputUri.SCHEME,
|
||||
pattern: '**/Arduino',
|
||||
};
|
@@ -1,10 +1,10 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
||||
import { Board } from '../../../common/protocol/boards-service';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||
import { MonitorModel } from '../../monitor-model';
|
||||
import { Unknown } from '../../../common/nls';
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
@@ -80,14 +80,13 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
|
||||
return nls.localize(
|
||||
'arduino/serial/message',
|
||||
"Message ({0} + Enter to send message to '{1}' on '{2}')",
|
||||
isOSX ? '⌘' : nls.localize('vscode/keybindingLabels/ctrlKey', 'Ctrl'),
|
||||
"Message (Enter to send message to '{0}' on '{1}')",
|
||||
board
|
||||
? Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})
|
||||
: 'unknown',
|
||||
port ? port.address : 'unknown'
|
||||
: Unknown,
|
||||
port ? port.address : Unknown
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,8 +108,8 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
|
||||
if (keyCode) {
|
||||
const { key, meta, ctrl } = keyCode;
|
||||
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
|
||||
const { key } = keyCode;
|
||||
if (key === Key.ENTER) {
|
||||
this.onSend();
|
||||
}
|
||||
}
|
||||
|
@@ -109,7 +109,7 @@ const _Row = ({
|
||||
}) => {
|
||||
const timestamp =
|
||||
(data.timestamp &&
|
||||
`${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) ||
|
||||
`${dateFormat(data.lines[index].timestamp, 'HH:MM:ss.l')} -> `) ||
|
||||
'';
|
||||
return (
|
||||
(data.lines[index].lineLen && (
|
||||
|
@@ -13,20 +13,30 @@ import { ipcRenderer } from '@theia/electron/shared/electron';
|
||||
import { MonitorManagerProxyClient } from '../../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||
import { MonitorModel } from '../../monitor-model';
|
||||
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
|
||||
import {
|
||||
CLOSE_PLOTTER_WINDOW,
|
||||
SHOW_PLOTTER_WINDOW,
|
||||
} from '../../../common/ipc-communication';
|
||||
|
||||
const queryString = require('query-string');
|
||||
|
||||
export namespace SerialPlotterContribution {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'serial-plotter-open',
|
||||
label: 'Serial Plotter',
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const OPEN: Command = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'serial-plotter-open',
|
||||
label: 'Serial Plotter',
|
||||
category: 'Arduino',
|
||||
},
|
||||
'arduino/serial/openSerialPlotter'
|
||||
);
|
||||
|
||||
export const RESET: Command = {
|
||||
id: 'serial-plotter-reset',
|
||||
label: 'Reset Serial Plotter',
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const OPEN_TOOLBAR: Command = {
|
||||
id: 'serial-plotter-open-toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -52,7 +62,7 @@ export class PlotterFrontendContribution extends Contribution {
|
||||
override onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString();
|
||||
|
||||
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
|
||||
ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => {
|
||||
if (!!this.window) {
|
||||
this.window = null;
|
||||
}
|
||||
@@ -69,6 +79,14 @@ export class PlotterFrontendContribution extends Contribution {
|
||||
registry.registerCommand(SerialPlotterContribution.Commands.RESET, {
|
||||
execute: () => this.reset(),
|
||||
});
|
||||
registry.registerCommand(
|
||||
{ id: SerialPlotterContribution.Commands.OPEN_TOOLBAR.id },
|
||||
{
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'right',
|
||||
execute: this.startPlotter.bind(this),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
@@ -82,7 +100,7 @@ export class PlotterFrontendContribution extends Contribution {
|
||||
async startPlotter(): Promise<void> {
|
||||
await this.monitorManagerProxy.startMonitor();
|
||||
if (!!this.window) {
|
||||
this.window.focus();
|
||||
ipcRenderer.send(SHOW_PLOTTER_WINDOW);
|
||||
return;
|
||||
}
|
||||
const wsPort = this.monitorManagerProxy.getWebSocketPort();
|
||||
|
@@ -49,3 +49,14 @@
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
.hc-black.hc-theia.theia-hc .arduino-select__option--is-selected {
|
||||
outline: 1px solid var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.hc-black.hc-theia.theia-hc .arduino-select__option--is-focused {
|
||||
outline: 1px dashed var(--theia-focusBorder);
|
||||
}
|
||||
|
@@ -2,9 +2,11 @@ div#select-board-dialog {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer .body {
|
||||
div#select-board-dialog .selectBoardContainer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.select-board-dialog .head {
|
||||
@@ -15,15 +17,16 @@ div.dialogContent.select-board-dialog > div.head .title {
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 1.2em;
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
color: var(--theia-editorWidget-foreground);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer .body .list .item.selected {
|
||||
|
||||
div#select-board-dialog .selectBoardContainer .list .item.selected {
|
||||
background: var(--theia-secondaryButton-hoverBackground);
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
}
|
||||
|
||||
@@ -34,7 +37,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
background: var(--theia-editor-background);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .search input {
|
||||
#select-board-dialog .selectBoardContainer .search input {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
@@ -43,61 +46,66 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
display: flex;
|
||||
color: var(--theia-editor-foreground);
|
||||
color: var(--theia-input-foreground);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .search input:focus {
|
||||
#select-board-dialog .selectBoardContainer .search input:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .container {
|
||||
#select-board-dialog .selectBoardContainer .container {
|
||||
flex: 1;
|
||||
padding: 0px 10px 0px 0px;
|
||||
min-width: 240px;
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .left.container .content {
|
||||
#select-board-dialog .selectBoardContainer .container .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .left.container .content {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .right.container .content {
|
||||
#select-board-dialog .selectBoardContainer .right.container .content {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .container .content .title {
|
||||
color: #7f8c8d;
|
||||
#select-board-dialog .selectBoardContainer .container .content .title {
|
||||
color: var(--theia-editorWidget-foreground);
|
||||
padding: 0px 0px 10px 0px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .container .content .footer {
|
||||
#select-board-dialog .selectBoardContainer .container .content .footer {
|
||||
padding: 10px 5px 10px 0px;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .container .content .loading {
|
||||
#select-board-dialog .selectBoardContainer .container .content .loading {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
color: var(--theia-arduino-branding-secondary);
|
||||
color: var(--theia-editorWidget-foreground);
|
||||
padding: 10px 5px 10px 10px;
|
||||
text-transform: uppercase;
|
||||
/* The max, min-height comes from `.body .list` 200px + 47px top padding - 2 * 10px top padding */
|
||||
/* The max, min-height comes from `.list` 200px + 47px top padding - 2 * 10px top padding */
|
||||
max-height: 227px;
|
||||
min-height: 227px;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item {
|
||||
#select-board-dialog .selectBoardContainer .list .item {
|
||||
padding: 10px 5px 10px 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .selected-icon {
|
||||
#select-board-dialog .selectBoardContainer .list .item .selected-icon {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .details {
|
||||
#select-board-dialog .selectBoardContainer .list .item .details {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
width: 155px; /* used heuristics for the calculation */
|
||||
@@ -106,132 +114,171 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item.missing {
|
||||
#select-board-dialog .selectBoardContainer .list .item.missing {
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item:hover {
|
||||
#select-board-dialog .selectBoardContainer .list .item:hover {
|
||||
background: var(--theia-secondaryButton-hoverBackground);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .label {
|
||||
max-width: 215px;
|
||||
width: 215px;
|
||||
#select-board-dialog .selectBoardContainer .list .label {
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list {
|
||||
#select-board-dialog .selectBoardContainer .list {
|
||||
max-height: 200px;
|
||||
min-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .ports.list {
|
||||
#select-board-dialog .selectBoardContainer .ports.list {
|
||||
margin: 47px 0px 0px 0px; /* 47 is 37 as input height for the `Boards`, plus 10 margin bottom. */
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .search {
|
||||
#select-board-dialog .selectBoardContainer .search {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogContent.select-board-dialog {
|
||||
width: 500px;
|
||||
}
|
||||
.arduino-boards-toolbar-item-container {
|
||||
margin-left: 3px;
|
||||
align-items: center;
|
||||
background: var(--theia-arduino-toolbar-dropdown-background);
|
||||
border-radius: 1px;
|
||||
color: var(--theia-arduino-toolbar-dropdown-label);
|
||||
border: 1px solid var(--theia-arduino-toolbar-dropdown-border);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
height: 28px;
|
||||
margin: 0 4px;
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item--protocol,
|
||||
.arduino-boards-dropdown-item--protocol {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item--protocol ,
|
||||
.arduino-boards-dropdown-item--protocol {
|
||||
color: var(--theia-arduino-toolbar-dropdown-label);
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container
|
||||
.arduino-boards-toolbar-item
|
||||
.inner-container {
|
||||
.arduino-boards-toolbar-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container
|
||||
.arduino-boards-toolbar-item
|
||||
.inner-container
|
||||
.notAttached {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: red;
|
||||
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;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item .label {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 5px;
|
||||
.arduino-boards-toolbar-item--label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item .caret {
|
||||
.arduino-boards-toolbar-item--label-connected {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container .caret {
|
||||
width: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item {
|
||||
background: var(--theia-tab-unfocusedActiveBackground);
|
||||
color: var(--theia-foreground);
|
||||
height: 22px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0px 3px 0px 3px;
|
||||
border: 1px solid var(--theia-dropdown-border);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-list {
|
||||
border: 3px solid var(--theia-activityBar-background);
|
||||
margin: -1px;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--theia-dropdown-border);
|
||||
border: 1px solid var(--theia-arduino-toolbar-dropdown-border);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-list:focus {
|
||||
border: 1px solid var(--theia-arduino-toolbar-dropdown-borderActive);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-list--items-container {
|
||||
overflow: auto;
|
||||
max-height: 404px;
|
||||
background: var(--theia-arduino-toolbar-dropdown-background);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-list--items-container::-webkit-scrollbar {
|
||||
background: var(--theia-arduino-toolbar-dropdown-background);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
background: var(--theia-arduino-toolbar-dropdown-background);
|
||||
color: var(--theia-arduino-toolbar-dropdown-label);
|
||||
cursor: default;
|
||||
display: flex;
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: var(--theia-foreground);
|
||||
background: var(--theia-tab-unfocusedActiveBackground);
|
||||
border: 1px solid var(--theia-tab-unfocusedActiveBackground);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item--label {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item--board-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item--port-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item:hover {
|
||||
background: var(--theia-arduino-toolbar-dropdown-option-backgroundHover);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item--selected,
|
||||
.arduino-boards-dropdown-item--selected:hover {
|
||||
background: var(--theia-arduino-toolbar-dropdown-option-backgroundSelected);
|
||||
border: 1px solid var(--theia-arduino-toolbar-dropdown-option-backgroundSelected);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item--selected
|
||||
.arduino-boards-dropdown-item--port-label {
|
||||
color: var(--theia-arduino-toolbar-dropdown-label);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item--selected .fa {
|
||||
color: var(--theia-arduino-toolbar-dropdown-iconSelected);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item .fa-check {
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item.selected,
|
||||
.arduino-boards-dropdown-item:hover {
|
||||
border: 1px solid var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.arduino-board-dropdown-footer {
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
color: var(--theia-secondaryButton-foreground);
|
||||
border-top: 1px solid var(--theia-dropdown-border);
|
||||
}
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
.hc-black.hc-theia.theia-hc #select-board-dialog .selectBoardContainer .list .item:hover {
|
||||
outline: 1px dashed var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.hc-black.hc-theia.theia-hc div#select-board-dialog .selectBoardContainer .list .item.selected {
|
||||
outline: 1px solid var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 400px) {
|
||||
div.dialogContent.select-board-dialog > div.head {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .container .content .title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
.certificate-uploader-dialog {
|
||||
#certificate-uploader-dialog-container > .dialogBlock {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
.certificate-uploader-dialog .arduino-select__control {
|
||||
height: 31px;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
background: var(--theia-dropdown-background) !important;
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .dialogRow > button{
|
||||
@@ -15,9 +15,10 @@
|
||||
}
|
||||
|
||||
.certificate-uploader-dialog .certificate-list {
|
||||
border: 1px solid #BDC7C7;
|
||||
border: 1px solid var(--theia-editorWidget-border);
|
||||
border-radius: 2px;;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
color: var(--theia-editor-foreground);
|
||||
background-color: var(--theia-editor-background);
|
||||
overflow: auto;
|
||||
height: 120px;
|
||||
flex: 1;
|
||||
@@ -60,9 +61,10 @@
|
||||
|
||||
.certificate-add {
|
||||
padding: 16px;
|
||||
background-color: var(--theia-list-hoverBackground);
|
||||
border-radius: 3px;
|
||||
border: 1px solid #BDC7C7;
|
||||
border: 1px solid var(--theia-editorWidget-border);
|
||||
color: var(--theia-editorWidget-foreground);
|
||||
background-color: var(--theia-editorWidget-background);
|
||||
}
|
||||
|
||||
.certificate-add input {
|
||||
@@ -71,4 +73,4 @@
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.27 20H2.58C1.80648 17.3894 1.80648 14.6106 2.58 12H7.27C7.08644 13.3254 6.99622 14.662 7 16C6.99622 17.338 7.08644 18.6746 7.27 20Z" fill="black"/>
|
||||
<path d="M7.63999 22C8.12954 24.499 9.15286 26.8629 10.64 28.93C7.43807 27.5988 4.84154 25.1305 3.34999 22H7.63999Z" fill="black"/>
|
||||
<path d="M7.63999 9.99982H3.34999C4.84154 6.86936 7.43807 4.40106 10.64 3.06982C9.15286 5.13697 8.12954 7.50082 7.63999 9.99982Z" fill="black"/>
|
||||
<path d="M15 2.1499V9.9999H9.7C10.71 5.8999 12.69 2.8399 15 2.1499Z" fill="black"/>
|
||||
<path d="M9.29999 12H15V20H9.29999C8.90004 17.3483 8.90004 14.6517 9.29999 12Z" fill="black"/>
|
||||
<path d="M9.7 22H15V29.85C12.69 29.16 10.71 26.1 9.7 22Z" fill="black"/>
|
||||
<path d="M22.3 22C21.3 26.1 19.3 29.16 17 29.85V22H22.3Z" fill="black"/>
|
||||
<path d="M22.3 9.9999H17V2.1499C19.31 2.8399 21.29 5.8999 22.3 9.9999Z" fill="black"/>
|
||||
<path d="M22.7 20H17V12H22.7C22.8985 13.3241 22.9987 14.6611 23 16C22.9987 17.3389 22.8985 18.6759 22.7 20Z" fill="black"/>
|
||||
<path d="M24.36 22H28.65C27.1584 25.1305 24.5619 27.5988 21.36 28.93C22.8471 26.8629 23.8704 24.499 24.36 22Z" fill="black"/>
|
||||
<path d="M24.36 9.99982C23.8704 7.50082 22.8471 5.13697 21.36 3.06982C24.5619 4.40106 27.1584 6.86936 28.65 9.99982H24.36Z" fill="black"/>
|
||||
<path d="M30 16C30.0023 17.3545 29.8069 18.702 29.42 20H24.73C24.9136 18.6746 25.0038 17.338 25 16C25.0038 14.662 24.9136 13.3254 24.73 12H29.42C29.8069 13.298 30.0023 14.6455 30 16Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |