Compare commits

..

1 Commits

Author SHA1 Message Date
Francesco Spissu
f84c15bea0 close websocket connection after closing serial plotter 2022-11-21 11:53:56 +01:00
473 changed files with 26715 additions and 37499 deletions

View File

@@ -15,10 +15,8 @@ module.exports = {
'.browser_modules/*',
'docs/*',
'scripts/*',
'electron-app/lib/*',
'electron-app/src-gen/*',
'electron-app/gen-webpack*.js',
'!electron-app/webpack.config.js',
'electron/*',
'electron-app/*',
'plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
],

View File

@@ -7,9 +7,6 @@
- name: "topic: CLI"
color: "00ffff"
description: Related to Arduino CLI
- name: "topic: cloud"
color: "00ffff"
description: Related to Arduino Cloud and cloud sketches
- name: "topic: debugger"
color: "00ffff"
description: Related to the integrated debugger

View File

@@ -1,11 +1,9 @@
name: Arduino IDE
on:
create:
push:
branches:
- main
- '[0-9]+.[0-9]+.x'
paths-ignore:
- '.github/**'
- '!.github/workflows/build.yml'
@@ -31,39 +29,13 @@ on:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: "1.17"
JOB_TRANSFER_ARTIFACT: build-artifacts
CHANGELOG_ARTIFACTS: changelog
jobs:
run-determination:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.determination.outputs.result }}
permissions: {}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
build:
name: build (${{ matrix.config.os }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
strategy:
matrix:
config:
@@ -71,7 +43,7 @@ jobs:
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-20.04
- 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
@@ -85,12 +57,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js 16.14
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '16.14'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Python 3.x
uses: actions/setup-python@v4
@@ -98,7 +69,7 @@ jobs:
python-version: '3.x'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@@ -128,30 +99,20 @@ jobs:
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] }}"
export CSC_FOR_PULL_REQUEST=true
fi
if [ "${{ runner.OS }}" = "Windows" ]; then
npm config set msvs_version 2017 --global
fi
npx node-gyp install
yarn install --immutable
yarn --cwd arduino-ide-extension build
yarn test
yarn --cwd arduino-ide-extension test:slow
yarn --cwd arduino-ide-extension lint
yarn --cwd electron-app rebuild
yarn --cwd electron-app build
yarn --cwd electron-app package
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: electron-app/dist/build-artifacts
path: electron/build/dist/build-artifacts/
artifacts:
name: ${{ matrix.artifact.name }} artifact
@@ -219,14 +180,10 @@ jobs:
fi
fi
echo -e "$BODY"
# Set workflow step output
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
DELIMITER="$RANDOM"
echo "BODY<<$DELIMITER" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "$DELIMITER" >> $GITHUB_OUTPUT
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
echo "BODY=$OUTPUT_SAFE_BODY" >> $GITHUB_OUTPUT
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
@@ -274,7 +231,7 @@ jobs:
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.7.0
uses: svenstaro/upload-release-action@2.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
@@ -283,15 +240,6 @@ jobs:
file_glob: true
body: ${{ needs.changelog.outputs.BODY }}
# Temporary measure to prevent release update offers before the manually produced builds are uploaded.
# The step must be removed once fully automated builds are regained.
- name: Remove "channel update info files" related to manual builds
run: |
# See: https://github.com/arduino/arduino-ide/issues/2018
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-linux.yml"
# See: https://github.com/arduino/arduino-ide/issues/408
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-mac.yml"
- name: Publish Release [S3]
if: github.repository == 'arduino/arduino-ide'
uses: docker://plugins/s3

View File

@@ -3,7 +3,6 @@ name: Check Certificates
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-certificates.ya?ml'
@@ -21,49 +20,12 @@ env:
EXPIRATION_WARNING_PERIOD: 30
jobs:
run-determination:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.determination.outputs.result }}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
REPO_SLUG="arduino/arduino-ide"
if [[
(
# Only run on branch creation when it is a release branch.
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
"${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
) &&
(
# Only run when the workflow will have access to the certificate secrets.
# This could be done via a GitHub Actions workflow conditional, but makes more sense to do it here as well.
(
"${{ github.event_name }}" != "pull_request" &&
"${{ github.repository }}" == "$REPO_SLUG"
) ||
(
"${{ github.event_name }}" == "pull_request" &&
"${{ github.event.pull_request.head.repo.full_name }}" == "$REPO_SLUG"
)
)
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check-certificates:
name: ${{ matrix.certificate.identifier }}
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
# Only run when the workflow will have access to the certificate secrets.
if: >
(github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide')
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -97,9 +59,7 @@ jobs:
(
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-legacy \
-noout \
-passin env:CERTIFICATE_PASSWORD
-noout -passin env:CERTIFICATE_PASSWORD
) || (
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
exit 1
@@ -127,7 +87,6 @@ jobs:
openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \
-clcerts \
-legacy \
-nodes \
-passin env:CERTIFICATE_PASSWORD
) | (

View File

@@ -2,11 +2,10 @@ name: Check Internationalization
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: "1.17"
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-i18n-task.ya?ml'
@@ -23,50 +22,21 @@ on:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.determination.outputs.result }}
permissions: {}
steps:
- name: Determine if the rest of the workflow should run
id: determination
run: |
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
TAG_REGEX="refs/tags/.*"
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
if [[
("${{ github.event_name }}" != "create" ||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX) &&
! "${{ github.ref }}" =~ $TAG_REGEX
]]; then
# Run the other jobs.
RESULT="true"
else
# There is no need to run the other jobs.
RESULT="false"
fi
echo "result=$RESULT" >> $GITHUB_OUTPUT
check:
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Node.js 16.14
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '16.14'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@@ -77,9 +47,7 @@ jobs:
version: 3.x
- name: Install dependencies
run: yarn install --immutable
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn
- name: Check for errors
run: yarn i18n:check

View File

@@ -32,7 +32,7 @@ jobs:
- name: Create full changelog
id: full-changelog
run: |
yarn add @octokit/rest@19.0.13 --ignore-workspace-root-check
yarn add @octokit/rest --ignore-workspace-root-check
mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}"
# Get the changelog file name to build

View File

@@ -2,7 +2,7 @@ name: i18n-nightly-push
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: "1.17"
on:
schedule:
@@ -16,15 +16,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js 16.14
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '16.14'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@@ -35,7 +34,7 @@ jobs:
version: 3.x
- name: Install dependencies
run: yarn install --immutable
run: yarn
- name: Run i18n:push script
run: yarn run i18n:push

View File

@@ -2,7 +2,7 @@ name: i18n-weekly-pull
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: "1.17"
on:
schedule:
@@ -16,15 +16,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js 16.14
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '16.14'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@@ -35,7 +34,7 @@ jobs:
version: 3.x
- name: Install dependencies
run: yarn install --immutable
run: yarn
- name: Run i18n:pull script
run: yarn run i18n:pull
@@ -46,7 +45,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v4
with:
commit-message: Updated translation files
title: Update translation files

View File

@@ -8,7 +8,7 @@ on:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.19"
GO_VERSION: "1.17"
NODE_VERSION: 16.x
jobs:
@@ -23,10 +23,9 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@@ -37,7 +36,7 @@ jobs:
version: 3.x
- name: Install dependencies
run: yarn install --immutable
run: yarn
- name: Run themes:pull script
run: yarn run themes:pull
@@ -55,7 +54,7 @@ jobs:
run: yarn run themes:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v4
with:
commit-message: Updated themes
title: Update themes

19
.gitignore vendored
View File

@@ -1,22 +1,25 @@
node_modules/
# .node_modules is a hack for the electron builder.
.node_modules/
lib/
downloads/
resources/
arduino-ide-extension/Examples/
build/
Examples/
!electron/build/
src-gen/
!webpack.config.js
gen-webpack.config.js
gen-webpack.node.config.js
.DS_Store
# switching from `electron` to `browser` in dev mode.
.browser_modules
yarn*.log
# For the VS Code extensions used by Theia.
electron-app/plugins
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes
scripts/themes/tokens
# environment variables
.env
# content trace files for electron
electron-app/traces
# any Arduino LS generated log files
inols*.log
# The electron-builder output.
electron-app/dist

71
.vscode/launch.json vendored
View File

@@ -4,33 +4,35 @@
{
"type": "node",
"request": "launch",
"name": "App",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"name": "App (Electron) [Dev]",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--app-project-path=${workspaceFolder}/electron-app",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:./plugins",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--no-ping-timeout",
"--content-trace",
"--open-devtools"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/electron-app/lib/backend/electron-main.js",
"${workspaceFolder}/electron-app/lib/backend/main.js",
"${workspaceFolder}/electron-app/lib/**/*.js",
"${workspaceFolder}/arduino-ide-extension/lib/**/*.js",
"${workspaceFolder}/node_modules/@theia/**/*.js"
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
@@ -39,35 +41,33 @@
{
"type": "node",
"request": "launch",
"name": "App [Dev]",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"name": "App (Electron)",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--app-project-path=${workspaceFolder}/electron-app",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:./plugins",
"--hosted-plugin-inspect=9339",
"--content-trace",
"--open-devtools",
"--no-ping-timeout",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/electron-app/lib/backend/electron-main.js",
"${workspaceFolder}/electron-app/lib/backend/main.js",
"${workspaceFolder}/electron-app/lib/**/*.js",
"${workspaceFolder}/arduino-ide-extension/lib/**/*.js",
"${workspaceFolder}/node_modules/@theia/**/*.js"
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
@@ -84,7 +84,7 @@
"type": "node",
"request": "launch",
"name": "Run Test [current]",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": [
"--require",
"reflect-metadata/Reflect",
@@ -94,16 +94,8 @@
"--colors",
"**/${fileBasenameNoExtension}.js"
],
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"env": {
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json",
"IDE2_TEST": "true"
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
},
"sourceMaps": true,
"smartStep": true,
@@ -116,12 +108,19 @@
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
}
],
"compounds": [
{
"name": "Launch Electron Backend & Frontend",
"configurations": [
"App",
"App (Electron)",
"Attach to Electron Frontend"
]
}

View File

@@ -2,9 +2,6 @@
"files.exclude": {
"**/lib": false
},
"search.exclude": {
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true

12
.vscode/tasks.json vendored
View File

@@ -2,7 +2,7 @@
"version": "2.0.0",
"tasks": [
{
"label": "Rebuild App",
"label": "Arduino IDE - Rebuild Electron App",
"type": "shell",
"command": "yarn rebuild:browser && yarn rebuild:electron",
"group": "build",
@@ -13,7 +13,7 @@
}
},
{
"label": "Watch Extension",
"label": "Arduino IDE - Watch IDE Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@@ -24,7 +24,7 @@
}
},
{
"label": "Watch App",
"label": "Arduino IDE - Watch Electron App",
"type": "shell",
"command": "yarn --cwd ./electron-app watch",
"group": "build",
@@ -35,11 +35,11 @@
}
},
{
"label": "Watch All",
"label": "Arduino IDE - Watch All [Electron]",
"type": "shell",
"dependsOn": [
"Watch Extension",
"Watch App"
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Electron App"
]
}
]

File diff suppressed because one or more lines are too long

View File

@@ -1,101 +1,89 @@
{
"name": "arduino-ide-extension",
"version": "2.2.1",
"version": "2.0.3",
"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-i18n && yarn download-examples",
"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-i18n": "ncp ../i18n ./src/node/resources/i18n",
"copy-i18n": "npx ncp ../i18n ./build/i18n",
"download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "eslint",
"prebuild": "rimraf lib",
"build": "tsc",
"build:dev": "yarn build",
"postbuild": "ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w",
"test": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.test.js\"",
"test:slow": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.slow-test.js\" --slow 5000"
"test": "mocha \"./lib/test/**/*.test.js\"",
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"@grpc/grpc-js": "^1.8.14",
"@theia/application-package": "1.39.0",
"@theia/core": "1.39.0",
"@theia/debug": "1.39.0",
"@theia/editor": "1.39.0",
"@theia/electron": "1.39.0",
"@theia/filesystem": "1.39.0",
"@theia/keymaps": "1.39.0",
"@theia/markers": "1.39.0",
"@theia/messages": "1.39.0",
"@theia/monaco": "1.39.0",
"@theia/monaco-editor-core": "1.72.3",
"@theia/navigator": "1.39.0",
"@theia/outline-view": "1.39.0",
"@theia/output": "1.39.0",
"@theia/plugin-ext": "1.39.0",
"@theia/preferences": "1.39.0",
"@theia/scm": "1.39.0",
"@theia/search-in-workspace": "1.39.0",
"@theia/terminal": "1.39.0",
"@theia/typehierarchy": "1.39.0",
"@theia/workspace": "1.39.0",
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/markers": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/outline-view": "1.25.0",
"@theia/output": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/search-in-workspace": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/jsdom": "^21.1.1",
"@types/keytar": "^4.4.0",
"@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4",
"@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"classnames": "^2.3.1",
"cpy": "^10.0.0",
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3",
"deepmerge": "^4.2.2",
"drivelist": "^9.2.4",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0",
"glob": "^7.1.6",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-online": "^10.0.0",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
"jsdom": "^21.1.1",
"jsonc-parser": "^2.2.0",
"just-diff": "^5.1.1",
"jwt-decode": "^3.1.2",
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"ncp": "^2.0.0",
"node-fetch": "^2.6.1",
"node-log-rotate": "^0.1.5",
"open": "^8.0.6",
"p-debounce": "^2.1.0",
"p-queue": "^2.4.2",
"process": "^0.11.10",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"query-string": "^7.0.1",
"react-disable": "^0.1.1",
"react-disable": "^0.1.0",
"react-markdown": "^8.0.0",
"react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0",
"react-select": "^3.0.4",
"react-tabs": "^3.1.2",
"react-window": "^1.8.6",
"semver": "^7.3.2",
@@ -103,34 +91,38 @@
"temp": "^0.9.1",
"temp-dir": "^2.0.0",
"tree-kill": "^1.2.1",
"util": "^0.12.5",
"vscode-arduino-api": "^0.1.2"
"upath": "^1.1.2",
"url": "^0.11.0",
"which": "^1.3.1"
},
"devDependencies": {
"@octokit/rest": "^18.12.0",
"@types/chai": "^4.2.7",
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"@types/react-window": "^1.8.5",
"@xhmikosr/downloader": "^13.0.1",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"chai": "^4.2.0",
"cross-env": "^7.0.3",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
"grpc_tools_node_protoc_ts": "^4.1.0",
"mocha": "^7.0.0",
"mockdate": "^3.0.5",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"typemoq": "^2.1.0",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
"optionalDependencies": {
"grpc-tools": "^1.9.0",
"protoc": "^1.0.4"
"grpc-tools": "^1.9.0"
},
"mocha": {
"require": [
@@ -150,9 +142,6 @@
"examples"
],
"theiaExtensions": [
{
"preload": "lib/electron-browser/preload"
},
{
"backend": "lib/node/arduino-ide-backend-module",
"frontend": "lib/browser/arduino-ide-frontend-module"
@@ -163,25 +152,22 @@
{
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{
"frontendElectron": "lib/electron-browser/electron-arduino-module"
},
{
"electronMain": "lib/electron-main/arduino-electron-main-module"
}
],
"arduino": {
"arduino-cli": {
"version": "0.34.0"
"cli": {
"version": "0.29.0"
},
"arduino-fwuploader": {
"version": "2.4.1"
},
"arduino-language-server": {
"version": "0.7.4"
"fwuploader": {
"version": "2.2.2"
},
"clangd": {
"version": "14.0.0"
},
"languageServer": {
"version": "0.7.2"
}
}
}

View File

@@ -19,7 +19,7 @@
return undefined;
}
const cli = arduino['arduino-cli'];
const { cli } = arduino;
if (!cli) {
return undefined;
}
@@ -34,15 +34,9 @@
}
const { platform, arch } = process;
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const buildFolder = path.join(__dirname, '..', 'build');
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
const destinationPath = path.join(resourcesFolder, cliName);
const destinationPath = path.join(buildFolder, cliName);
if (typeof version === 'string') {
const suffix = (() => {

View File

@@ -4,45 +4,33 @@
const version = '1.10.0';
(async () => {
const os = require('node:os');
const { existsSync, promises: fs } = require('node:fs');
const path = require('node:path');
const os = require('os');
const { promises: fs } = require('fs');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const { exec } = require('./utils');
const destination = path.join(
__dirname,
'..',
'src',
'node',
'resources',
'Examples'
);
if (existsSync(destination)) {
shell.echo(
`Skipping Git checkout of the examples because the repository already exists: ${destination}`
);
return;
}
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
exec(
'git',
['clone', 'https://github.com/arduino/arduino-examples.git', repository],
shell
);
if (
shell.exec(
`git clone https://github.com/arduino/arduino-examples.git ${repository}`
).code !== 0
) {
shell.exit(1);
}
exec(
'git',
['-C', repository, 'checkout', `tags/${version}`, '-b', version],
shell
);
if (
shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`)
.code !== 0
) {
shell.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);

View File

@@ -1,11 +1,12 @@
// @ts-check
(async () => {
const path = require('node:path');
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
const semver = require('semver');
const downloader = require('./downloader');
const { taskBuildFromGit } = require('./utils');
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
@@ -18,7 +19,7 @@
return undefined;
}
const fwuploader = arduino['arduino-fwuploader'];
const { fwuploader } = arduino;
if (!fwuploader) {
return undefined;
}
@@ -35,17 +36,11 @@
}
const { platform, arch } = process;
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const buildFolder = path.join(__dirname, '..', 'build');
const fwuploderName = `arduino-fwuploader${
platform === 'win32' ? '.exe' : ''
}`;
const destinationPath = path.join(resourcesFolder, fwuploderName);
const destinationPath = path.join(buildFolder, fwuploderName);
if (typeof version === 'string') {
const suffix = (() => {
@@ -91,6 +86,81 @@
shell.exit(1);
}
} else {
taskBuildFromGit(version, destinationPath, 'Firmware Uploader');
// 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 Firmware Uploader from ${url}. Commitish: ${
commitish ? commitish : 'HEAD'
}`
);
if (fs.existsSync(destinationPath)) {
shell.echo(
`Skipping the Firmware Uploader 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 Firmware Uploader source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Cloned Firmware Uploader 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 Firmware Uploader...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Firmware Uploader build done.');
if (!fs.existsSync(path.join(tempRepoPath, fwuploderName))) {
shell.echo(
`Could not find the Firmware Uploader at ${path.join(
tempRepoPath,
fwuploderName
)}.`
);
shell.exit(1);
}
const builtFwUploaderPath = path.join(tempRepoPath, fwuploderName);
shell.echo(
`>>> Copying Firmware Uploader from ${builtFwUploaderPath} to ${destinationPath}...`
);
if (shell.cp(builtFwUploaderPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the Firmware Uploader.`);
shell.echo('<<< Verifying Firmware Uploader...');
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo('>>> Verified Firmware Uploader.');
}
})();

View File

@@ -16,8 +16,7 @@
const { arduino } = pkg;
if (!arduino) return [undefined, undefined];
const { clangd } = arduino;
const languageServer = arduino['arduino-language-server'];
const { languageServer, clangd } = arduino;
if (!languageServer) return [undefined, undefined];
if (!clangd) return [undefined, undefined];
@@ -63,50 +62,35 @@
const force = yargs['force-download'];
const { platform, arch } = process;
const platformArch = platform + '-' + arch;
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const build = path.join(__dirname, '..', 'build');
const lsExecutablePath = path.join(
resourcesFolder,
build,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'darwin-arm64':
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_ARM64.tar.gz';
clangdSuffix = 'macOS_ARM64';
break;
case 'linux-x64':
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'linux-arm64':
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
lsSuffix = 'Linux_ARM64.tar.gz';
clangdSuffix = 'Linux_ARM64';
break;
case 'win32-x64':
clangdExecutablePath = path.join(resourcesFolder, 'clangd.exe');
clangFormatExecutablePath = path.join(
resourcesFolder,
'clang-format.exe'
);
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
@@ -126,31 +110,20 @@
? 'nightly/arduino-language-server'
: 'arduino-language-server_' + lsVersion
}_${lsSuffix}`;
downloader.downloadUnzipAll(
lsUrl,
resourcesFolder,
lsExecutablePath,
force
);
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,
resourcesFolder,
clangdExecutablePath,
force,
{
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}
); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
resourcesFolder,
build,
clangFormatExecutablePath,
force,
{

View File

@@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
const shell = require('shelljs');
const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untargz = require('decompress-targz');
@@ -46,7 +47,6 @@ exports.downloadUnzipFile = async (
}
shell.echo(`>>> Downloading from '${url}'...`);
const { default: download } = await import('@xhmikosr/downloader');
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
@@ -107,7 +107,6 @@ exports.downloadUnzipAll = async (
}
shell.echo(`>>> Downloading from '${url}'...`);
const { default: download } = await import('@xhmikosr/downloader');
const data = await download(url);
shell.echo(`<<< Download succeeded.`);

View File

@@ -1,13 +1,15 @@
// @ts-check
(async () => {
const os = require('node:os');
const path = require('node:path');
const { exec } = require('./utils');
const os = require('os');
const path = require('path');
const glob = require('glob');
const { v4 } = require('uuid');
const shell = require('shelljs');
const protoc = path.dirname(require('protoc/protoc'));
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${protoc}`;
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
if (shell.mkdir('-p', repository).code !== 0) {
@@ -21,28 +23,23 @@
shell.exit(1);
}
const defaultVersion = {
owner: 'arduino',
repo: 'arduino-cli',
commitish: undefined,
};
const { arduino } = pkg;
if (!arduino) {
return defaultVersion;
return { owner: 'arduino', repo: 'arduino-cli' };
}
const cli = arduino['arduino-cli'];
const { cli } = arduino;
if (!cli) {
return defaultVersion;
return { owner: 'arduino', repo: 'arduino-cli' };
}
const { version } = cli;
if (!version) {
return defaultVersion;
return { owner: 'arduino', repo: 'arduino-cli' };
}
if (typeof version === 'string') {
return defaultVersion;
return { owner: 'arduino', repo: 'arduino-cli' };
}
// We assume an object with `owner`, `repo`, commitish?` properties.
@@ -61,22 +58,15 @@
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(`>>> Cloning repository from '${url}'...`);
exec('git', ['clone', url, repository], shell);
if (shell.exec(`git clone ${url} ${repository}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Repository cloned.`);
const { platform } = process;
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
const cli = path.join(
resourcesFolder,
`arduino-cli${platform === 'win32' ? '.exe' : ''}`
);
const versionJson = exec(cli, ['version', '--format', 'json'], shell).trim();
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const versionJson = shell.exec(`${cli} version --format json`).trim();
if (!versionJson) {
shell.echo(`Could not retrieve the CLI version from ${cli}.`);
shell.exit(1);
@@ -97,94 +87,70 @@
*/
const versionObject = JSON.parse(versionJson);
const version = versionObject.VersionString;
if (
version &&
!version.startsWith('nightly-') &&
version !== '0.0.0-git' &&
version !== 'git-snapshot'
) {
if (version && !version.startsWith('nightly-') && version !== '0.0.0-git' && version !== 'git-snapshot') {
shell.echo(`>>> Checking out tagged version: '${version}'...`);
exec('git', ['-C', repository, 'fetch', '--all', '--tags'], shell);
exec(
'git',
['-C', repository, 'checkout', `tags/${version}`, '-b', version],
shell
);
shell.echo(`<<< Checked out tagged version: '${version}'.`);
shell.exec(`git -C ${repository} fetch --all --tags`);
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out tagged version: '${commitish}'.`);
} else if (commitish) {
shell.echo(
`>>> Checking out commitish from 'package.json': '${commitish}'...`
);
exec('git', ['-C', repository, 'checkout', commitish], shell);
shell.echo(
`<<< Checked out commitish from 'package.json': '${commitish}'.`
);
shell.echo(`>>> Checking out commitish from 'package.json': '${commitish}'...`);
if (shell.exec(`git -C ${repository} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out commitish from 'package.json': '${commitish}'.`);
} else if (versionObject.Commit) {
shell.echo(
`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`
);
exec('git', ['-C', repository, 'checkout', versionObject.Commit], shell);
shell.echo(
`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`
);
shell.echo(`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`);
if (shell.exec(`git -C ${repository} checkout ${versionObject.Commit}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`);
} else {
shell.echo(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
}
shell.echo('>>> Generating TS/JS API from:');
exec('git', ['-C', repository, 'rev-parse', '--abbrev-ref', 'HEAD'], shell);
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
shell.exit(1);
}
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
shell.mkdir('-p', out);
const protos = await new Promise((resolve) =>
const protos = await new Promise(resolve =>
glob('**/*.proto', { cwd: rpc }, (error, matches) => {
if (error) {
shell.echo(error.stack ?? error.message);
shell.echo(error.stack);
resolve([]);
return;
}
resolve(matches.map((filename) => path.join(rpc, filename)));
})
);
resolve(matches.map(filename => path.join(rpc, filename)));
}));
if (!protos || protos.length === 0) {
shell.echo(`Could not find any .proto files under ${rpc}.`);
shell.exit(1);
}
// Generate JS code from the `.proto` files.
exec(
'grpc_tools_node_protoc',
[
`--js_out=import_style=commonjs,binary:${out}`,
`--grpc_out=generate_package_definition:${out}`,
'-I',
rpc,
...protos,
],
shell
);
if (shell.exec(`grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${out} \
--grpc_out=generate_package_definition:${out} \
-I ${rpc} \
${protos.join(' ')}`).code !== 0) {
shell.exit(1);
}
// Generate the `.d.ts` files for JS.
exec(
path.join(protoc, `protoc${platform === 'win32' ? '.exe' : ''}`),
[
`--plugin=protoc-gen-ts=${path.resolve(
__dirname,
'..',
'node_modules',
'.bin',
`protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`
)}`,
`--ts_out=generate_package_definition:${out}`,
'-I',
rpc,
...protos,
],
shell
);
if (shell.exec(`protoc \
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`)} \
--ts_out=generate_package_definition:${out} \
-I ${rpc} \
${protos.join(' ')}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Generation was successful.');
})();

View File

@@ -1,31 +1,3 @@
// @ts-check
const exec = (
/** @type {string} */ command,
/** @type {readonly string[]} */ args,
/** @type {import('shelljs')|undefined}*/ shell = undefined,
/** @type {import('node:child_process').ExecFileSyncOptionsWithStringEncoding|undefined} */ options = undefined
) => {
try {
const stdout = require('node:child_process').execFileSync(
command,
args,
options ? options : { encoding: 'utf8' }
);
if (shell) {
shell.echo(stdout.trim());
}
return stdout;
} catch (err) {
if (shell) {
shell.echo(err instanceof Error ? err.message : String(err));
shell.exit(1);
}
throw err;
}
};
exports.exec = exec;
/**
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
*
@@ -49,15 +21,11 @@ exports.goBuildFromGit = (version, destinationPath, taskName) => {
};
/**
* The `command` must be either `'go'` or `'task'`.
* @param {string} command
* @param {{ owner: any; repo: any; commitish: any; }} version
* @param {string} destinationPath
* @param {string} taskName
* The `command` is either `go` or `task`.
*/
function buildFromGit(command, version, destinationPath, taskName) {
const fs = require('node:fs');
const path = require('node:path');
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
@@ -90,33 +58,31 @@ function buildFromGit(command, version, destinationPath, taskName) {
return;
}
const resourcesFolder = path.join(
__dirname,
'..',
'src',
'node',
'resources'
);
if (shell.mkdir('-p', resourcesFolder).code !== 0) {
shell.echo('Could not create resources folder.');
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}...`);
exec('git', ['clone', url, tempRepoPath], shell);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Cloned ${taskName} repo.`);
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
exec('git', ['-C', tempRepoPath, 'checkout', commitish], shell);
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
exec('git', ['-C', tempRepoPath, 'rev-parse', '--short', 'HEAD'], shell);
shell.echo(`>>> Building the ${taskName}...`);
exec(command, ['build'], shell, { cwd: tempRepoPath, encoding: 'utf8' });
if (shell.exec(`${command} build`, { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Done ${taskName} build.`);
const binName = path.basename(destinationPath);

View File

@@ -1,16 +0,0 @@
import type { Disposable } from '@theia/core/lib/common/disposable';
import type { AppInfo } from '../electron-common/electron-arduino';
import type { StartupTasks } from '../electron-common/startup-task';
import type { Sketch } from './contributions/contribution';
export type { AppInfo };
export const AppService = Symbol('AppService');
export interface AppService {
quit(): void;
info(): Promise<AppInfo>;
registerStartupTasksHandler(
handler: (tasks: StartupTasks) => void
): Disposable;
scheduleDeletion(sketch: Sketch): void; // TODO: find a better place
}

View File

@@ -1,47 +1,44 @@
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
ColorTheme,
CssStyleCollector,
StylingParticipant,
} from '@theia/core/lib/browser/styling-service';
import {
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
} from '@theia/core/lib/common/menu';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { isHighContrast } from '@theia/core/lib/common/theme';
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import React from '@theia/core/shared/react';
import { EditorCommands } from '@theia/editor/lib/browser/editor-command';
import { EditorMainMenu } from '@theia/editor/lib/browser/editor-menu';
import * as React from '@theia/core/shared/react';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
} from '@theia/core';
import {
FrontendApplication,
FrontendApplicationContribution,
} from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { 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 { 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 { ArduinoPreferences } from './arduino-preferences';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
@injectable()
export class ArduinoFrontendContribution
@@ -50,8 +47,7 @@ export class ArduinoFrontendContribution
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution,
StylingParticipant
ColorContribution
{
@inject(MessageService)
private readonly messageService: MessageService;
@@ -62,14 +58,14 @@ export class ArduinoFrontendContribution
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(ElectronWindowPreferences)
private readonly electronWindowPreferences: ElectronWindowPreferences;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@postConstruct()
protected init(): void {
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn(
@@ -81,25 +77,30 @@ export class ArduinoFrontendContribution
}
}
onStart(): void {
this.electronWindowPreferences.onPreferenceChanged((event) => {
onStart(app: FrontendApplication): void {
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
case 'window.zoomLevel':
case 'arduino.window.zoomLevel':
if (typeof event.newValue === 'number') {
window.electronTheiaCore.setZoomLevel(event.newValue || 0);
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
break;
}
}
});
this.appStateService.reachedState('ready').then(() =>
this.electronWindowPreferences.ready.then(() => {
const zoomLevel =
this.electronWindowPreferences.get('window.zoomLevel');
window.electronTheiaCore.setZoomLevel(zoomLevel);
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get(
'arduino.window.zoomLevel'
);
webContents.setZoomLevel(zoomLevel);
})
);
// Removes the _Settings_ (cog) icon from the left sidebar
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -173,8 +174,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'button.background',
light: 'button.background',
hcDark: 'activityBar.inactiveForeground',
hcLight: 'activityBar.inactiveForeground',
hc: 'activityBar.inactiveForeground',
},
description:
'Background color of the toolbar items. Such as Upload, Verify, etc.',
@@ -184,8 +184,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'button.hoverBackground',
light: 'button.hoverBackground',
hcDark: 'button.background',
hcLight: 'button.background',
hc: 'button.background',
},
description:
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
@@ -195,8 +194,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'secondaryButton.foreground',
light: 'button.foreground',
hcDark: 'activityBar.inactiveForeground',
hcLight: 'activityBar.inactiveForeground',
hc: 'activityBar.inactiveForeground',
},
description:
'Foreground color of the toolbar items. Such as Serial Monitor and Serial Plotter',
@@ -206,8 +204,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'secondaryButton.hoverBackground',
light: 'button.hoverBackground',
hcDark: 'textLink.foreground',
hcLight: 'textLink.foreground',
hc: 'textLink.foreground',
},
description:
'Background color of the toolbar items when hovering over them, such as "Serial Monitor" and "Serial Plotter"',
@@ -217,8 +214,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'editor.selectionBackground',
light: 'editor.selectionBackground',
hcDark: 'textPreformat.foreground',
hcLight: 'textPreformat.foreground',
hc: 'textPreformat.foreground',
},
description:
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
@@ -228,38 +224,37 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'dropdown.border',
light: 'dropdown.border',
hcDark: 'dropdown.border',
hcLight: 'dropdown.border',
hc: 'dropdown.border',
},
description: 'Border color of the Board Selector.',
},
{
id: 'arduino.toolbar.dropdown.borderActive',
defaults: {
dark: 'focusBorder',
light: 'focusBorder',
hcDark: 'focusBorder',
hcLight: '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',
hcDark: 'dropdown.background',
hcLight: 'dropdown.background',
hc: 'dropdown.background',
},
description: 'Background color of the Board Selector.',
},
{
id: 'arduino.toolbar.dropdown.label',
defaults: {
dark: 'dropdown.foreground',
light: 'dropdown.foreground',
hcDark: 'dropdown.foreground',
hcLight: 'dropdown.foreground',
hc: 'dropdown.foreground',
},
description: 'Font color of the Board Selector.',
},
@@ -268,8 +263,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'list.activeSelectionIconForeground',
light: 'list.activeSelectionIconForeground',
hcDark: 'list.activeSelectionIconForeground',
hcLight: 'list.activeSelectionIconForeground',
hc: 'list.activeSelectionIconForeground',
},
description:
'Color of the selected protocol icon in the Board Selector.',
@@ -279,8 +273,7 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'list.hoverBackground',
light: 'list.hoverBackground',
hcDark: 'list.hoverBackground',
hcLight: 'list.hoverBackground',
hc: 'list.hoverBackground',
},
description: 'Background color on hover of the Board Selector options.',
},
@@ -289,191 +282,11 @@ export class ArduinoFrontendContribution
defaults: {
dark: 'list.activeSelectionBackground',
light: 'list.activeSelectionBackground',
hcDark: 'list.activeSelectionBackground',
hcLight: 'list.activeSelectionBackground',
hc: 'list.activeSelectionBackground',
},
description:
'Background color of the selected board in the Board Selector.',
}
);
}
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
const warningForeground = theme.getColor('warningForeground');
const warningBackground = theme.getColor('warningBackground');
const focusBorder = theme.getColor('focusBorder');
const contrastBorder = theme.getColor('contrastBorder');
const notificationsBackground = theme.getColor('notifications.background');
const buttonBorder = theme.getColor('button.border');
const buttonBackground = theme.getColor('button.background') || 'none';
const dropdownBackground = theme.getColor('dropdown.background');
const arduinoToolbarButtonBackground = theme.getColor(
'arduino.toolbar.button.background'
);
if (isHighContrast(theme.type)) {
// toolbar items
collector.addRule(`
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-monitor,
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-plotter {
background: transparent;
}
`);
if (contrastBorder) {
collector.addRule(`
.quick-input-widget {
outline: 1px solid ${contrastBorder};
outline-offset: -1px;
}
`);
}
if (focusBorder) {
// customized react-select widget
collector.addRule(`
.arduino-select__option--is-selected {
outline: 1px solid ${focusBorder};
}
`);
collector.addRule(`
.arduino-select__option--is-focused {
outline: 1px dashed ${focusBorder};
}
`);
// boards selector dropdown
collector.addRule(`
#select-board-dialog .selectBoardContainer .list .item:hover {
outline: 1px dashed ${focusBorder};
}
`);
// button hover
collector.addRule(`
.theia-button:hover,
button.theia-button:hover {
outline: 1px dashed ${focusBorder};
}
`);
collector.addRule(`
.theia-button {
border: 1px solid ${focusBorder};
}
`);
collector.addRule(`
.component-list-item .header .installed-version:hover:before {
background-color: transparent;
outline: 1px dashed ${focusBorder};
}
`);
// tree node
collector.addRule(`
.theia-TreeNode:hover {
outline: 1px dashed ${focusBorder};
}
`);
collector.addRule(`
.quick-input-list .monaco-list-row.focused,
.theia-Tree .theia-TreeNode.theia-mod-selected {
outline: 1px dotted ${focusBorder};
}
`);
collector.addRule(`
div#select-board-dialog .selectBoardContainer .list .item.selected,
.theia-Tree:focus .theia-TreeNode.theia-mod-selected,
.theia-Tree .ReactVirtualized__List:focus .theia-TreeNode.theia-mod-selected {
outline: 1px solid ${focusBorder};
}
`);
// quick input
collector.addRule(`
.quick-input-list .monaco-list-row:hover {
outline: 1px dashed ${focusBorder};
}
`);
// editor tab-bar
collector.addRule(`
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:hover {
outline: 1px dashed ${focusBorder};
}
`);
collector.addRule(`
#theia-main-content-panel .p-TabBar .p-TabBar-tab:hover {
outline: 1px dashed ${focusBorder};
outline-offset: -4px;
}
`);
collector.addRule(`
#theia-main-content-panel .p-TabBar .p-TabBar-tab.p-mod-current {
outline: 1px solid ${focusBorder};
outline-offset: -4px;
}
`);
// boards selector dropdown
collector.addRule(`
.arduino-boards-dropdown-item:hover {
outline: 1px dashed ${focusBorder};
outline-offset: -2px;
}
`);
if (notificationsBackground) {
// notification
collector.addRule(`
.theia-notification-list-item:hover:not(:focus) {
background-color: ${notificationsBackground};
outline: 1px dashed ${focusBorder};
outline-offset: -2px;
}
`);
}
if (arduinoToolbarButtonBackground) {
// toolbar item
collector.addRule(`
.item.arduino-tool-item.toggled .arduino-upload-sketch--toolbar,
.item.arduino-tool-item.toggled .arduino-verify-sketch--toolbar {
background-color: ${arduinoToolbarButtonBackground} !important;
outline: 1px solid ${focusBorder};
}
`);
collector.addRule(`
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div {
background: ${arduinoToolbarButtonBackground};
outline: 1px dashed ${focusBorder};
}
`);
}
}
if (dropdownBackground) {
// boards selector dropdown
collector.addRule(`
.arduino-boards-dropdown-item:hover {
background: ${dropdownBackground};
}
`);
}
if (warningForeground && warningBackground) {
// <input> widget with inverted foreground and background colors
collector.addRule(`
.theia-input.warning:focus,
.theia-input.warning::placeholder,
.theia-input.warning {
color: ${warningBackground};
background-color: ${warningForeground};
}
`);
}
if (buttonBorder) {
collector.addRule(`
button.theia-button,
button.theia-button.secondary,
.component-list-item .theia-button.secondary.no-border,
.component-list-item .theia-button.secondary.no-border:hover {
border: 1px solid ${buttonBorder};
}
`);
collector.addRule(`
.component-list-item .header .installed-version:before {
color: ${buttonBackground};
border: 1px solid ${buttonBorder};
}
`);
}
}
}
}

View File

@@ -3,7 +3,10 @@ import { ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
TabBarToolbarContribution,
TabBarToolbarFactory,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import {
FrontendApplicationContribution,
@@ -23,14 +26,11 @@ import {
SketchesService,
SketchesServicePath,
} from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import {
BoardListDumper,
BoardsServiceProvider,
} from './boards/boards-service-provider';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
@@ -64,6 +64,7 @@ import {
BoardsConfigDialog,
BoardsConfigDialogProps,
} from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
@@ -81,12 +82,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 {
ArduinoComponentContextMenuRenderer,
ListItemRenderer,
} from './widgets/component-list/list-item-renderer';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import {
MonacoThemeJson,
MonacoThemingService,
} from '@theia/monaco/lib/browser/monaco-theming-service';
import {
ArduinoDaemonPath,
ArduinoDaemon,
@@ -95,14 +96,12 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
import {
FrontendConnectionStatusService,
ApplicationConnectionStatusContribution,
DaemonPort,
IsOnline,
} from './theia/core/connection-status-service';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
} from '@theia/core/lib/browser/connection-status-service';
import { BoardsDataMenuUpdater } from './contributions/boards-data-menu-updater';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core/lib/common/logger';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
@@ -134,10 +133,11 @@ import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { OpenSettings } from './contributions/open-settings';
import { Settings } from './contributions/settings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { BurnBootloader } from './contributions/burn-bootloader';
@@ -181,6 +181,8 @@ import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug } from './contributions/debug';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
@@ -210,6 +212,7 @@ 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';
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
@@ -238,7 +241,9 @@ import { UploadFirmware } from './contributions/upload-firmware';
import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
UploadFirmwareDialogWidget,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
import { UploadCertificate } from './contributions/upload-certificate';
import {
ArduinoFirmwareUploader,
@@ -253,6 +258,7 @@ import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-c
import {
UserFieldsDialog,
UserFieldsDialogProps,
UserFieldsDialogWidget,
} from './dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
@@ -265,6 +271,7 @@ import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
import {
IDEUpdaterDialog,
IDEUpdaterDialogProps,
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model';
@@ -296,7 +303,7 @@ 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 { StartupTasksExecutor } from './contributions/startup-tasks-executor';
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';
@@ -306,6 +313,10 @@ 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';
@@ -326,58 +337,41 @@ import { InterfaceScale } from './contributions/interface-scale';
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import {
MonacoThemingService,
CleanupObsoleteThemes,
ThemesRegistrationSummary,
MonacoThemeRegistry,
} from './theia/monaco/monaco-theming-service';
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { ConfigServiceClient } from './config/config-service-client';
import { ValidateSketch } from './contributions/validate-sketch';
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
import { CreateFeatures } from './create/create-features';
import { Account } from './contributions/account';
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import { CreateCloudCopy } from './contributions/create-cloud-copy';
import { FileResourceResolver } from './theia/filesystem/file-resource';
import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource';
import { StylingParticipant } from '@theia/core/lib/browser/styling-service';
import { MonacoEditorMenuContribution } from './theia/monaco/monaco-menu';
import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu';
import { UpdateArduinoState } from './contributions/update-arduino-state';
import { TerminalFrontendContribution } from './theia/terminal/terminal-frontend-contribution';
import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
// Hack to fix copy/cut/paste issue after electron version update in Theia.
// https://github.com/eclipse-theia/theia/issues/12487
import('@theia/core/lib/browser/common-frontend-contribution.js').then(
(theiaCommonContribution) => {
theiaCommonContribution['supportCopy'] = true;
theiaCommonContribution['supportCut'] = true;
theiaCommonContribution['supportPaste'] = true;
}
);
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, colors, theme adjustments, and toolbar items
// Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution);
bind(StylingParticipant).toService(ArduinoFrontendContribution);
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
@@ -430,8 +424,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(ConfigServiceClient).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
// Boards service
bind(BoardsService)
@@ -446,9 +438,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
bind(CommandContribution).toService(BoardsServiceProvider);
bind(BoardListDumper).toSelf().inSingletonScope();
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution)
.to(BoardsDataMenuUpdater)
.inSingletonScope();
bind(BoardsDataStore).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsDataStore);
// Logger for the Arduino daemon
@@ -477,6 +471,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: nls.localize(
@@ -504,7 +499,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget),
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
},
}));
bind(MonitorManagerProxyFactory).toFactory(
@@ -584,6 +587,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(WorkspaceDeleteHandler)
.inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
@@ -708,7 +719,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, EditContributions);
Contribution.configure(bind, QuitApp);
Contribution.configure(bind, SketchControl);
Contribution.configure(bind, OpenSettings);
Contribution.configure(bind, Settings);
Contribution.configure(bind, BurnBootloader);
Contribution.configure(bind, BuiltInExamples);
Contribution.configure(bind, LibraryExamples);
@@ -727,7 +738,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
Contribution.configure(bind, StartupTasksExecutor);
Contribution.configure(bind, StartupTasks);
Contribution.configure(bind, IndexesUpdateProgress);
Contribution.configure(bind, Daemon);
Contribution.configure(bind, FirstStartupInstaller);
@@ -743,13 +754,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
Contribution.configure(bind, NewCloudSketch);
Contribution.configure(bind, ValidateSketch);
Contribution.configure(bind, RenameCloudSketch);
Contribution.configure(bind, Account);
Contribution.configure(bind, CloudSketchbookContribution);
Contribution.configure(bind, CreateCloudCopy);
Contribution.configure(bind, UpdateArduinoState);
Contribution.configure(bind, BoardsDataMenuUpdater);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -834,6 +838,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService(
@@ -847,6 +854,10 @@ 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);
@@ -878,6 +889,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
),
});
bind(StorageWrapper).toSelf().inSingletonScope();
bind(CommandContribution).toService(StorageWrapper);
bind(NotificationManager).toSelf().inSingletonScope();
rebind(TheiaNotificationManager).toService(NotificationManager);
bind(NotificationsRenderer).toSelf().inSingletonScope();
@@ -907,8 +921,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
bind(CreateApi).toSelf().inSingletonScope();
bind(SketchCache).toSelf().inSingletonScope();
bind(CreateFeatures).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CreateFeatures);
bind(ShareSketchDialog).toSelf().inSingletonScope();
bind(AuthenticationClientService).toSelf().inSingletonScope();
@@ -925,14 +937,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(CreateFsProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CreateFsProvider);
bind(FileServiceContribution).toService(CreateFsProvider);
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(CloudSketchbookContribution);
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalCacheFsProvider);
bind(CloudSketchbookCompositeWidget).toSelf();
bind(WidgetFactory).toDynamicValue((ctx) => ({
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
}));
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
bind(UploadFirmwareDialogProps).toConstantValue({
title: 'UploadFirmware',
@@ -943,11 +958,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'UploadCertificate',
});
bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope();
bind(IDEUpdaterDialog).toSelf().inSingletonScope();
bind(IDEUpdaterDialogProps).toConstantValue({
title: 'IDEUpdater',
});
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields',
@@ -974,60 +991,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
// custom window titles
bind(WindowTitleUpdater).toSelf().inSingletonScope();
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
// register Arduino themes
bind(MonacoThemingService).toSelf().inSingletonScope();
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
// workaround for themes cannot be removed after registration
// https://github.com/eclipse-theia/theia/issues/11151
bind(CleanupObsoleteThemes).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CleanupObsoleteThemes);
bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
// disable type-hierarchy support
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyServiceProvider).toService(
TypeHierarchyServiceProvider
);
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
bind(SidebarBottomMenuWidget).toSelf();
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope();
bind(DaemonPort).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(DaemonPort);
bind(IsOnline).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(IsOnline);
// https://github.com/arduino/arduino-ide/issues/437
bind(FileResourceResolver).toSelf().inSingletonScope();
rebind(TheiaFileResourceResolver).toService(FileResourceResolver);
// Full control over the editor context menu to filter undesired menu items contributed by Theia.
// https://github.com/arduino/arduino-ide/issues/1394
// https://github.com/arduino/arduino-ide/pull/2027#pullrequestreview-1414246614
bind(MonacoEditorMenuContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorMenuContribution).toService(
MonacoEditorMenuContribution
);
// Patch terminal issues.
bind(TerminalFrontendContribution).toSelf().inSingletonScope();
rebind(TheiaTerminalFrontendContribution).toService(
TerminalFrontendContribution
);
});

View File

@@ -1,15 +1,12 @@
import {
PreferenceContribution,
PreferenceProxy,
PreferenceSchema,
PreferenceService,
createPreferenceProxy,
} from '@theia/core/lib/browser/preferences';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { nls } from '@theia/core/lib/common/nls';
import { PreferenceSchemaProperty } from '@theia/core/lib/common/preferences/preference-schema';
import { interfaces } from '@theia/core/shared/inversify';
import { serialMonitorWidgetLabel } from '../common/nls';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceContribution,
PreferenceSchema,
} from '@theia/core/lib/browser/preferences';
import { nls } from '@theia/core/lib/common';
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
export enum UpdateChannel {
@@ -34,7 +31,7 @@ export const ErrorRevealStrategyLiterals = [
*/
'centerIfOutsideViewport',
] as const;
export type ErrorRevealStrategy = (typeof ErrorRevealStrategyLiterals)[number];
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
export namespace ErrorRevealStrategy {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function is(arg: any): arg is ErrorRevealStrategy {
@@ -43,24 +40,9 @@ export namespace ErrorRevealStrategy {
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
}
export type MonitorWidgetDockPanel = Extract<
ApplicationShell.Area,
'bottom' | 'right'
>;
export const defaultMonitorWidgetDockPanel: MonitorWidgetDockPanel = 'bottom';
export function isMonitorWidgetDockPanel(
arg: unknown
): arg is MonitorWidgetDockPanel {
return arg === 'bottom' || arg === 'right';
}
type StrictPreferenceSchemaProperties<T extends object> = {
[p in keyof T]: PreferenceSchemaProperty;
};
type ArduinoPreferenceSchemaProperties =
StrictPreferenceSchemaProperties<ArduinoConfiguration> & { 'arduino.window.zoomLevel': PreferenceSchemaProperty };
const properties: ArduinoPreferenceSchemaProperties = {
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
properties: {
'arduino.language.log': {
type: 'boolean',
description: nls.localize(
@@ -132,12 +114,11 @@ const properties: ArduinoPreferenceSchemaProperties = {
},
'arduino.window.zoomLevel': {
type: 'number',
description: '',
default: 0,
deprecationMessage: nls.localize(
'arduino/preferences/window.zoomLevel/deprecationMessage',
"Deprecated. Use 'window.zoomLevel' instead."
description: nls.localize(
'arduino/preferences/window.zoomLevel',
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
),
default: 0,
},
'arduino.ide.updateChannel': {
type: 'string',
@@ -276,22 +257,8 @@ const properties: ArduinoPreferenceSchemaProperties = {
),
default: undefined,
},
'arduino.monitor.dockPanel': {
type: 'string',
enum: ['bottom', 'right'],
markdownDescription: nls.localize(
'arduino/preferences/monitor/dockPanel',
'The area of the application shell where the _{0}_ widget will reside. It is either "bottom" or "right". It defaults to "{1}".',
serialMonitorWidgetLabel,
defaultMonitorWidgetDockPanel
),
default: defaultMonitorWidgetDockPanel,
},
};
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
properties,
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
@@ -303,6 +270,7 @@ export interface ArduinoConfiguration {
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string;
'arduino.board.certificates': string;
@@ -320,7 +288,6 @@ export interface ArduinoConfiguration {
'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean;
'arduino.monitor.dockPanel': MonitorWidgetDockPanel;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@@ -0,0 +1,68 @@
import { URI } from '@theia/core/shared/vscode-uri';
import { isWindows } from '@theia/core/lib/common/os';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
/**
* Class for determining the default workspace location from the
* `location.hash`, the historical workspace locations, and recent sketch files.
*
* The following logic is used for determining the default workspace location:
* - `hash` points to an existing location?
* - Yes
* - `validate location`. Is valid sketch location?
* - Yes
* - Done.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
*/
namespace ArduinoWorkspaceRootResolver {
export interface InitOptions {
readonly isValid: (uri: string) => MaybePromise<boolean>;
}
export interface ResolveOptions {
readonly hash?: string;
readonly recentWorkspaces: string[];
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
readonly recentSketches: string[];
}
}
export class ArduinoWorkspaceRootResolver {
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {}
async resolve(
options: ArduinoWorkspaceRootResolver.ResolveOptions
): Promise<{ uri: string } | undefined> {
const { hash, recentWorkspaces, recentSketches } = options;
for (const uri of [
this.hashToUri(hash),
...recentWorkspaces,
...recentSketches,
].filter(notEmpty)) {
const valid = await this.isValid(uri);
if (valid) {
return { uri };
}
}
return undefined;
}
protected isValid(uri: string): MaybePromise<boolean> {
return this.options.isValid(uri);
}
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
// This is important for Windows only and a NOOP on POSIX.
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
protected hashToUri(hash: string | undefined): string | undefined {
if (hash && hash.length > 1 && hash.startsWith('#')) {
const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators
return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString();
}
return undefined;
}
}

View File

@@ -9,13 +9,13 @@ import {
CommandContribution,
} from '@theia/core/lib/common/command';
import {
AuthOptions,
AuthenticationService,
AuthenticationServiceClient,
AuthenticationSession,
authServerPort,
} from '../../common/protocol/authentication-service';
import { CloudUserCommands } from './cloud-user-commands';
import { serverPort } from '../../node/auth/authentication-server';
import { AuthOptions } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences';
@injectable()
@@ -61,7 +61,7 @@ export class AuthenticationClientService
setOptions(): Promise<void> {
return this.service.setOptions({
redirectUri: `http://localhost:${authServerPort}/callback`,
redirectUri: `http://localhost:${serverPort}/callback`,
responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'],
domain: this.arduinoPreferences['arduino.auth.domain'],
@@ -83,13 +83,9 @@ export class AuthenticationClientService
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudUserCommands.LOGIN, {
execute: () => this.service.login(),
isEnabled: () => !this._session,
isVisible: () => !this._session,
});
registry.registerCommand(CloudUserCommands.LOGOUT, {
execute: () => this.service.logout(),
isEnabled: () => !!this._session,
isVisible: () => !!this._session,
});
}

View File

@@ -1,8 +1,5 @@
import { Command } from '@theia/core/lib/common/command';
export const LEARN_MORE_URL =
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
export namespace CloudUserCommands {
export const LOGIN = Command.toLocalizedCommand(
{
@@ -19,4 +16,9 @@ export namespace CloudUserCommands {
},
'arduino/cloud/signOut'
);
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}

View File

@@ -1,201 +1,243 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { injectable, inject } from '@theia/core/shared/inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { notEmpty } from '@theia/core/lib/common/objects';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { InstallManually } from '../../common/nls';
import { Installable, ResponseServiceClient } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
BoardIdentifier,
BoardsPackage,
BoardsService,
createPlatformIdentifier,
isBoardIdentifierChangeEvent,
PlatformIdentifier,
platformIdentifierEquals,
serializePlatformIdentifier,
BoardsPackage,
Board,
Port,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
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 `BoardsConfigChangeEvent`s, if a board is selected which does not
* 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)
private readonly messageService: MessageService;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
protected readonly messageService: MessageService;
@inject(BoardsService)
private readonly boardsService: BoardsService;
protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
protected readonly responseService: ResponseServiceClient;
@inject(BoardsListWidgetFrontendContribution)
private readonly boardsManagerWidgetContribution: BoardsListWidgetFrontendContribution;
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
private readonly installNotificationInfos: Readonly<{
boardName: string;
platformId: string;
notificationId: string;
}>[] = [];
private readonly toDispose = new DisposableCollection();
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.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.ensureCoreExists(event.selectedBoard);
const setEventListeners = () => {
this.boardsServiceClient.onBoardsConfigChanged((config) => {
const { selectedBoard, selectedPort } = config;
const boardWasUnplugged =
!selectedPort && this.portSelectedOnLastRefusal;
this.clearLastRefusedPromptInfo();
if (
boardWasUnplugged ||
!selectedBoard ||
this.promptAlreadyShowingForBoard(selectedBoard)
) {
return;
}
}),
this.notificationCenter.onPlatformDidInstall((event) =>
this.clearAllNotificationForPlatform(event.item.id)
),
]);
this.boardsServiceProvider.ready.then(() => {
const { selectedBoard } = this.boardsServiceProvider.boardsConfig;
this.ensureCoreExists(selectedBoard);
this.ensureCoreExists(selectedBoard, selectedPort);
});
// 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 async findPlatformToInstall(
selectedBoard: BoardIdentifier
): Promise<BoardsPackage | undefined> {
const platformId = await this.findPlatformIdToInstall(selectedBoard);
if (!platformId) {
return undefined;
private removeNotificationByBoard(selectedBoard: Board): void {
const index = this.notifications.findIndex((notification) =>
Board.sameAs(notification, selectedBoard)
);
if (index !== -1) {
this.notifications.splice(index, 1);
}
const id = serializePlatformIdentifier(platformId);
const platform = await this.boardsService.getBoardPackage({ id });
if (!platform) {
console.warn(`Could not resolve platform for ID: ${id}`);
return undefined;
}
if (platform.installedVersion) {
return undefined;
}
return platform;
}
private async findPlatformIdToInstall(
selectedBoard: BoardIdentifier
): Promise<PlatformIdentifier | undefined> {
const selectedBoardPlatformId = createPlatformIdentifier(selectedBoard);
// The board is installed or the FQBN is available from the `board list watch` for Arduino boards. The latter might change!
if (selectedBoardPlatformId) {
const installedPlatforms =
await this.boardsService.getInstalledPlatforms();
const installedPlatformIds = installedPlatforms
.map((platform) => createPlatformIdentifier(platform.id))
.filter(notEmpty);
if (
installedPlatformIds.every(
(installedPlatformId) =>
!platformIdentifierEquals(
installedPlatformId,
selectedBoardPlatformId
)
)
) {
return selectedBoardPlatformId;
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 {
// IDE2 knows that selected board is not installed. Look for board `name` match in not yet installed platforms.
// The order should be correct when there is a board name collision (e.g. Arduino Nano RP2040 from Arduino Mbed OS Nano Boards, [DEPRECATED] Arduino Mbed OS Nano Boards). The CLI boosts the platforms, so picking the first name match should be fine.
const platforms = await this.boardsService.search({});
for (const platform of platforms) {
// Ignore installed platforms
if (platform.installedVersion) {
continue;
this.removeNotificationByBoard(selectedBoard);
}
if (
platform.boards.some((board) => board.name === selectedBoard.name)
) {
const platformId = createPlatformIdentifier(platform.id);
if (platformId) {
return platformId;
}
}
}
}
return undefined;
});
}
private async ensureCoreExists(
selectedBoard: BoardIdentifier | undefined
): Promise<void> {
if (!selectedBoard) {
return;
}
const candidate = await this.findPlatformToInstall(selectedBoard);
if (!candidate) {
return;
}
const platformIdToInstall = candidate.id;
const selectedBoardName = selectedBoard.name;
if (
this.installNotificationInfos.some(
({ boardName, platformId }) =>
platformIdToInstall === platformId && selectedBoardName === boardName
)
) {
// Already has a notification for the board with the same platform. Nothing to do.
return;
}
this.clearAllNotificationForPlatform(platformIdToInstall);
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 yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const message = 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,
const info = this.generatePromptInfoText(
candidateName,
version,
selectedBoard.name
);
const notificationId = this.notificationId(message, InstallManually, yes);
this.installNotificationInfos.push({
boardName: selectedBoardName,
platformId: platformIdToInstall,
notificationId,
});
const answer = await this.messageService.info(
message,
InstallManually,
yes
);
if (answer) {
const index = this.installNotificationInfos.findIndex(
({ boardName, platformId }) =>
platformIdToInstall === platformId && selectedBoardName === boardName
);
if (index !== -1) {
this.installNotificationInfos.splice(index, 1);
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);
}
if (answer === yes) {
await Installable.installWithProgress({
installable: this.boardsService,
item: candidate,
messageService: this.messageService,
responseService: this.responseService,
version: candidate.availableVersions[0],
});
return;
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
);
}
if (answer === InstallManually) {
this.boardsManagerWidgetContribution
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({
@@ -203,27 +245,37 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
type: 'All',
})
);
}
}
}
private clearAllNotificationForPlatform(predicatePlatformId: string): void {
// Discard all install notifications for the same platform.
const notificationsLength = this.installNotificationInfos.length;
for (let i = notificationsLength - 1; i >= 0; i--) {
const { notificationId, platformId } = this.installNotificationInfos[i];
if (platformId === predicatePlatformId) {
this.installNotificationInfos.splice(i, 1);
this.notificationManager.clear(notificationId);
}
}
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager['getMessageId']({
text: message,
actions,
type: MessageType.Info,
},
},
{
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();
}
};
}
}

View File

@@ -1,328 +0,0 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Event } from '@theia/core/lib/common/event';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
import { nls } from '@theia/core/lib/common/nls';
import React from '@theia/core/shared/react';
import { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
import {
Board,
BoardIdentifier,
BoardWithPackage,
DetectedPort,
findMatchingPortIndex,
Port,
PortIdentifier,
} from '../../common/protocol/boards-service';
import type { Defined } from '../../common/types';
import { NotificationCenter } from '../notification-center';
import { BoardsConfigDialogState } from './boards-config-dialog';
namespace BoardsConfigComponent {
export interface Props {
/**
* This is not the real config, it's only living in the dialog. Users can change it without update and can cancel any modifications.
*/
readonly boardsConfig: BoardsConfigDialogState;
readonly searchSet: BoardIdentifier[] | undefined;
readonly notificationCenter: NotificationCenter;
readonly appState: FrontendApplicationState;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<
Defined<EditBoardsConfigActionParams['query']>
>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
readonly onBoardSelected: (board: BoardIdentifier) => void;
readonly onPortSelected: (port: PortIdentifier) => void;
readonly searchBoards: (query?: {
query?: string;
}) => Promise<BoardWithPackage[]>;
readonly ports: (
predicate?: (port: DetectedPort) => boolean
) => readonly DetectedPort[];
}
export interface State {
searchResults: Array<BoardWithPackage>;
showAllPorts: boolean;
query: string;
}
}
export abstract class Item<T> extends React.Component<{
item: T;
label: string;
selected: boolean;
onClick: (item: T) => void;
missing?: boolean;
details?: string;
}> {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
}
private readonly onClick = () => {
this.props.onClick(this.props.item);
};
}
export class BoardsConfigComponent extends React.Component<
BoardsConfigComponent.Props,
BoardsConfigComponent.State
> {
private readonly toDispose: DisposableCollection;
constructor(props: BoardsConfigComponent.Props) {
super(props);
this.state = {
searchResults: [],
showAllPorts: false,
query: '',
};
this.toDispose = new DisposableCollection();
}
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.onAppStateDidChange(async (state) => {
if (state === 'ready') {
const searchResults = await this.queryBoards({});
this.setState({ searchResults });
}
}),
this.props.notificationCenter.onPlatformDidInstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStop(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) => {
if (typeof query === 'string') {
this.setState({ query }, () => this.updateBoards(this.state.query));
}
}),
]);
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
private readonly updateBoards = (
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
) => {
const query =
typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then((searchResults) =>
this.setState({ searchResults })
);
};
private readonly queryBoards = async (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
const result = await this.props.searchBoards(options);
const { searchSet } = this.props;
if (searchSet) {
return result.filter((board) =>
searchSet.some(
(restriction) =>
restriction.fqbn === board.fqbn || restriction.name === board.fqbn
)
);
}
return result;
};
private readonly toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
private readonly selectPort = (selectedPort: PortIdentifier) => {
this.props.onPortSelected(selectedPort);
};
private readonly selectBoard = (selectedBoard: BoardWithPackage) => {
this.props.onBoardSelected(selectedBoard);
};
private readonly focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
override render(): React.ReactNode {
return (
<>
{this.renderContainer(
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</>
);
}
private renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
</div>
</div>
);
}
private renderBoards(): React.ReactNode {
const { boardsConfig } = this.props;
const { searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(
boardsConfig.selectedBoard,
searchResults
)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
}
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
));
return (
<React.Fragment>
<div className="search">
<input
type="search"
value={query}
className="theia-input"
placeholder={nls.localize(
'arduino/board/searchBoard',
'Search board'
)}
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
<i className="fa fa-search"></i>
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
private renderPorts(): React.ReactNode {
const predicate = this.state.showAllPorts ? undefined : Port.isVisiblePort;
const detectedPorts = this.props.ports(predicate);
const matchingIndex = findMatchingPortIndex(
this.props.boardsConfig.selectedPort,
detectedPorts
);
return !detectedPorts.length ? (
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
<div className="ports list">
{detectedPorts.map((detectedPort, index) => (
<Item<Port>
key={`${Port.keyOf(detectedPort.port)}`}
item={detectedPort.port}
label={Port.toString(detectedPort.port)}
selected={index === matchingIndex}
onClick={this.selectPort}
/>
))}
</div>
);
}
private renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label
title={nls.localize(
'arduino/board/showAllAvailablePorts',
'Shows all available ports when enabled'
)}
>
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);
}
}

View File

@@ -0,0 +1,71 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsServiceProvider } from './boards-service-provider';
import { NotificationCenter } from '../notification-center';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
protected readonly onBoardConfigChangedEmitter =
new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined;
constructor() {
super();
this.id = 'select-board-dialog';
this.toDispose.pushAll([
this.onBoardConfigChangedEmitter,
this.onFilterTextDidChangeEmitter,
]);
}
search(query: string): void {
this.onFilterTextDidChangeEmitter.fire(query);
}
protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config);
};
protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element;
};
protected render(): React.ReactNode {
return (
<div className="selectBoardContainer">
<BoardsConfig
boardsServiceProvider={this.boardsServiceClient}
notificationCenter={this.notificationCenter}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
/>
</div>
);
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@@ -0,0 +1,142 @@
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { BoardsConfig } from './boards-config';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {}
@injectable()
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogWidget)
protected readonly widget: BoardsConfigDialogWidget;
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected config: BoardsConfig.Config = {};
constructor(
@inject(BoardsConfigDialogProps)
protected override readonly props: BoardsConfigDialogProps
) {
super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
}
@postConstruct()
protected init(): void {
this.toDispose.push(
this.boardsServiceClient.onBoardsConfigChanged((config) => {
this.config = config;
this.update();
})
);
}
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
override async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
this.widget.search(query);
}
return super.open();
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
nls.localize(
'arduino/board/configDialog1',
'Select both a Board and a Port if you want to upload a sketch.'
),
nls.localize(
'arduino/board/configDialog2',
'If you only select a Board you will be able to compile, but not to upload your sketch.'
),
]) {
const p = document.createElement('div');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(
this.widget.onBoardConfigChanged((config) => {
this.config = config;
this.update();
})
);
super.onAfterAttach(msg);
this.update();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected override isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(
'arduino/board/pleasePickBoard',
'Please pick a board connected to the port you have selected.'
);
}
return false;
}
return '';
}
get value(): BoardsConfig.Config {
return this.config;
}
}

View File

@@ -1,202 +0,0 @@
import { DialogError, DialogProps } from '@theia/core/lib/browser/dialogs';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { deepClone } from '@theia/core/lib/common/objects';
import type { Message } from '@theia/core/shared/@phosphor/messaging';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import React from '@theia/core/shared/react';
import type { ReactNode } from '@theia/core/shared/react/index';
import { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
import {
BoardIdentifier,
BoardsConfig,
BoardWithPackage,
DetectedPort,
emptyBoardsConfig,
PortIdentifier,
} from '../../common/protocol/boards-service';
import type { Defined } from '../../common/types';
import { NotificationCenter } from '../notification-center';
import { ReactDialog } from '../theia/dialogs/dialogs';
import { BoardsConfigComponent } from './boards-config-component';
import { BoardsServiceProvider } from './boards-service-provider';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {}
export type BoardsConfigDialogState = Omit<BoardsConfig, 'selectedBoard'> & {
selectedBoard: BoardsConfig['selectedBoard'] | BoardWithPackage;
};
@injectable()
export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private readonly onFilterTextDidChangeEmitter: Emitter<
Defined<EditBoardsConfigActionParams['query']>
>;
private readonly onBoardSelected = (board: BoardWithPackage): void => {
this._boardsConfig.selectedBoard = board;
this.update();
};
private readonly onPortSelected = (port: PortIdentifier): void => {
this._boardsConfig.selectedPort = port;
this.update();
};
private readonly setFocusNode = (element: HTMLElement | undefined): void => {
this.focusNode = element;
};
private readonly searchBoards = (options: {
query?: string;
}): Promise<BoardWithPackage[]> => {
return this.boardsServiceProvider.searchBoards(options);
};
private readonly ports = (
predicate?: (port: DetectedPort) => boolean
): readonly DetectedPort[] => {
return this.boardsServiceProvider.boardList.ports(predicate);
};
private _boardsConfig: BoardsConfigDialogState;
/**
* When the dialog's boards result set is limited to a subset of boards when searching, this field is set.
*/
private _searchSet: BoardIdentifier[] | undefined;
private focusNode: HTMLElement | undefined;
constructor(
@inject(BoardsConfigDialogProps)
protected override readonly props: BoardsConfigDialogProps
) {
super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog');
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
this._boardsConfig = emptyBoardsConfig();
this.onFilterTextDidChangeEmitter = new Emitter();
}
@postConstruct()
protected init(): void {
this.boardsServiceProvider.onBoardListDidChange(() => {
this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig);
this.update();
});
this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig);
}
override async open(
params?: EditBoardsConfigActionParams
): Promise<BoardsConfig | undefined> {
this._searchSet = undefined;
this._boardsConfig.selectedBoard =
this.boardsServiceProvider.boardsConfig.selectedBoard;
this._boardsConfig.selectedPort =
this.boardsServiceProvider.boardsConfig.selectedPort;
if (params) {
if (typeof params.query === 'string') {
this.onFilterTextDidChangeEmitter.fire(params.query);
}
if (params.portToSelect) {
this._boardsConfig.selectedPort = params.portToSelect;
}
if (params.boardToSelect) {
this._boardsConfig.selectedBoard = params.boardToSelect;
}
if (params.searchSet) {
this._searchSet = params.searchSet.slice();
}
}
return super.open();
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.update();
}
protected override render(): ReactNode {
return (
<>
<div className="head">
<div className="text">
<div>
{nls.localize(
'arduino/board/configDialog1',
'Select both a Board and a Port if you want to upload a sketch.'
)}
</div>
<div>
{nls.localize(
'arduino/board/configDialog2',
'If you only select a Board you will be able to compile, but not to upload your sketch.'
)}
</div>
</div>
</div>
<div id="select-board-dialog" className="p-Widget ps">
<div className="selectBoardContainer">
<BoardsConfigComponent
boardsConfig={this._boardsConfig}
searchSet={this._searchSet}
onBoardSelected={this.onBoardSelected}
onPortSelected={this.onPortSelected}
notificationCenter={this.notificationCenter}
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={
this.onFilterTextDidChangeEmitter.event
}
appState={this.appStateService.state}
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
searchBoards={this.searchBoards}
ports={this.ports}
/>
</div>
</div>
</>
);
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected override isValid(value: BoardsConfig): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(
'arduino/board/pleasePickBoard',
'Please pick a board connected to the port you have selected.'
);
}
return false;
}
return '';
}
get value(): BoardsConfigDialogState {
return this._boardsConfig;
}
}

View File

@@ -0,0 +1,434 @@
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Board,
Port,
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
import {
AvailableBoard,
BoardsServiceProvider,
} from './boards-service-provider';
import { naturalCompare } from '../../common/utils';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export interface Config {
selectedBoard?: Board;
selectedPort?: Port;
}
export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider;
readonly notificationCenter: NotificationCenter;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
}
export interface State extends Config {
searchResults: Array<BoardWithPackage>;
knownPorts: Port[];
showAllPorts: boolean;
query: string;
}
}
export abstract class Item<T> extends React.Component<{
item: T;
label: string;
selected: boolean;
onClick: (item: T) => void;
missing?: boolean;
details?: string;
}> {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
}
protected onClick = () => {
this.props.onClick(this.props.item);
};
}
export class BoardsConfig extends React.Component<
BoardsConfig.Props,
BoardsConfig.State
> {
protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) {
super(props);
const { boardsConfig } = props.boardsServiceProvider;
this.state = {
searchResults: [],
knownPorts: [],
showAllPorts: false,
query: '',
...boardsConfig,
};
}
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.onAppStateDidChange((state) => {
if (state === 'ready') {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
}
}),
this.props.boardsServiceProvider.onAvailablePortsChanged(
({ newState, oldState }) => {
const removedPorts = oldState.filter(
(oldPort) =>
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
);
this.updatePorts(newState, removedPorts);
}
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () =>
this.fireConfigChanged()
);
}
),
this.props.notificationCenter.onPlatformDidInstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStop(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
this.setState({ query }, () => this.updateBoards(this.state.query))
),
]);
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged(): void {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
) => {
const query =
typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then((searchResults) =>
this.setState({ searchResults })
);
};
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
this.fireConfigChanged()
);
});
};
protected queryBoards = (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
};
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty);
}
protected get availableBoards(): AvailableBoard[] {
return this.props.boardsServiceProvider.availableBoards;
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
// Available ports must be sorted in this order:
// 1. Serial with recognized boards
// 2. Serial with guessed boards
// 3. Serial with incomplete boards
// 4. Network with recognized boards
// 5. Other protocols with recognized boards
const ports = (await availablePorts).sort((left: Port, right: Port) => {
if (left.protocol === 'serial' && right.protocol !== 'serial') {
return -1;
} else if (left.protocol !== 'serial' && right.protocol === 'serial') {
return 1;
} else if (left.protocol === 'network' && right.protocol !== 'network') {
return -1;
} else if (left.protocol !== 'network' && right.protocol === 'network') {
return 1;
} else if (left.protocol === right.protocol) {
// We show ports, including those that have guessed
// or unrecognized boards, so we must sort those too.
const leftBoard = this.availableBoards.find(
(board) => board.port === left
);
const rightBoard = this.availableBoards.find(
(board) => board.port === right
);
if (leftBoard && !rightBoard) {
return -1;
} else if (!leftBoard && rightBoard) {
return 1;
} else if (leftBoard?.state! < rightBoard?.state!) {
return -1;
} else if (leftBoard?.state! > rightBoard?.state!) {
return 1;
}
}
return naturalCompare(left.address, right.address);
});
return { knownPorts: ports };
};
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
};
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
};
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
override render(): React.ReactNode {
return (
<>
{this.renderContainer(
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</>
);
}
protected renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
</div>
</div>
);
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
}
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
));
return (
<React.Fragment>
<div className="search">
<input
type="search"
value={query}
className="theia-input"
placeholder={nls.localize(
'arduino/board/searchBoard',
'Search board'
)}
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
<i className="fa fa-search"></i>
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
protected renderPorts(): React.ReactNode {
let ports = [] as Port[];
if (this.state.showAllPorts) {
ports = this.state.knownPorts;
} else {
ports = this.state.knownPorts.filter(
Port.visiblePorts(this.availableBoards)
);
}
return !ports.length ? (
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
<div className="ports list">
{ports.map((port) => (
<Item<Port>
key={`${Port.keyOf(port)}`}
item={port}
label={Port.toString(port)}
selected={Port.sameAs(this.state.selectedPort, port)}
onClick={this.selectPort}
/>
))}
</div>
);
}
protected renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label
title={nls.localize(
'arduino/board/showAllAvailablePorts',
'Shows all available ports when enabled'
)}
>
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);
}
}
export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | Board): boolean {
const { selectedBoard, selectedPort } = config;
if (Board.is(other)) {
return (
!!selectedBoard &&
Board.equals(other, selectedBoard) &&
Port.sameAs(selectedPort, other.port)
);
}
return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return (
left.selectedBoard === right.selectedBoard &&
left.selectedPort === right.selectedPort
);
}
export function toString(
config: Config,
options: { default: string } = { default: '' }
): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ` at ${port.address}` : ''}`;
}
}
}

View File

@@ -1,66 +1,67 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import PQueue from 'p-queue';
import {
BoardIdentifier,
ConfigOption,
isBoardIdentifierChangeEvent,
Programmer,
} from '../../common/protocol';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsServiceProvider } from './boards-service-provider';
import { Board, ConfigOption, Programmer } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
import {
CommandRegistry,
Contribution,
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class BoardsDataMenuUpdater extends Contribution {
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
private readonly toDisposeOnBoardChange = new DisposableCollection();
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
override onStart(): void {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
this.appStateService
.reachedState('ready')
.then(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
)
);
this.boardsDataStore.onChanged(() =>
this.updateMenuActions(
this.boardsServiceProvider.boardsConfig.selectedBoard
this.boardsServiceClient.boardsConfig.selectedBoard
)
);
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateMenuActions(event.selectedBoard);
}
});
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() =>
this.updateMenuActions(
this.boardsServiceProvider.boardsConfig.selectedBoard
)
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.updateMenuActions(selectedBoard)
);
}
private async updateMenuActions(
selectedBoard: BoardIdentifier | undefined
protected async updateMenuActions(
selectedBoard: Board | undefined
): Promise<void> {
return this.queue.add(async () => {
this.toDisposeOnBoardChange.dispose();
this.menuManager.update();
this.mainMenuManager.update();
if (selectedBoard) {
const { fqbn } = selectedBoard;
if (fqbn) {
@@ -79,16 +80,16 @@ export class BoardsDataMenuUpdater extends Contribution {
string,
Disposable & { label: string }
>();
let selectedValue = '';
for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`;
const command = { id };
const selectedValue = value.value;
const handler = {
execute: () =>
this.boardsDataStore.selectConfigOption({
fqbn,
option,
selectedValue: value.value,
selectedValue,
}),
isToggled: () => value.selected,
};
@@ -99,14 +100,8 @@ export class BoardsDataMenuUpdater extends Contribution {
{ label: value.label }
)
);
if (value.selected) {
selectedValue = value.label;
}
}
this.menuRegistry.registerSubmenu(
menuPath,
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
);
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() =>
@@ -171,7 +166,7 @@ export class BoardsDataMenuUpdater extends Contribution {
]);
}
}
this.menuManager.update();
this.mainMenuManager.update();
}
}
});

View File

@@ -1,72 +1,71 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import {
FrontendApplicationContribution,
LocalStorageService,
} from '@theia/core/lib/browser';
import { notEmpty } from '../../common/utils';
import {
BoardDetails,
BoardsService,
ConfigOption,
BoardDetails,
Programmer,
} from '../../common/protocol';
import { notEmpty } from '../../common/utils';
import { NotificationCenter } from '../notification-center';
@injectable()
export class BoardsDataStore implements FrontendApplicationContribution {
@inject(ILogger)
@named('store')
private readonly logger: ILogger;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(LocalStorageService)
private readonly storageService: LocalStorageService;
protected readonly logger: ILogger;
private readonly onChangedEmitter = new Emitter<string[]>();
private readonly toDispose = new DisposableCollection(this.onChangedEmitter);
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(LocalStorageService)
protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void {
this.toDispose.push(
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
const dataDidChangePerFqbn: string[] = [];
let shouldFireChanged = false;
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)
.filter(notEmpty)
.filter((fqbn) => !!fqbn)) {
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<ConfigOption[]>(key);
let data = await this.storageService.getData<
ConfigOption[] | undefined
>(key);
if (!data || !data.length) {
const details = await this.getBoardDetailsSafe(fqbn);
if (details) {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
dataDidChangePerFqbn.push(fqbn);
shouldFireChanged = true;
}
}
}
}
if (dataDidChangePerFqbn.length) {
this.fireChanged(...dataDidChangePerFqbn);
if (shouldFireChanged) {
this.fireChanged();
}
})
);
});
}
onStop(): void {
this.toDispose.dispose();
}
get onChanged(): Event<string[]> {
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined
fqbn: string | undefined,
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
@@ -101,13 +100,12 @@ export class BoardsDataStore implements FrontendApplicationContribution {
return data;
}
async selectProgrammer({
async selectProgrammer(
{
fqbn,
selectedProgrammer,
}: {
fqbn: string;
selectedProgrammer: Programmer;
}): Promise<boolean> {
}: { fqbn: string; selectedProgrammer: Programmer },
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { programmers } = data;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
@@ -118,19 +116,17 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn,
data: { ...data, selectedProgrammer },
});
this.fireChanged(fqbn);
this.fireChanged();
return true;
}
async selectConfigOption({
async selectConfigOption(
{
fqbn,
option,
selectedValue,
}: {
fqbn: string;
option: string;
selectedValue: string;
}): Promise<boolean> {
}: { fqbn: string; option: string; selectedValue: string }
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
@@ -150,7 +146,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
return false;
}
await this.setData({ fqbn, data });
this.fireChanged(fqbn);
this.fireChanged();
return true;
}
@@ -194,8 +190,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
}
}
protected fireChanged(...fqbn: string[]): void {
this.onChangedEmitter.fire(fqbn);
protected fireChanged(): void {
this.onChangedEmitter.fire();
}
}

View File

@@ -30,6 +30,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
searchable: service,
installable: service,
itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer,
filterRenderer,
defaultSearchOptions: { query: '', type: 'All' },

View File

@@ -1,21 +1,16 @@
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar';
import { codicon } from '@theia/core/lib/browser/widgets/widget';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
import { OpenBoardsConfig } from '../contributions/open-boards-config';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import React from '@theia/core/shared/react';
import ReactDOM from '@theia/core/shared/react-dom';
import classNames from 'classnames';
import { boardIdentifierLabel, Port } from '../../common/protocol';
import { BoardListItemUI } from '../../common/protocol/board-list';
import { assertUnreachable } from '../../common/utils';
import type {
BoardListUI,
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;
@@ -27,18 +22,18 @@ export interface BoardsDropDownListCoords {
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly boardList: BoardListUI;
readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
readonly openBoardsConfig: () => void;
readonly hide: () => void;
}
}
export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
private dropdownElement: HTMLElement;
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) {
@@ -56,14 +51,11 @@ export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
}
override render(): React.ReactNode {
return ReactDOM.createPortal(
this.renderBoardListItems(),
this.dropdownElement
);
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
private renderBoardListItems(): React.ReactNode {
const { coords, boardList } = this.props;
protected renderNode(): React.ReactNode {
const { coords, items } = this.props;
if (coords === 'hidden') {
return '';
}
@@ -82,12 +74,14 @@ export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
tabIndex={0}
>
<div className="arduino-boards-dropdown-list--items-container">
{boardList.items.map((item, index) =>
this.renderBoardListItem({
item,
selected: index === boardList.selectedIndex,
})
)}
{items
.map(({ name, port, selected, onClick }) => ({
boardLabel: name,
port,
selected,
onClick,
}))
.map(this.renderItem)}
</div>
<div
key={footerLabel}
@@ -101,43 +95,31 @@ export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
);
}
private readonly onDefaultAction = (item: BoardListItemUI): unknown => {
const { boardList, hide } = this.props;
const { type, params } = item.defaultAction;
hide();
switch (type) {
case 'select-boards-config': {
return boardList.select(params);
}
case 'edit-boards-config': {
return boardList.edit(params);
}
default:
return assertUnreachable(type);
protected renderItem({
boardLabel,
port,
selected,
onClick,
}: {
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();
}
};
private renderBoardListItem({
item,
selected,
}: {
item: BoardListItemUI;
selected: boolean;
}): React.ReactNode {
const { boardLabel, portLabel, portProtocol, tooltip } = item.labels;
const port = item.port;
const onKeyUp = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
this.onDefaultAction(item);
}
};
return (
<div
key={`board-item--${Port.keyOf(port)}`}
key={`board-item--${boardLabel}-${port.address}`}
className={classNames('arduino-boards-dropdown-item', {
'arduino-boards-dropdown-item--selected': selected,
})}
onClick={() => this.onDefaultAction(item)}
onClick={onClick}
onKeyUp={onKeyUp}
tabIndex={0}
>
@@ -145,81 +127,21 @@ export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
className={classNames(
'arduino-boards-dropdown-item--protocol',
'fa',
iconNameFromProtocol(portProtocol)
protocolIcon
)}
/>
<div className="arduino-boards-dropdown-item--label" title={tooltip}>
<div className="arduino-boards-dropdown-item--board-header">
<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>
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
{portLabel}
{port.addressLabel}
</div>
</div>
{this.renderActions(item)}
</div>
);
}
private renderActions(item: BoardListItemUI): React.ReactNode {
const { boardList, hide } = this.props;
const { revert, edit } = item.otherActions;
if (!edit && !revert) {
return undefined;
}
const handleOnClick = (
event: React.MouseEvent<HTMLElement, MouseEvent>,
callback: () => void
) => {
event.preventDefault();
event.stopPropagation();
hide();
callback();
};
return (
<div className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR}>
{edit && (
<div
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}
>
{
<div
id="edit"
className={codicon('pencil', true)}
title={nls.localize(
'arduino/board/editBoardsConfig',
'Edit Board and Port...'
)}
onClick={(event) =>
handleOnClick(event, () => boardList.edit(edit.params))
}
/>
}
</div>
)}
{revert && (
<div
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}
>
{
<div
id="revert"
className={codicon('discard', true)}
title={nls.localize(
'arduino/board/revertBoardsConfig',
"Use '{0}' discovered on '{1}'",
boardIdentifierLabel(revert.params.selectedBoard),
item.labels.portLabel
)}
onClick={(event) =>
handleOnClick(event, () => boardList.select(revert.params))
}
/>
}
</div>
)}
{selected ? <div className="fa fa-check" /> : ''}
</div>
);
}
@@ -231,27 +153,26 @@ export class BoardsToolBarItem extends React.Component<
> {
static TOOLBAR_ID: 'boards-toolbar';
private readonly toDispose: DisposableCollection;
protected readonly toDispose: DisposableCollection =
new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) {
super(props);
const { boardList } = props.boardsServiceProvider;
const { availableBoards } = props.boardsServiceProvider;
this.state = {
boardList,
availableBoards,
coords: 'hidden',
};
const listener = () => this.setState({ coords: 'hidden' });
document.addEventListener('click', listener);
this.toDispose = new DisposableCollection(
Disposable.create(() => document.removeEventListener('click', listener))
);
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
}
override componentDidMount(): void {
this.toDispose.push(
this.props.boardsServiceProvider.onBoardListDidChange((boardList) =>
this.setState({ boardList })
)
this.props.boardsServiceProvider.onAvailableBoardsChanged(
(availableBoards) => this.setState({ availableBoards })
);
}
@@ -259,7 +180,7 @@ export class BoardsToolBarItem extends React.Component<
this.toDispose.dispose();
}
private readonly show = (event: React.MouseEvent<HTMLElement>): void => {
protected readonly show = (event: React.MouseEvent<HTMLElement>): void => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
@@ -280,26 +201,31 @@ export class BoardsToolBarItem extends React.Component<
event.nativeEvent.stopImmediatePropagation();
};
private readonly hide = () => {
this.setState({ coords: 'hidden' });
};
override render(): React.ReactNode {
const { coords, boardList } = this.state;
const { boardLabel, selected, portProtocol, tooltip } = boardList.labels;
const protocolIcon = portProtocol
? iconNameFromProtocol(portProtocol)
const { coords, availableBoards } = this.state;
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"
title={tooltip}
title={selectedPortLabel}
onClick={this.show}
>
{protocolIcon && <div className={protocolIconClassNames} />}
@@ -308,22 +234,57 @@ export class BoardsToolBarItem extends React.Component<
'arduino-boards-toolbar-item--label',
'noWrapInfo',
'noselect',
{ 'arduino-boards-toolbar-item--label-connected': selected }
{ 'arduino-boards-toolbar-item--label-connected': isConnected }
)}
>
{boardLabel}
</div>
<div className="fa fa-caret-down caret" />
</div>
<BoardListDropDown
<BoardsDropDown
coords={coords}
boardList={boardList}
openBoardsConfig={() => boardList.edit({ query: '' })}
hide={this.hide}
/>
items={availableBoards
.filter(AvailableBoard.hasPort)
.map((board) => ({
...board,
onClick: () => {
if (!board.fqbn) {
const previousBoardConfig =
this.props.boardsServiceProvider.boardsConfig;
this.props.boardsServiceProvider.boardsConfig = {
selectedPort: board.port,
};
this.openDialog(previousBoardConfig);
} else {
this.props.boardsServiceProvider.boardsConfig = {
selectedBoard: board,
selectedPort: board.port,
};
}
this.setState({ coords: 'hidden' });
},
}))}
openBoardsConfig={this.openDialog}
></BoardsDropDown>
</React.Fragment>
);
}
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 {
@@ -332,7 +293,7 @@ export namespace BoardsToolBarItem {
}
export interface State {
boardList: BoardListUI;
availableBoards: AvailableBoard[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
@@ -343,10 +304,19 @@ function iconNameFromProtocol(protocol: string): string {
return 'fa-arduino-technology-usb';
case 'network':
return 'fa-arduino-technology-connection';
// it is fine to assign dedicated icons to the protocols used by the official boards,
// but other than that it is best to avoid implementing any special handling
// for specific protocols in the IDE codebase.
/*
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');
}

View File

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

View File

@@ -1,102 +0,0 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { deepClone } from '@theia/core/lib/common/objects';
import URI from '@theia/core/lib/common/uri';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { ConfigService, ConfigState } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
@injectable()
export class ConfigServiceClient implements FrontendApplicationContribution {
@inject(ConfigService)
private readonly delegate: ConfigService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(MessageService)
private readonly messageService: MessageService;
private readonly didChangeSketchDirUriEmitter = new Emitter<
URI | undefined
>();
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
private readonly toDispose = new DisposableCollection(
this.didChangeSketchDirUriEmitter,
this.didChangeDataDirUriEmitter
);
private config: ConfigState | undefined;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(async () => {
const config = await this.delegate.getConfiguration();
this.use(config);
});
}
onStart(): void {
this.notificationCenter.onConfigDidChange((config) => this.use(config));
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSketchDirUri(): Event<URI | undefined> {
return this.didChangeSketchDirUriEmitter.event;
}
get onDidChangeDataDirUri(): Event<URI | undefined> {
return this.didChangeDataDirUriEmitter.event;
}
/**
* CLI config related error messages if any.
*/
tryGetMessages(): string[] | undefined {
return this.config?.messages;
}
/**
* `directories.user`
*/
tryGetSketchDirUri(): URI | undefined {
return this.config?.config?.sketchDirUri
? new URI(this.config?.config?.sketchDirUri)
: undefined;
}
/**
* `directories.data`
*/
tryGetDataDirUri(): URI | undefined {
return this.config?.config?.dataDirUri
? new URI(this.config?.config?.dataDirUri)
: undefined;
}
private use(config: ConfigState): void {
const oldConfig = deepClone(this.config);
this.config = config;
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
}
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
}
if (this.config.messages?.length) {
const message = this.config.messages.join(' ');
// toast the error later otherwise it might not show up in IDE2
setTimeout(() => this.messageService.error(message), 1_000);
}
}
}

View File

@@ -1,24 +1,26 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as moment from 'moment';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { nls } from '@theia/core/lib/common/nls';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { inject, injectable } from '@theia/core/shared/inversify';
import moment from 'moment';
import { AppService } from '../app-service';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Command,
CommandRegistry,
Contribution,
Command,
MenuModelRegistry,
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ConfigService } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable()
export class About extends Contribution {
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
@inject(AppService)
private readonly appService: AppService;
protected readonly clipboardService: ClipboardService;
@inject(ConfigService)
protected readonly configService: ConfigService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, {
@@ -38,18 +40,23 @@ export class About extends Contribution {
});
}
private async showAbout(): Promise<void> {
const appInfo = await this.appService.info();
const { appVersion, cliVersion, buildDate } = appInfo;
async showAbout(): Promise<void> {
const {
version,
commit,
status: cliStatus,
} = await this.configService.getVersion();
const buildDate = this.buildDate;
const detail = (showAll: boolean) =>
nls.localize(
'arduino/about/detail',
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
appVersion,
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}',
remote.app.getVersion(),
buildDate ? buildDate : nls.localize('', 'dev build'),
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
cliVersion,
version,
cliStatus ? ` ${cliStatus}` : '',
commit,
nls.localize(
'arduino/about/copyright',
'Copyright © {0} Arduino SA',
@@ -59,7 +66,9 @@ export class About extends Contribution {
const ok = nls.localize('vscode/issueMainService/ok', 'OK');
const copy = nls.localize('vscode/textInputActions/copy', 'Copy');
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
const { response } = await this.dialogService.showMessageBox({
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: `${this.applicationName}`,
title: `${this.applicationName}`,
type: 'info',
@@ -68,18 +77,23 @@ export class About extends Contribution {
noLink: true,
defaultId: buttons.indexOf(ok),
cancelId: buttons.indexOf(ok),
});
}
);
if (buttons[response] === copy) {
await this.clipboardService.writeText(detail(false).trim());
}
}
private get applicationName(): string {
protected get applicationName(): string {
return FrontendApplicationConfigProvider.get().applicationName;
}
private ago(isoTime: string): string {
protected get buildDate(): string | undefined {
return FrontendApplicationConfigProvider.get().buildDate;
}
protected ago(isoTime: string): string {
const now = moment(Date.now());
const other = moment(isoTime);
let result = now.diff(other, 'minute');

View File

@@ -1,155 +0,0 @@
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MenuPath } from '@theia/core/lib/common/menu';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
import { CreateFeatures } from '../create/create-features';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import {
Command,
CommandRegistry,
Contribution,
MenuModelRegistry,
} from './contribution';
export const accountMenu: SidebarMenu = {
id: 'arduino-accounts-menu',
iconClass: 'codicon codicon-account',
title: nls.localize('arduino/account/menuTitle', 'Arduino Cloud'),
menuPath: ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT,
order: 0,
};
@injectable()
export class Account extends Contribution {
@inject(WindowService)
private readonly windowService: WindowService;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private readonly toDispose = new DisposableCollection();
private app: FrontendApplication;
override onStart(app: FrontendApplication): void {
this.app = app;
this.updateSidebarCommand();
this.toDispose.push(
this.createFeatures.onDidChangeEnabled((enabled) =>
this.updateSidebarCommand(enabled)
)
);
}
onStop(): void {
this.toDispose.dispose();
}
override registerCommands(registry: CommandRegistry): void {
const openExternal = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
const loggedIn = () => Boolean(this.createFeatures.session);
const loggedInWithInternetConnection = () =>
loggedIn() && this.connectionStatus.offlineStatus !== 'internet';
registry.registerCommand(Account.Commands.LEARN_MORE, {
execute: () => openExternal(LEARN_MORE_URL),
isEnabled: () => !loggedIn(),
isVisible: () => !loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
execute: () => openExternal('https://id.arduino.cc/'),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
execute: () => openExternal('https://create.arduino.cc/editor'),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
execute: () => openExternal('https://create.arduino.cc/iot/'),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
}
override registerMenus(registry: MenuModelRegistry): void {
const register = (
menuPath: MenuPath,
...commands: (Command | [command: Command, menuLabel: string])[]
) =>
commands.forEach((command, index) => {
const commandId = Array.isArray(command) ? command[0].id : command.id;
const label = Array.isArray(command) ? command[1] : command.label;
registry.registerMenuAction(menuPath, {
label,
commandId,
order: String(index),
});
});
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP, [
CloudUserCommands.LOGIN,
nls.localize('arduino/cloud/signInToCloud', 'Sign in to Arduino Cloud'),
]);
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP, [
Account.Commands.LEARN_MORE,
nls.localize('arduino/cloud/learnMore', 'Learn more'),
]);
register(
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP,
[
Account.Commands.GO_TO_PROFILE,
nls.localize('arduino/account/goToProfile', 'Go to Profile'),
],
[
Account.Commands.GO_TO_CLOUD_EDITOR,
nls.localize('arduino/account/goToCloudEditor', 'Go to Cloud Editor'),
],
[
Account.Commands.GO_TO_IOT_CLOUD,
nls.localize('arduino/account/goToIoTCloud', 'Go to IoT Cloud'),
]
);
register(
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP,
CloudUserCommands.LOGOUT
);
}
private updateSidebarCommand(
visible: boolean = this.preferences['arduino.cloud.enabled']
): void {
if (!this.app) {
return;
}
const handler = this.app.shell.leftPanelHandler;
if (visible) {
handler.addBottomMenu(accountMenu);
} else {
handler.removeBottomMenu(accountMenu.id);
}
}
}
export namespace Account {
export namespace Commands {
export const GO_TO_PROFILE: Command = {
id: 'arduino-go-to-profile',
};
export const GO_TO_CLOUD_EDITOR: Command = {
id: 'arduino-go-to-cloud-editor',
};
export const GO_TO_IOT_CLOUD: Command = {
id: 'arduino-go-to-iot-cloud',
};
export const LEARN_MORE: Command = {
id: 'arduino-learn-more',
};
}
}

View File

@@ -1,21 +1,21 @@
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import {
SketchContribution,
Command,
CommandRegistry,
MenuModelRegistry,
Sketch,
SketchContribution,
URI,
} from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
private readonly fileDialogService: FileDialogService; // TODO: use dialogService
private readonly fileDialogService: FileDialogService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
@@ -46,10 +46,12 @@ export class AddFile extends SketchContribution {
if (!toAddUri) {
return;
}
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await this.dialogService.showMessageBox({
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: nls.localize('arduino/contributions/replaceTitle', 'Replace'),
buttons: [
@@ -78,22 +80,6 @@ export class AddFile extends SketchContribution {
}
);
}
// https://github.com/arduino/arduino-ide/issues/284#issuecomment-1364533662
// File the file to add has one of the following extension, it goes to the sketch folder root: .ino, .h, .cpp, .c, .S
// Otherwise, the files goes to the `data` folder inside the sketch folder root.
private resolveTarget(
sketch: Sketch,
toAddUri: URI
): { uri: URI; filename: string } {
const path = toAddUri.path;
const filename = path.base;
let root = new URI(sketch.uri);
if (!Sketch.Extensions.CODE_FILES.includes(path.ext)) {
root = root.resolve('data');
}
return { uri: root.resolve(filename), filename: filename };
}
}
export namespace AddFile {

View File

@@ -1,6 +1,8 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus';
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
@@ -14,6 +16,9 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
private readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
@@ -41,7 +46,9 @@ export class AddZipLibrary extends SketchContribution {
private async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await this.dialogService.showOpenDialog({
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/selectZip',
"Select a zip file containing the library you'd like to add"
@@ -54,7 +61,8 @@ export class AddZipLibrary extends SketchContribution {
extensions: ['zip'],
},
],
});
}
);
if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try {

View File

@@ -1,5 +1,7 @@
import { injectable } from '@theia/core/shared/inversify';
import dateFormat from 'dateformat';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
SketchContribution,
@@ -8,7 +10,7 @@ import {
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class ArchiveSketch extends SketchContribution {
@@ -27,7 +29,10 @@ export class ArchiveSketch extends SketchContribution {
}
private async archiveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) {
return;
}
@@ -35,16 +40,19 @@ export class ArchiveSketch extends SketchContribution {
new Date(),
'yymmdd'
)}a.zip`;
const defaultContainerUri = await this.defaultUri();
const defaultUri = defaultContainerUri.resolve(archiveBasename);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await this.dialogService.showSaveDialog({
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
});
}
);
if (!filePath || canceled) {
return;
}
@@ -52,7 +60,7 @@ export class ArchiveSketch extends SketchContribution {
if (!destinationUri) {
return;
}
await this.sketchesService.archive(sketch, destinationUri.toString());
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(
nls.localize(
'arduino/sketch/createdArchive',

View File

@@ -1,111 +1,133 @@
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
import type { MenuPath } from '@theia/core/lib/common/menu/menu-types';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MainMenuManager } from '../../common/main-menu-manager';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
BoardsService,
BoardWithPackage,
createPlatformIdentifier,
getBoardInfo,
InstalledBoardWithPackage,
platformIdentifierEquals,
Port,
serializePlatformIdentifier,
} from '../../common/protocol';
import type { BoardList } from '../../common/protocol/board-list';
DisposableCollection,
Disposable,
} from '@theia/core/lib/common/disposable';
import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
ArduinoMenus,
PlaceholderMenuNode,
unregisterSubmenu,
} from '../menu/arduino-menus';
import { NotificationCenter } from '../notification-center';
import { Command, CommandRegistry, SketchContribution } from './contribution';
import {
BoardsService,
InstalledBoardWithPackage,
AvailablePorts,
Port,
} from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardSelection extends SketchContribution {
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry)
private readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly commandRegistry: CommandRegistry;
private readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
// do not query installed platforms on every change
private _installedBoards: Deferred<InstalledBoardWithPackage[]> | undefined;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const boardInfo = await getBoardInfo(
this.boardsServiceProvider.boardList
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) {
this.messageService.info(
nls.localize(
'arduino/board/selectBoardForInfo',
'Please select a board to obtain board info.'
)
);
if (typeof boardInfo === 'string') {
this.messageService.info(boardInfo);
return;
}
const { BN, VID, PID, SN } = boardInfo;
const detail = `
BN: ${BN}
if (!selectedBoard.fqbn) {
this.messageService.info(
nls.localize(
'arduino/board/platformMissing',
"The platform for the selected '{0}' board is not installed.",
selectedBoard.name
)
);
return;
}
if (!selectedPort) {
this.messageService.info(
nls.localize(
'arduino/board/selectPortForInfo',
'Please select a port to obtain board info.'
)
);
return;
}
const boardDetails = await this.boardsService.getBoardDetails({
fqbn: selectedBoard.fqbn,
});
if (boardDetails) {
const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name}
VID: ${VID}
PID: ${PID}
SN: ${SN}
`.trim();
await this.dialogService.showMessageBox({
PID: ${PID}`;
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info',
detail,
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
});
}
},
});
}
override onStart(): void {
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus(true));
this.notificationCenter.onPlatformDidUninstall(() =>
this.updateMenus(true)
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus());
this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus());
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
this.updateMenus()
);
this.boardsServiceProvider.onAvailablePortsChanged(() =>
this.updateMenus()
);
this.boardsServiceProvider.onBoardListDidChange(() => this.updateMenus());
}
override async onReady(): Promise<void> {
this.updateMenus();
}
private async updateMenus(discardCache = false): Promise<void> {
if (discardCache) {
this._installedBoards?.reject();
this._installedBoards = undefined;
}
if (!this._installedBoards) {
this._installedBoards = new Deferred();
this.installedBoards().then((installedBoards) =>
this._installedBoards?.resolve(installedBoards)
);
}
const installedBoards = await this._installedBoards.promise;
this.rebuildMenus(installedBoards, this.boardsServiceProvider.boardList);
protected async updateMenus(): Promise<void> {
const [installedBoards, availablePorts, config] = await Promise.all([
this.installedBoards(),
this.boardsService.getState(),
this.boardsServiceProvider.boardsConfig,
]);
this.rebuildMenus(installedBoards, availablePorts, config);
}
private rebuildMenus(
protected rebuildMenus(
installedBoards: InstalledBoardWithPackage[],
boardList: BoardList
availablePorts: AvailablePorts,
config: BoardsConfig.Config
): void {
this.toDisposeBeforeMenuRebuild.dispose();
@@ -114,8 +136,7 @@ SN: ${SN}
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'1_boards',
];
const { selectedBoard, selectedPort } = boardList.boardsConfig;
const boardsSubmenuLabel = selectedBoard?.name;
const boardsSubmenuLabel = config.selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
@@ -134,8 +155,11 @@ SN: ${SN}
);
// Ports submenu
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
const portsSubmenuLabel = selectedPort?.address;
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
nls.localize(
@@ -174,85 +198,45 @@ SN: ${SN}
label: `${BoardsListWidget.WIDGET_LABEL}...`,
});
const selectedBoardPlatformId = selectedBoard
? createPlatformIdentifier(selectedBoard)
: undefined;
// Keys are the vendor IDs
type BoardsPerVendor = Record<string, BoardWithPackage[]>;
// Group boards by their platform names. The keys are the platform names as menu labels.
// If there is a platform name (menu label) collision, refine the menu label with the vendor ID.
const groupedBoards = new Map<string, BoardsPerVendor>();
for (const board of installedBoards) {
const { packageId, packageName } = board;
const { vendorId } = packageId;
let boardsPerPackageName = groupedBoards.get(packageName);
if (!boardsPerPackageName) {
boardsPerPackageName = {} as BoardsPerVendor;
groupedBoards.set(packageName, boardsPerPackageName);
}
let boardPerVendor: BoardWithPackage[] | undefined =
boardsPerPackageName[vendorId];
if (!boardPerVendor) {
boardPerVendor = [];
boardsPerPackageName[vendorId] = boardPerVendor;
}
boardPerVendor.push(board);
}
// Installed boards
Array.from(groupedBoards.entries()).forEach(
([packageName, boardsPerPackage]) => {
const useVendorSuffix = Object.keys(boardsPerPackage).length > 1;
Object.entries(boardsPerPackage).forEach(([vendorId, boards]) => {
let platformMenuPath: MenuPath | undefined = undefined;
boards.forEach((board, index) => {
const { packageId, fqbn, name, manuallyInstalled } = board;
// create the platform submenu once.
// creating and registering the same submenu twice in Theia is a noop, though.
if (!platformMenuPath) {
let packageLabel =
installedBoards.forEach((board, index) => {
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
const packageLabel =
packageName +
`${
manuallyInstalled
? nls.localize(
'arduino/board/inSketchbook',
' (in Sketchbook)'
)
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
}`;
if (
selectedBoardPlatformId &&
platformIdentifierEquals(packageId, selectedBoardPlatformId)
) {
packageLabel = `${packageLabel}`;
}
if (useVendorSuffix) {
packageLabel += ` (${vendorId})`;
}
// Platform submenu
platformMenuPath = [
...boardsPackagesGroup,
serializePlatformIdentifier(packageId),
];
this.menuModelRegistry.registerSubmenu(
platformMenuPath,
packageLabel,
{
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel, {
order: packageName.toLowerCase(),
}
);
}
});
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () =>
this.boardsServiceProvider.updateConfig({
name: name,
fqbn: fqbn,
}),
isToggled: () => fqbn === selectedBoard?.fqbn,
execute: () => {
if (
fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: {
name,
fqbn,
port: this.boardsServiceProvider.boardsConfig.selectedBoard
?.port, // TODO: verify!
},
selectedPort:
this.boardsServiceProvider.boardsConfig.selectedPort,
};
}
},
isToggled: () =>
fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
};
// Board menu
@@ -263,27 +247,20 @@ SN: ${SN}
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(
platformMenuPath,
menuAction
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
);
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
});
});
}
);
// Detected ports
// Installed ports
const registerPorts = (
protocol: string,
ports: ReturnType<BoardList['ports']>,
protocolOrder: number
protocolOrder: number,
ports: AvailablePorts
) => {
if (!ports.length) {
const portIDs = Object.keys(ports);
if (!portIDs.length) {
return;
}
@@ -308,26 +285,46 @@ SN: ${SN}
)
);
for (let i = 0; i < ports.length; i++) {
const { port, boards } = ports[i];
const portKey = Port.keyOf(port);
// First we show addresses with recognized boards connected,
// then all the rest.
const sortedIDs = Object.keys(ports).sort(
(left: string, right: string): number => {
const [, leftBoards] = ports[left];
const [, rightBoards] = ports[right];
return rightBoards.length - leftBoards.length;
}
);
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
const [port, boards] = ports[portID];
let label = `${port.addressLabel}`;
if (boards?.length) {
if (boards.length) {
const boardsList = boards.map((board) => board.name).join(', ');
label = `${label} (${boardsList})`;
}
const id = `arduino-select-port--${portKey}`;
const id = `arduino-select-port--${portID}`;
const command = { id };
const handler = {
execute: () => {
this.boardsServiceProvider.updateConfig({
protocol: port.protocol,
address: port.address,
});
},
isToggled: () => {
return i === ports.matchingIndex;
if (
!Port.sameAs(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
)
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard:
this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port,
};
}
},
isToggled: () =>
Port.sameAs(
port,
this.boardsServiceProvider.boardsConfig.selectedPort
),
};
const menuAction = {
commandId: id,
@@ -344,12 +341,22 @@ SN: ${SN}
}
};
const groupedPorts = boardList.portsGroupedByProtocol();
const grouped = AvailablePorts.groupByProtocol(availablePorts);
let protocolOrder = 100;
Object.entries(groupedPorts).forEach(([protocol, ports]) => {
registerPorts(protocol, ports, protocolOrder);
protocolOrder += 100;
// We first show serial and network ports, then all the rest
['serial', 'network'].forEach((protocol) => {
const ports = grouped.get(protocol);
if (ports) {
registerPorts(protocol, protocolOrder, ports);
grouped.delete(protocol);
protocolOrder = protocolOrder + 100;
}
});
grouped.forEach((ports, protocol) => {
registerPorts(protocol, protocolOrder, ports);
protocolOrder = protocolOrder + 100;
});
this.mainMenuManager.update();
}

View File

@@ -1,26 +1,26 @@
import { Dialog } from '@theia/core/lib/browser/dialogs';
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 type { MaybePromise } from '@theia/core/lib/common/types';
import type {
FrontendApplication,
OnWillStopAction,
} from '@theia/core/lib/browser/frontend-application';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { nls } from '@theia/core/lib/common/nls';
import type { MaybePromise } from '@theia/core/lib/common/types';
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { WindowServiceExt } from '../theia/core/window-service-ext';
import {
SketchContribution,
Command,
CommandRegistry,
KeybindingRegistry,
MenuModelRegistry,
KeybindingRegistry,
Sketch,
SketchContribution,
URI,
} from './contribution';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch';
/**
@@ -28,9 +28,6 @@ import { SaveAsSketch } from './save-as-sketch';
*/
@injectable()
export class Close extends SketchContribution {
@inject(WindowServiceExt)
private readonly windowServiceExt: WindowServiceExt;
private shell: ApplicationShell | undefined;
override onStart(app: FrontendApplication): MaybePromise<void> {
@@ -59,7 +56,7 @@ export class Close extends SketchContribution {
}
}
}
return this.windowServiceExt.close();
return remote.getCurrentWindow().close();
},
});
}
@@ -153,7 +150,9 @@ export class Close extends SketchContribution {
}
private async prompt(isTemp: boolean): Promise<Prompt> {
const { response } = await this.dialogService.showMessageBox({
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize(
'arduino/sketch/saveSketch',
'Save your sketch to open it again later.'
@@ -169,7 +168,8 @@ export class Close extends SketchContribution {
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
],
defaultId: 2, // `Save`/`Save As...` button index is the default.
});
}
);
switch (response) {
case 0:
return Prompt.DoNotSave;
@@ -185,7 +185,7 @@ export class Close extends SketchContribution {
private async isCurrentSketchTemp(): Promise<false | Sketch> {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
const isTemp = await this.sketchesService.isTemp(currentSketch);
const isTemp = await this.sketchService.isTemp(currentSketch);
if (isTemp) {
return currentSketch;
}

View File

@@ -1,121 +0,0 @@
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CreateApi } from '../create/create-api';
import { CreateFeatures } from '../create/create-features';
import { CreateUri } from '../create/create-uri';
import { Create, isNotFound } from '../create/typings';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { SketchContribution } from './contribution';
export function sketchAlreadyExists(input: string): string {
return nls.localize(
'arduino/cloudSketch/alreadyExists',
"Cloud sketch '{0}' already exists.",
input
);
}
export function sketchNotFound(input: string): string {
return nls.localize(
'arduino/cloudSketch/notFound',
"Could not pull the cloud sketch '{0}'. It does not exist.",
input
);
}
export const synchronizingSketchbook = nls.localize(
'arduino/cloudSketch/synchronizingSketchbook',
'Synchronizing sketchbook...'
);
export function pullingSketch(input: string): string {
return nls.localize(
'arduino/cloudSketch/pulling',
"Synchronizing sketchbook, pulling '{0}'...",
input
);
}
export function pushingSketch(input: string): string {
return nls.localize(
'arduino/cloudSketch/pushing',
"Synchronizing sketchbook, pushing '{0}'...",
input
);
}
@injectable()
export abstract class CloudSketchContribution extends SketchContribution {
@inject(SketchbookWidgetContribution)
private readonly widgetContribution: SketchbookWidgetContribution;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(CreateFeatures)
protected readonly createFeatures: CreateFeatures;
protected async treeModel(): Promise<
(CloudSketchbookTreeModel & { root: CompositeTreeNode }) | undefined
> {
const { enabled, session } = this.createFeatures;
if (enabled && session) {
const widget = await this.widgetContribution.widget;
const treeModel = this.treeModelFrom(widget);
if (treeModel) {
const root = treeModel.root;
if (CompositeTreeNode.is(root)) {
return treeModel as CloudSketchbookTreeModel & {
root: CompositeTreeNode;
};
}
}
}
return undefined;
}
protected async pull(
sketch: Create.Sketch
): Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
const treeModel = await this.treeModel();
if (!treeModel) {
return undefined;
}
const id = CreateUri.toUri(sketch).path.toString();
const node = treeModel.getNode(id);
if (!node) {
throw new Error(
`Could not find cloud sketchbook tree node with ID: ${id}.`
);
}
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
throw new Error(
`Cloud sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
);
}
try {
await treeModel.sketchbookTree().pull({ node }, true);
return node;
} catch (err) {
if (isNotFound(err)) {
await treeModel.refresh();
this.messageService.error(sketchNotFound(sketch.name));
return undefined;
}
throw err;
}
}
private treeModelFrom(
widget: SketchbookWidget
): CloudSketchbookTreeModel | undefined {
for (const treeWidget of widget.getTreeWidgets()) {
if (treeWidget instanceof CloudSketchbookTreeWidget) {
const model = treeWidget.model;
if (model instanceof CloudSketchbookTreeModel) {
return model;
}
}
}
return undefined;
}
}

View File

@@ -6,18 +6,14 @@ import {
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import {
MenuModelRegistry,
MenuContribution,
@@ -44,9 +40,10 @@ import { SettingsService } from '../dialogs/settings/settings';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../sketches-service-client-impl';
} from '../../common/protocol/sketches-service-client-impl';
import {
SketchesService,
ConfigService,
FileSystemExt,
Sketch,
CoreService,
@@ -61,14 +58,9 @@ 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/lib/browser/notifications-manager';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ConfigServiceClient } from '../config/config-service-client';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { DialogService } from '../dialog-service';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
export {
Command,
@@ -114,12 +106,6 @@ export abstract class Contribution
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(DialogService)
protected readonly dialogService: DialogService;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady());
@@ -152,11 +138,11 @@ export abstract class SketchContribution extends Contribution {
@inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigServiceClient)
protected readonly configService: ConfigServiceClient;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
protected readonly sketchService: SketchesService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@@ -170,12 +156,6 @@ export abstract class SketchContribution extends Contribution {
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
@inject(ApplicationConnectionStatusContribution)
protected readonly connectionStatusService: ApplicationConnectionStatusContribution;
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
@@ -189,25 +169,6 @@ export abstract class SketchContribution extends Contribution {
}
return override;
}
/**
* Defaults to `directories.user` if defined and not CLI config errors were detected.
* Otherwise, the URI of the user home directory.
*/
protected async defaultUri(): Promise<URI> {
const errors = this.configService.tryGetMessages();
let defaultUri = this.configService.tryGetSketchDirUri();
if (!defaultUri || errors?.length) {
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
}
return defaultUri;
}
protected async defaultPath(): Promise<string> {
const defaultUri = await this.defaultUri();
return this.fileService.fsPath(defaultUri);
}
}
@injectable()
@@ -230,9 +191,6 @@ export abstract class CoreServiceContribution extends SketchContribution {
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
/**
* 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.)
@@ -295,9 +253,6 @@ export abstract class CoreServiceContribution extends SketchContribution {
keepOutput?: boolean;
task: (progressId: string, coreService: CoreService) => Promise<T>;
}): Promise<T> {
const toDisposeOnComplete = new DisposableCollection(
this.maybeActivateMonitorWidget()
);
const { progressText, keepOutput, task } = options;
this.outputChannelManager
.getChannel('Arduino')
@@ -309,26 +264,11 @@ export abstract class CoreServiceContribution extends SketchContribution {
run: ({ progressId }) => task(progressId, this.coreService),
keepOutput,
});
toDisposeOnComplete.dispose();
return result;
}
// TODO: cleanup!
// this dependency does not belong here
// support core command contribution handlers, the monitor-widget should implement it and register itself as a handler
// the monitor widget should reveal itself after a successful core command execution
private maybeActivateMonitorWidget(): Disposable {
const currentWidget = this.shell.bottomPanel.currentTitle?.owner;
if (currentWidget?.id === 'serial-monitor') {
return Disposable.create(() =>
this.shell.bottomPanel.activateWidget(currentWidget)
);
}
return Disposable.NULL;
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager['getMessageId']({
return this.notificationManager.getMessageId({
text: message,
actions,
type: MessageType.Error,

View File

@@ -1,118 +0,0 @@
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ApplicationShell } from '@theia/core/lib/browser/shell';
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Create } from '../create/typings';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
import {
CreateNewCloudSketchCallback,
NewCloudSketch,
NewCloudSketchParams,
} from './new-cloud-sketch';
import { saveOntoCopiedSketch } from './save-as-sketch';
interface CreateCloudCopyParams {
readonly model: SketchbookTreeModel;
readonly node: SketchbookTree.SketchDirNode;
}
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
return (
typeof arg === 'object' &&
(<CreateCloudCopyParams>arg).model !== undefined &&
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
(<CreateCloudCopyParams>arg).node !== undefined &&
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
);
}
@injectable()
export class CreateCloudCopy extends CloudSketchContribution {
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private shell: ApplicationShell;
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
isEnabled: (args: unknown) =>
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
isVisible: (args: unknown) =>
Boolean(this.createFeatures.enabled) &&
Boolean(this.createFeatures.session) &&
this.connectionStatus.offlineStatus !== 'internet' &&
isCreateCloudCopyParams(args),
});
}
/**
* - creates new cloud sketch with the name of the params sketch,
* - pulls the cloud sketch,
* - copies files from params sketch to pulled cloud sketch in the cache folder,
* - pushes the cloud sketch, and
* - opens in new window.
*/
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
const sketch = await this.sketchesService.loadSketch(
params.node.fileStat.resource.toString()
);
const callback: CreateNewCloudSketchCallback = async (
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
) => {
const treeModel = await this.treeModel();
if (!treeModel) {
throw new Error('Could not retrieve the cloud sketchbook tree model.');
}
progress.report({
message: nls.localize(
'arduino/createCloudCopy/copyingSketchFilesMessage',
'Copying local sketch files...'
),
});
const localCacheFolderUri = newNode.uri.toString();
await this.sketchesService.copy(sketch, {
destinationUri: localCacheFolderUri,
onlySketchFiles: true,
});
await saveOntoCopiedSketch(
sketch,
localCacheFolderUri,
this.shell,
this.editorManager
);
progress.report({ message: pushingSketch(newSketch.name) });
await treeModel.sketchbookTree().push(newNode, true, true);
};
return this.commandService.executeCommand(
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
<NewCloudSketchParams>{
initialValue: params.node.fileStat.name,
callback,
skipShowErrorMessageOnOpen: false,
}
);
}
}
export namespace CreateCloudCopy {
export namespace Commands {
export const CREATE_CLOUD_COPY: Command = {
id: 'arduino-create-cloud-copy',
iconClass: 'fa fa-arduino-cloud-upload',
};
}
}

View File

@@ -3,14 +3,7 @@ import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { NotificationCenter } from '../notification-center';
import {
Board,
BoardIdentifier,
BoardsService,
ExecutableService,
isBoardIdentifierChangeEvent,
Sketch,
} from '../../common/protocol';
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
URI,
@@ -20,11 +13,12 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { ArduinoMenus } from '../menu/arduino-menus';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
import { MainMenuManager } from '../../common/main-menu-manager';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
@injectable()
export class Debug extends SketchContribution {
@inject(HostedPluginSupport)
@@ -42,6 +36,9 @@ export class Debug extends SketchContribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
/**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/
@@ -90,11 +87,9 @@ export class Debug extends SketchContribution {
: Debug.Commands.START_DEBUGGING.label
}`)
);
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.refreshState(event.selectedBoard);
}
});
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
this.refreshState(selectedBoard)
);
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
}
@@ -173,7 +168,7 @@ export class Debug extends SketchContribution {
}
private async startDebug(
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
@@ -191,7 +186,7 @@ export class Debug extends SketchContribution {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, configPath] = await Promise.all([
@@ -208,28 +203,7 @@ export class Debug extends SketchContribution {
sketchPath,
configPath,
};
try {
await this.commandService.executeCommand('arduino.debug.start', config);
} catch (err) {
if (await this.isSketchNotVerifiedError(err, sketch)) {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const answer = await this.messageService.error(
nls.localize(
'arduino/debug/sketchIsNotCompiled',
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
sketch.name
),
yes
);
if (answer === yes) {
this.commandService.executeCommand('arduino-verify-sketch');
}
} else {
this.messageService.error(
err instanceof Error ? err.message : String(err)
);
}
}
return this.commandService.executeCommand('arduino.debug.start', config);
}
get compileForDebug(): boolean {
@@ -241,24 +215,7 @@ export class Debug extends SketchContribution {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
this.menuManager.update();
}
private async isSketchNotVerifiedError(
err: unknown,
sketch: Sketch
): Promise<boolean> {
if (err instanceof Error) {
try {
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
return tempBuildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath)
);
} catch {
return false;
}
}
return false;
this.mainMenuManager.update();
}
}
export namespace Debug {

View File

@@ -1,131 +1,32 @@
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common/nls';
import type { MaybeArray } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { injectable } from '@theia/core/shared/inversify';
import { SketchesError } from '../../common/protocol';
import { Sketch } from '../contributions/contribution';
import { isNotFound } from '../create/typings';
import { Command, CommandRegistry } from './contribution';
import { CloudSketchContribution } from './cloud-contribution';
import { AppService } from '../app-service';
export interface DeleteSketchParams {
/**
* Either the URI of the sketch folder or the sketch to delete.
*/
readonly toDelete: string | Sketch;
/**
* If `true`, the currently opened sketch is expected to be deleted.
* Hence, the editors must be closed, the sketch will be scheduled
* for deletion, and the browser window will close or navigate away.
* If `false`, the sketch will be scheduled for deletion,
* but the current window remains open. If `force`, the window will
* navigate away, but IDE2 won't open any confirmation dialogs.
*/
readonly willNavigateAway?: boolean | 'force';
}
import {
Command,
CommandRegistry,
SketchContribution,
Sketch,
} from './contribution';
@injectable()
export class DeleteSketch extends CloudSketchContribution {
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
@inject(AppService)
private readonly appService: AppService;
export class DeleteSketch extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
execute: (params: DeleteSketchParams) => this.deleteSketch(params),
execute: (uri: string) => this.deleteSketch(uri),
});
}
private async deleteSketch(params: DeleteSketchParams): Promise<void> {
const { toDelete, willNavigateAway } = params;
let sketch: Sketch;
if (typeof toDelete === 'string') {
const resolvedSketch = await this.loadSketch(toDelete);
if (!resolvedSketch) {
console.info(
`Failed to load the sketch. It was not found at '${toDelete}'. Skipping deletion.`
);
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;
}
sketch = resolvedSketch;
} else {
sketch = toDelete;
}
if (!willNavigateAway) {
this.scheduleDeletion(sketch);
return;
}
const cloudUri = this.createFeatures.cloudUri(sketch);
if (willNavigateAway !== 'force') {
const { response } = await this.dialogService.showMessageBox({
title: nls.localizeByDefault('Delete'),
type: 'question',
buttons: [Dialog.CANCEL, Dialog.OK],
message: cloudUri
? nls.localize(
'theia/workspace/deleteCloudSketch',
"The cloud sketch '{0}' will be permanently deleted from the Arduino servers and the local caches. This action is irreversible. Do you want to delete the current sketch?",
sketch.name
)
: nls.localize(
'theia/workspace/deleteCurrentSketch',
"The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
sketch.name
),
});
// cancel
if (response === 0) {
return;
}
}
if (cloudUri) {
const posixPath = cloudUri.path.toString();
const cloudSketch = this.createApi.sketchCache.getSketch(posixPath);
if (!cloudSketch) {
throw new Error(
`Cloud sketch with path '${posixPath}' was not cached. Cache: ${this.createApi.sketchCache.toString()}`
);
}
try {
// IDE2 cannot use DELETE directory as the server responses with HTTP 500 if it's missing.
// https://github.com/arduino/arduino-ide/issues/1825#issuecomment-1406301406
await this.createApi.deleteSketch(cloudSketch.path);
} catch (err) {
if (!isNotFound(err)) {
throw err;
} else {
console.info(
`Could not delete the cloud sketch with path '${posixPath}'. It does not exist.`
);
}
}
}
await Promise.all([
...Sketch.uris(sketch).map((uri) =>
this.closeWithoutSaving(new URI(uri))
),
]);
this.windowService.setSafeToShutDown();
this.scheduleDeletion(sketch);
return window.close();
}
private scheduleDeletion(sketch: Sketch): void {
this.appService.scheduleDeletion(sketch);
return this.sketchService.deleteSketch(sketch);
}
private async loadSketch(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(uri);
const sketch = await this.sketchService.loadSketch(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
@@ -134,13 +35,6 @@ export class DeleteSketch extends CloudSketchContribution {
throw err;
}
}
// fix: https://github.com/eclipse-theia/theia/issues/12107
private async closeWithoutSaving(uri: URI): Promise<void> {
const affected = getAffected(this.shell.widgets, uri);
const toClose = [...affected].map(([, widget]) => widget);
await this.shell.closeMany(toClose, { save: false });
}
}
export namespace DeleteSketch {
export namespace Commands {
@@ -149,20 +43,3 @@ export namespace DeleteSketch {
};
}
}
function getAffected<T extends Widget>(
widgets: Iterable<T>,
context: MaybeArray<URI>
): [URI, T & NavigatableWidget][] {
const uris = Array.isArray(context) ? context : [context];
const result: [URI, T & NavigatableWidget][] = [];
for (const widget of widgets) {
if (NavigatableWidget.is(widget)) {
const resourceUri = widget.getResourceUri();
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
result.push([resourceUri, widget]);
}
}
}
return result;
}

View File

@@ -57,11 +57,9 @@ export class EditContributions extends Contribution {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`
\`\`\`cpp
this.clipboardService.writeText(`\`\`\`cpp
${value}
\`\`\`
`);
\`\`\``);
}
},
});

View File

@@ -1,17 +1,18 @@
import PQueue from 'p-queue';
import * as PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
import { MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
import { CommandHandler } from '@theia/core/lib/common/command';
import {
MenuPath,
CompositeMenuNode,
SubMenuOptions,
} from '@theia/core/lib/common/menu';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import {
ArduinoMenus,
examplesLabel,
PlaceholderMenuNode,
} from '../menu/arduino-menus';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService } from '../../common/protocol/examples-service';
import {
@@ -25,75 +26,10 @@ import {
SketchRef,
SketchContainer,
SketchesError,
CoreService,
SketchesService,
Sketch,
isBoardIdentifierChangeEvent,
BoardIdentifier,
CoreService,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
import { unregisterSubmenu } from '../menu/arduino-menus';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ApplicationError } from '@theia/core/lib/common/application-error';
/**
* Creates a cloned copy of the example sketch and opens it in a new window.
*/
export async function openClonedExample(
uri: string,
services: {
sketchesService: SketchesService;
commandService: CommandService;
},
onError: {
onDidFailClone?: (
err: ApplicationError<
number,
{
uri: string;
}
>,
uri: string
) => MaybePromise<unknown>;
onDidFailOpen?: (
err: ApplicationError<
number,
{
uri: string;
}
>,
sketch: Sketch
) => MaybePromise<unknown>;
} = {}
): Promise<void> {
const { sketchesService, commandService } = services;
const { onDidFailClone, onDidFailOpen } = onError;
try {
const sketch = await sketchesService.cloneExample(uri);
try {
await commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (openError) {
if (SketchesError.NotFound.is(openError)) {
if (onDidFailOpen) {
await onDidFailOpen(openError, sketch);
return;
}
}
throw openError;
}
} catch (cloneError) {
if (SketchesError.NotFound.is(cloneError)) {
if (onDidFailClone) {
await onDidFailClone(cloneError, uri);
return;
}
}
throw cloneError;
}
}
import { nls } from '@theia/core/lib/common';
@injectable()
export abstract class Examples extends SketchContribution {
@@ -101,7 +37,10 @@ export abstract class Examples extends SketchContribution {
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@@ -110,25 +49,14 @@ export abstract class Examples extends SketchContribution {
protected readonly coreService: CoreService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly toDispose = new DisposableCollection();
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.handleBoardChanged(event.selectedBoard);
}
});
this.notificationCenter.onDidReinitialize(() =>
this.update({
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
// No force refresh. The core client was already refreshed.
})
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
}
@@ -138,16 +66,29 @@ export abstract class Examples extends SketchContribution {
}
protected abstract update(options?: {
board?: BoardIdentifier | undefined;
board?: Board | undefined;
forceRefresh?: boolean;
}): void;
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
const groupPath =
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
}
// Registering the same submenu multiple times has no side-effect.
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu(
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
examplesLabel,
nls.localize('arduino/examples/menu', 'Examples'),
{
order: '4',
}
@@ -183,11 +124,6 @@ export abstract class Examples extends SketchContribution {
const { label } = sketchContainerOrPlaceholder;
submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
this.toDispose.push(
Disposable.create(() =>
unregisterSubmenu(submenuPath, this.menuRegistry)
)
);
sketches.push(...sketchContainerOrPlaceholder.sketches);
children.push(...sketchContainerOrPlaceholder.children);
} else {
@@ -227,33 +163,47 @@ export abstract class Examples extends SketchContribution {
}
protected createHandler(uri: string): CommandHandler {
const forceUpdate = () =>
this.update({
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
forceRefresh: true,
});
return {
execute: async () => {
await openClonedExample(
uri,
{
sketchesService: this.sketchesService,
commandService: this.commandRegistry,
},
{
onDidFailClone: () => {
// Do not toast the error message. It's handled by the `Open Sketch` command.
forceUpdate();
},
onDidFailOpen: (err) => {
this.messageService.error(err.message);
forceUpdate();
},
}
const sketch = await this.clone(uri);
if (sketch) {
try {
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// Do not toast the error message. It's handled by the `Open Sketch` command.
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
},
};
}
private async clone(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.cloneExample(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
}
@injectable()
@@ -293,6 +243,9 @@ export class BuiltInExamples extends Examples {
@injectable()
export class LibraryExamples extends Examples {
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
override onStart(): void {
@@ -310,7 +263,7 @@ export class LibraryExamples extends Examples {
protected override async update(
options: { board?: Board; forceRefresh?: boolean } = {
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
board: this.boardsServiceClient.boardsConfig.selectedBoard,
}
): Promise<void> {
const { board, forceRefresh } = options;

View File

@@ -1,4 +1,4 @@
import PQueue from 'p-queue';
import * as PQueue from 'p-queue';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
@@ -17,7 +17,7 @@ import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@@ -37,7 +37,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly notificationCenter: NotificationCenter;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@@ -46,14 +46,13 @@ export class IncludeLibrary extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
override onStart(): void {
this.boardsServiceProvider.onBoardsConfigDidChange(() =>
this.boardsServiceClient.onBoardsConfigChanged(() =>
this.updateMenuActions()
);
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
this.notificationCenter.onLibraryDidUninstall(() =>
this.updateMenuActions()
);
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
}
override async onReady(): Promise<void> {
@@ -98,7 +97,7 @@ export class IncludeLibrary extends SketchContribution {
this.toDispose.dispose();
this.mainMenuManager.update();
const libraries: LibraryPackage[] = [];
const fqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
// Show all libraries, when no board is selected.
// Otherwise, show libraries only for the selected board.
libraries.push(...(await this.libraryService.list({ fqbn })));

View File

@@ -1,24 +1,15 @@
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Mutex } from 'async-mutex';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
ArduinoDaemon,
assertSanitizedFqbn,
BoardIdentifier,
BoardsService,
ExecutableService,
isBoardIdentifierChangeEvent,
sanitizeFqbn,
} from '../../common/protocol';
import { CurrentSketch } from '../sketches-service-client-impl';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginEvents } from '../hosted-plugin-events';
import { NotificationCenter } from '../notification-center';
import { SketchContribution, URI } from './contribution';
import { BoardsDataStore } from '../boards/boards-data-store';
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 {
@@ -37,19 +28,12 @@ export class InoLanguage extends SketchContribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore;
private readonly toDispose = new DisposableCollection();
private readonly languageServerStartMutex = new Mutex();
private languageServerFqbn?: string;
private languageServerStartMutex = new Mutex();
override onReady(): void {
const start = (
selectedBoard: BoardIdentifier | undefined,
{ selectedBoard }: BoardsConfig.Config,
forceStart = false
) => {
if (selectedBoard) {
@@ -59,65 +43,25 @@ export class InoLanguage extends SketchContribution {
}
}
};
const forceRestart = () => {
start(this.boardsServiceProvider.boardsConfig.selectedBoard, true);
};
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
start(event.selectedBoard);
}
}),
this.boardsServiceProvider.onBoardsConfigChanged(start);
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceProvider.boardsConfig.selectedBoard)
),
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':
forceRestart();
start(this.boardsServiceProvider.boardsConfig, true);
}
}
}
),
this.notificationCenter.onLibraryDidInstall(() => forceRestart()),
this.notificationCenter.onLibraryDidUninstall(() => forceRestart()),
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onChanged((dataChangePerFqbn) => {
if (this.languageServerFqbn) {
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
if (!sanitizeFqbn) {
throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
);
}
const matchingFqbn = dataChangePerFqbn.find(
(fqbn) => sanitizedFqbn === fqbn
);
const { boardsConfig } = this.boardsServiceProvider;
if (
matchingFqbn &&
boardsConfig.selectedBoard?.fqbn === matchingFqbn
) {
start(boardsConfig.selectedBoard);
}
}
}),
]);
this.boardsServiceProvider.ready.then(() =>
start(this.boardsServiceProvider.boardsConfig.selectedBoard)
);
}
onStop(): void {
this.toDispose.dispose();
start(this.boardsServiceProvider.boardsConfig);
}
private async startLanguageServer(
@@ -130,7 +74,6 @@ export class InoLanguage extends SketchContribution {
return;
}
const release = await this.languageServerStartMutex.acquire();
const toDisposeOnRelease = new DisposableCollection();
try {
await this.hostedPluginEvents.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
@@ -158,18 +101,11 @@ export class InoLanguage extends SketchContribution {
}
return;
}
assertSanitizedFqbn(fqbn);
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) {
throw new Error(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
);
}
if (!forceStart && fqbnWithConfig === this.languageServerFqbn) {
if (!forceStart && fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbnWithConfig}`);
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.preferences.get('arduino.language.log');
const realTimeDiagnostics = this.preferences.get(
'arduino.language.realTimeDiagnostics'
@@ -190,13 +126,12 @@ export class InoLanguage extends SketchContribution {
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) => {
const timer = setTimeout(
new Promise<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
);
toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
}),
)
),
this.commandService.executeCommand<string>(
'arduino.languageserver.start',
{
@@ -206,7 +141,7 @@ export class InoLanguage extends SketchContribution {
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
board: {
fqbn: fqbnWithConfig,
fqbn,
name: name ? `"${name}"` : undefined,
},
realTimeDiagnostics,
@@ -215,10 +150,9 @@ export class InoLanguage extends SketchContribution {
),
]);
} catch (e) {
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
toDisposeOnRelease.dispose();
release();
}
}

View File

@@ -1,17 +1,31 @@
import { injectable } from '@theia/core/shared/inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Contribution,
Command,
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import {
CommandRegistry,
DisposableCollection,
MaybePromise,
nls,
} from '@theia/core/lib/common';
import { Settings } from '../dialogs/settings/settings';
import debounce from 'lodash.debounce';
import { MainMenuManager } from '../../common/main-menu-manager';
import debounce = require('lodash.debounce');
@injectable()
export class InterfaceScale extends Contribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly menuActionsDisposables = new DisposableCollection();
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
increase: true,
decrease: true,
@@ -48,22 +62,63 @@ export class InterfaceScale extends Contribution {
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
this.menuActionsDisposables.dispose();
const increaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
};
const decreaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
});
};
if (this.fontScalingEnabled.increase) {
this.menuActionsDisposables.push(
registry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction.label,
{ order: increaseFontSizeMenuAction.order }
)
)
);
}
if (this.fontScalingEnabled.decrease) {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction.label,
{ order: decreaseFontSizeMenuAction.order }
)
)
);
}
this.mainMenuManager.update();
}
private updateFontScalingEnabled(): void {
@@ -98,7 +153,7 @@ export class InterfaceScale extends Contribution {
);
if (isChanged) {
this.fontScalingEnabled = fontScalingEnabled;
this.menuManager.update();
this.registerMenus(this.menuRegistry);
}
}

View File

@@ -1,62 +1,76 @@
import { DialogError } from '@theia/core/lib/browser/dialogs';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { Create, isConflict } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Widget } from '@theia/core/lib/browser/widgets/widget';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
TaskFactoryImpl,
WorkspaceInputDialogWithProgress,
} from '../theia/workspace/workspace-input-dialog';
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
Progress,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
import { v4 } from 'uuid';
import { MainMenuManager } from '../../common/main-menu-manager';
import type { AuthenticationSession } from '../../node/auth/types';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { CreateApi } from '../create/create-api';
import { CreateUri } from '../create/create-uri';
import { Create } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import {
CloudSketchContribution,
pullingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch } from './contribution';
export interface CreateNewCloudSketchCallback {
(
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
): Promise<void>;
}
export interface NewCloudSketchParams {
/**
* Value to populate the dialog `<input>` when it opens.
*/
readonly initialValue?: string | undefined;
/**
* Additional callback to call when the new cloud sketch has been created.
*/
readonly callback?: CreateNewCloudSketchCallback;
/**
* If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`.
*/
readonly skipShowErrorMessageOnOpen?: boolean;
}
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { Command, CommandRegistry, Contribution, URI } from './contribution';
@injectable()
export class NewCloudSketch extends CloudSketchContribution {
export class NewCloudSketch extends Contribution {
@inject(CreateApi)
private readonly createApi: CreateApi;
@inject(SketchbookWidgetContribution)
private readonly widgetContribution: SketchbookWidgetContribution;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly toDispose = new DisposableCollection();
private _session: AuthenticationSession | undefined;
private _enabled: boolean;
override onReady(): void {
this.toDispose.pushAll([
this.createFeatures.onDidChangeEnabled(() => this.menuManager.update()),
this.createFeatures.onDidChangeSession(() => this.menuManager.update()),
this.authenticationService.onSessionDidChange((session) => {
const oldSession = this._session;
this._session = session;
if (!!oldSession !== !!this._session) {
this.mainMenuManager.update();
}
}),
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.cloud.enabled') {
const oldEnabled = this._enabled;
this._enabled = Boolean(newValue);
if (this._enabled !== oldEnabled) {
this.mainMenuManager.update();
}
}
}),
]);
if (this.createFeatures.session) {
this.menuManager.update();
this._enabled = this.preferences['arduino.cloud.enabled'];
this._session = this.authenticationService.session;
if (this._session) {
this.mainMenuManager.update();
}
}
@@ -66,21 +80,16 @@ export class NewCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: (params: NewCloudSketchParams) =>
this.createNewSketch(
params?.skipShowErrorMessageOnOpen === false ? false : true,
params?.initialValue,
params?.callback
),
isEnabled: () => Boolean(this.createFeatures.session),
isVisible: () => this.createFeatures.enabled,
execute: () => this.createNewSketch(),
isEnabled: () => !!this._session,
isVisible: () => this._enabled,
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
label: nls.localize('arduino/cloudSketch/new', 'New Cloud Sketch'),
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
order: '1',
});
}
@@ -93,106 +102,153 @@ export class NewCloudSketch extends CloudSketchContribution {
}
private async createNewSketch(
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const treeModel = await this.treeModel();
if (treeModel) {
const rootNode = treeModel.root;
return this.openWizard(
rootNode,
treeModel,
skipShowErrorMessageOnOpen,
initialValue,
callback
initialValue?: string | undefined
): Promise<unknown> {
const widget = await this.widgetContribution.widget;
const treeModel = this.treeModelFrom(widget);
if (!treeModel) {
return undefined;
}
const rootNode = CompositeTreeNode.is(treeModel.root)
? treeModel.root
: undefined;
if (!rootNode) {
return undefined;
}
return this.openWizard(rootNode, treeModel, initialValue);
}
private withProgress(
value: string,
treeModel: CloudSketchbookTreeModel
): (progress: Progress) => Promise<unknown> {
return async (progress: Progress) => {
let result: Create.Sketch | undefined | 'conflict';
try {
progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating remote sketch '{0}'...",
value
),
});
result = await this.createApi.createSketch(value);
} catch (err) {
if (isConflict(err)) {
result = 'conflict';
} else {
throw err;
}
} finally {
if (result) {
progress.report({
message: nls.localize(
'arduino/cloudSketch/synchronizing',
"Synchronizing sketchbook, pulling '{0}'...",
value
),
});
await treeModel.refresh();
}
}
if (result === 'conflict') {
return this.createNewSketch(value);
}
if (result) {
return this.open(treeModel, result);
}
return undefined;
};
}
private async open(
treeModel: CloudSketchbookTreeModel,
newSketch: Create.Sketch
): Promise<URI | undefined> {
const id = CreateUri.toUri(newSketch).path.toString();
const node = treeModel.getNode(id);
if (!node) {
throw new Error(
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
);
}
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
throw new Error(
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
);
}
try {
await treeModel.sketchbookTree().pull({ node });
} catch (err) {
if (isNotFound(err)) {
await treeModel.refresh();
this.messageService.error(
nls.localize(
'arduino/newCloudSketch/notFound',
"Could not pull the remote sketch '{0}'. It does not exist.",
newSketch.name
)
);
return undefined;
}
throw err;
}
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
);
}
private treeModelFrom(
widget: SketchbookWidget
): CloudSketchbookTreeModel | undefined {
const treeWidget = widget.getTreeWidget();
if (treeWidget instanceof CloudSketchbookTreeWidget) {
const model = treeWidget.model;
if (model instanceof CloudSketchbookTreeModel) {
return model;
}
}
return undefined;
}
private async openWizard(
rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
initialValue?: string | undefined
): Promise<unknown> {
const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.createNewSketchWithProgress(treeModel, value, callback)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
return new NewCloudSketchDialog(
{
title: nls.localize(
'arduino/newCloudSketch/newSketchTitle',
'Name of the new Cloud Sketch'
'Name of a new Remote Sketch'
),
parentUri: CreateUri.root,
initialValue,
validate: (input) => {
if (existingNames.includes(input)) {
return sketchAlreadyExists(input);
return nls.localize(
'arduino/newCloudSketch/sketchAlreadyExists',
"Remote sketch '{0}' already exists.",
input
);
}
return Sketch.validateCloudSketchFolderName(input) ?? '';
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
return '';
}
return nls.localize(
'arduino/newCloudSketch/invalidSketchName',
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
);
},
},
this.labelProvider,
taskFactory
);
await dialog.open(skipShowErrorMessageOnOpen);
if (dialog.taskResult) {
this.openInNewWindow(dialog.taskResult);
}
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.createNewSketch(
false,
taskFactory.value ?? initialValue,
callback
);
}
throw err;
}
}
private createNewSketchWithProgress(
treeModel: CloudSketchbookTreeModel,
value: string,
callback?: CreateNewCloudSketchCallback
): (
progress: Progress
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
return async (progress: Progress) => {
progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating cloud sketch '{0}'...",
value
),
});
const sketch = await this.createApi.createSketch(value);
progress.report({ message: synchronizingSketchbook });
await treeModel.refresh();
progress.report({ message: pullingSketch(sketch.name) });
const node = await this.pull(sketch);
if (callback && node) {
await callback(sketch, node, progress);
}
return node;
};
}
private openInNewWindow(
node: CloudSketchbookTree.CloudSketchDirNode
): Promise<void> {
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node, treeWidgetId: 'cloud-sketchbook-composite-widget' }
);
(value) => this.withProgress(value, treeModel)
).open();
}
}
export namespace NewCloudSketch {
@@ -202,3 +258,115 @@ export namespace NewCloudSketch {
};
}
}
function isConflict(err: unknown): boolean {
return isErrorWithStatusOf(err, 409);
}
function isNotFound(err: unknown): boolean {
return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
err: unknown,
status: number
): err is Error & { status: number } {
if (err instanceof Error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = err as any;
return 'status' in object && object.status === status;
}
return false;
}
@injectable()
class NewCloudSketchDialog extends WorkspaceInputDialog {
constructor(
@inject(WorkspaceInputDialogProps)
protected override readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider,
private readonly withProgress: (
value: string
) => (progress: Progress) => Promise<unknown>
) {
super(props, labelProvider);
}
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
const spinner = document.createElement('div');
spinner.classList.add('spinner');
const disposables = new DisposableCollection();
try {
this.toggleButtons(true);
disposables.push(Disposable.create(() => this.toggleButtons(false)));
const closeParent = this.closeCrossNode.parentNode;
closeParent?.removeChild(this.closeCrossNode);
disposables.push(
Disposable.create(() => {
closeParent?.appendChild(this.closeCrossNode);
})
);
this.errorMessageNode.classList.add('progress');
disposables.push(
Disposable.create(() =>
this.errorMessageNode.classList.remove('progress')
)
);
const errorParent = this.errorMessageNode.parentNode;
errorParent?.insertBefore(spinner, this.errorMessageNode);
disposables.push(
Disposable.create(() => errorParent?.removeChild(spinner))
);
const cancellationSource = new CancellationTokenSource();
const progress: Progress = {
id: v4(),
cancel: () => cancellationSource.cancel(),
report: (update: ProgressUpdate) => {
this.setProgressMessage(update);
},
result: Promise.resolve(value),
};
await this.withProgress(value)(progress);
} finally {
disposables.dispose();
}
this.resolve(value);
Widget.detach(this);
}
}
private toggleButtons(disabled: boolean): void {
if (this.acceptButton) {
this.acceptButton.disabled = disabled;
}
if (this.closeButton) {
this.closeButton.disabled = disabled;
}
}
private setProgressMessage(update: ProgressUpdate): void {
if (update.work && update.work.done === update.work.total) {
this.errorMessageNode.innerText = '';
} else {
if (update.message) {
this.errorMessageNode.innerText = update.message;
}
}
}
}

View File

@@ -35,7 +35,7 @@ export class NewSketch extends SketchContribution {
async newSketch(): Promise<void> {
try {
const sketch = await this.sketchesService.createNewSketch();
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri));
} catch (e) {
await this.messageService.error(e.toString());

View File

@@ -1,18 +1,25 @@
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { CommandRegistry } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
import { BoardsConfigDialog } from '../boards/boards-config-dialog';
import { Contribution } from './contribution';
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 (params?: EditBoardsConfigActionParams) =>
this.boardsConfigDialog.open(params),
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
return (this.boardsServiceProvider.boardsConfig = boardsConfig);
}
},
});
}
}

View File

@@ -47,7 +47,7 @@ export class OpenRecentSketch extends SketchContribution {
}
private update(forceUpdate?: boolean): void {
this.sketchesService
this.sketchService
.recentlyOpenedSketches(forceUpdate)
.then((sketches) => this.refreshMenu(sketches));
}

View File

@@ -1,4 +1,5 @@
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
@@ -8,7 +9,7 @@ import {
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common/nls';
import { nls } from '@theia/core/lib/common';
@injectable()
export class OpenSketchExternal extends SketchContribution {
@@ -40,7 +41,7 @@ export class OpenSketchExternal extends SketchContribution {
if (exists) {
const fsPath = await this.fileService.fsPath(new URI(uri));
if (fsPath) {
window.electronTheiaCore.showItemInFolder(fsPath);
remote.shell.showItemInFolder(fsPath);
}
}
}

View File

@@ -20,8 +20,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
export class OpenSketchFiles extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
execute: (uri: URI, focusMainSketchFile) =>
this.openSketchFiles(uri, focusMainSketchFile),
execute: (uri: URI) => this.openSketchFiles(uri),
});
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
execute: (
@@ -34,23 +33,13 @@ export class OpenSketchFiles extends SketchContribution {
});
}
private async openSketchFiles(
uri: URI,
focusMainSketchFile = false
): Promise<void> {
private async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchesService.loadSketch(uri.toString());
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
if (focusMainSketchFile) {
await this.ensureOpened(mainFileUri, true, {
mode: 'activate',
preview: false,
counter: 0,
});
}
if (mainFileUri.endsWith('.pde')) {
const message = nls.localize(
'arduino/common/oldFormat',
@@ -116,9 +105,8 @@ export class OpenSketchFiles extends SketchContribution {
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
fileService: this.fileService,
sketchesService: this.sketchesService,
sketchService: this.sketchService,
labelProvider: this.labelProvider,
dialogService: this.dialogService,
});
if (movedSketch) {
this.workspaceService.open(new URI(movedSketch.uri), {
@@ -130,7 +118,7 @@ export class OpenSketchFiles extends SketchContribution {
}
private async openFallbackSketch(): Promise<void> {
const sketch = await this.sketchesService.createNewSketch();
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
}
@@ -138,7 +126,7 @@ export class OpenSketchFiles extends SketchContribution {
uri: string,
forceOpen = false,
options?: EditorOpenerOptions
): Promise<EditorWidget | undefined> {
): Promise<unknown> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
@@ -196,24 +184,23 @@ export class OpenSketchFiles extends SketchContribution {
// The editor is expected to be attached to the shell and visible in the UI.
// The deferred promise does not have to wait for the `editorManager#onCreated` event.
// It can resolve earlier.
if (widget) {
if (!widget) {
deferred.resolve(editorWidget);
}
});
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
const result: EditorWidget | undefined | 'timeout' = await Promise.race([
const result = await Promise.race([
deferred.promise,
wait(timeout).then(() => {
disposables.dispose();
return 'timeout' as const;
return 'timeout';
}),
]);
if (result === 'timeout') {
console.warn(
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
);
return undefined;
}
return result;
}

View File

@@ -1,3 +1,4 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
@@ -17,7 +18,6 @@ import {
SketchContribution,
URI,
} from './contribution';
import { DialogService } from '../dialog-service';
export type SketchLocation = string | URI | SketchRef;
export namespace SketchLocation {
@@ -71,7 +71,7 @@ export class OpenSketch extends SketchContribution {
}
const uri = SketchLocation.toUri(toOpen);
try {
await this.sketchesService.loadSketch(uri.toString());
await this.sketchService.loadSketch(uri.toString());
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
@@ -82,8 +82,13 @@ export class OpenSketch extends SketchContribution {
}
private async selectSketch(): Promise<Sketch | undefined> {
const defaultPath = await this.defaultPath();
const { filePaths } = await this.dialogService.showOpenDialog({
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
@@ -92,7 +97,8 @@ export class OpenSketch extends SketchContribution {
extensions: ['ino', 'pde'],
},
],
});
}
);
if (!filePaths.length) {
return undefined;
}
@@ -103,16 +109,15 @@ export class OpenSketch extends SketchContribution {
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
return promptMoveSketch(sketchFileUri, {
fileService: this.fileService,
sketchesService: this.sketchesService,
sketchService: this.sketchService,
labelProvider: this.labelProvider,
dialogService: this.dialogService,
});
}
}
@@ -130,18 +135,16 @@ export async function promptMoveSketch(
sketchFileUri: string | URI,
options: {
fileService: FileService;
sketchesService: SketchesService;
sketchService: SketchesService;
labelProvider: LabelProvider;
dialogService: DialogService;
}
): Promise<Sketch | undefined> {
const { fileService, sketchesService, labelProvider, dialogService } =
options;
const { fileService, sketchService, labelProvider } = options;
const uri =
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
const name = uri.path.name;
const nameWithExt = labelProvider.getName(uri);
const { response } = await dialogService.showMessageBox({
const { response } = await remote.dialog.showMessageBox({
title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question',
buttons: [
@@ -160,7 +163,7 @@ export async function promptMoveSketch(
const newSketchUri = uri.parent.resolve(name);
const exists = await fileService.exists(newSketchUri);
if (exists) {
await dialogService.showMessageBox({
await remote.dialog.showMessageBox({
type: 'error',
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
message: nls.localize(
@@ -176,6 +179,6 @@ export async function promptMoveSketch(
uri,
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return sketchesService.getSketchFolder(newSketchUri.toString());
return sketchService.getSketchFolder(newSketchUri.toString());
}
}

View File

@@ -1,4 +1,5 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX } from '@theia/core/lib/common/os';
import {
Contribution,
@@ -8,18 +9,14 @@ import {
CommandRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common/nls';
import { AppService } from '../app-service';
import { nls } from '@theia/core/lib/common';
@injectable()
export class QuitApp extends Contribution {
@inject(AppService)
private readonly appService: AppService;
override registerCommands(registry: CommandRegistry): void {
if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => this.appService.quit(),
execute: () => remote.app.quit(),
});
}
}

View File

@@ -1,166 +0,0 @@
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import {
TaskFactoryImpl,
WorkspaceInputDialogWithProgress,
} from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import {
CloudSketchContribution,
pullingSketch,
pushingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch, URI } from './contribution';
export interface RenameCloudSketchParams {
readonly cloudUri: URI;
readonly sketch: Sketch;
}
@injectable()
export class RenameCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH, {
execute: (params: RenameCloudSketchParams) =>
this.renameSketch(params, true),
});
}
private async renameSketch(
params: RenameCloudSketchParams,
skipShowErrorMessageOnOpen: boolean,
initValue: string = params.sketch.name
): Promise<string | undefined> {
const treeModel = await this.treeModel();
if (treeModel) {
const posixPath = params.cloudUri.path.toString();
const node = treeModel.getNode(posixPath);
const parentNode = node?.parent;
if (
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
CompositeTreeNode.is(parentNode)
) {
return this.openWizard(
params,
node,
parentNode,
treeModel,
skipShowErrorMessageOnOpen,
initValue
);
}
}
return undefined;
}
private async openWizard(
params: RenameCloudSketchParams,
node: CloudSketchbookTree.CloudSketchDirNode,
parentNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
): Promise<string | undefined> {
const parentUri = CloudSketchbookTree.CloudSketchDirNode.is(parentNode)
? parentNode.uri
: CreateUri.root;
const existingNames = parentNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.renameSketchWithProgress(params, node, treeModel, value)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
{
title: nls.localize(
'arduino/renameCloudSketch/renameSketchTitle',
'New name of the Cloud Sketch'
),
parentUri,
initialValue,
validate: (input) => {
if (existingNames.includes(input)) {
return sketchAlreadyExists(input);
}
return Sketch.validateCloudSketchFolderName(input) ?? '';
},
},
this.labelProvider,
taskFactory
);
await dialog.open(skipShowErrorMessageOnOpen);
return dialog.taskResult;
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.renameSketch(
params,
false,
taskFactory.value ?? initialValue
);
}
throw err;
}
}
private renameSketchWithProgress(
params: RenameCloudSketchParams,
node: CloudSketchbookTree.CloudSketchDirNode,
treeModel: CloudSketchbookTreeModel,
value: string
): (progress: Progress) => Promise<string | undefined> {
return async (progress: Progress) => {
const fromName = params.cloudUri.path.name;
const fromPosixPath = params.cloudUri.path.toString();
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
// push
progress.report({ message: pushingSketch(params.sketch.name) });
await treeModel.sketchbookTree().push(node, true);
// rename
progress.report({
message: nls.localize(
'arduino/cloudSketch/renaming',
"Renaming cloud sketch from '{0}' to '{1}'...",
fromName,
value
),
});
await this.createApi.rename(fromPosixPath, toPosixPath);
// sync
progress.report({
message: synchronizingSketchbook,
});
this.createApi.sketchCache.init(); // invalidate the cache
await this.createApi.sketches(); // IDE2 must pull all sketches to find the new one
const sketch = this.createApi.sketchCache.getSketch(toPosixPath);
if (!sketch) {
return undefined;
}
await treeModel.refresh();
// pull
progress.report({ message: pullingSketch(sketch.name) });
const pulledNode = await this.pull(sketch);
return pulledNode
? node.uri.parent.resolve(sketch.name).toString()
: undefined;
};
}
}
export namespace RenameCloudSketch {
export namespace Commands {
export const RENAME_CLOUD_SKETCH: Command = {
id: 'arduino-rename-cloud-sketch',
};
}
}

View File

@@ -1,35 +1,28 @@
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
import { StartupTasks } from '../../electron-common/startup-task';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
import {
SketchContribution,
URI,
Command,
CommandRegistry,
KeybindingRegistry,
MenuModelRegistry,
Sketch,
URI,
KeybindingRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/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';
import {
RenameCloudSketch,
RenameCloudSketchParams,
} from './rename-cloud-sketch';
import { assertConnectedToBackend } from './save-sketch';
@injectable()
export class SaveAsSketch extends CloudSketchContribution {
export class SaveAsSketch extends SketchContribution {
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
private readonly applicationShell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
@@ -42,7 +35,7 @@ export class SaveAsSketch extends CloudSketchContribution {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: nls.localizeByDefault('Save As...'),
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
order: '7',
});
}
@@ -65,77 +58,21 @@ export class SaveAsSketch extends CloudSketchContribution {
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
assertConnectedToBackend({
connectionStatusService: this.connectionStatusService,
messageService: this.messageService,
});
const sketch = await this.sketchServiceClient.currentSketch();
const [sketch, configuration] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) {
return false;
}
let destinationUri: string | undefined;
const cloudUri = this.createFeatures.cloudUri(sketch);
if (cloudUri) {
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
} else {
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
}
if (!destinationUri) {
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return false;
}
const copiedSketch = await this.sketchesService.copy(sketch, {
destinationUri,
});
const newWorkspaceUri = copiedSketch.uri;
await saveOntoCopiedSketch(
sketch,
newWorkspaceUri,
this.shell,
this.editorManager
);
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
const options: WorkspaceInput & StartupTasks = {
preserveWindow: true,
tasks: [],
};
if (openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [{ toDelete: sketch.uri }],
});
}
this.workspaceService.open(new URI(newWorkspaceUri), options);
}
return !!newWorkspaceUri;
}
private async createCloudCopy(
params: RenameCloudSketchParams
): Promise<string | undefined> {
return this.commandService.executeCommand<string>(
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
params
);
}
private async createLocalCopy(
sketch: Sketch,
execOnlyIfTemp?: boolean
): Promise<string | undefined> {
const isTemp = await this.sketchesService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return undefined;
}
const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = await this.defaultUri();
const sketchbookDirUri = new URI(configuration.sketchDirUri);
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
// Otherwise, it proposes the parent folder of the current sketch.
@@ -150,102 +87,89 @@ export class SaveAsSketch extends CloudSketchContribution {
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
const defaultUri = containerDirUri.resolve(
Sketch.toValidSketchFolderName(sketch.name, exists)
exists
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
: sketch.name
);
const defaultPath = await this.fileService.fsPath(defaultUri);
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
}
/**
* Prompts for the new sketch folder name until a valid one is give,
* then resolves with the destination sketch folder URI string,
* or `undefined` if the operation was canceled.
*/
private async promptLocalSketchFolderDestination(
sketch: Sketch,
defaultPath: string
): Promise<string | undefined> {
let sketchFolderDestinationUri: string | undefined;
while (!sketchFolderDestinationUri) {
const { filePath } = await this.dialogService.showSaveDialog({
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
});
if (!filePath) {
return undefined;
}
);
if (!filePath || canceled) {
return false;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
// The new location of the sketch cannot be inside the location of current sketch.
// https://github.com/arduino/arduino-ide/issues/1882
let dialogContent: InvalidSketchFolderDialogContent | undefined;
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) {
dialogContent = {
message: nls.localize(
'arduino/sketch/invalidSketchFolderLocationMessage',
"Invalid sketch folder location: '{0}'",
filePath
),
details: nls.localize(
'arduino/sketch/invalidSketchFolderLocationDetails',
'You cannot save a sketch into a folder inside itself.'
),
question: nls.localize(
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
'Do you want to try saving the sketch to a different location?'
),
};
if (!destinationUri) {
return false;
}
if (!dialogContent) {
const sketchFolderName = new URI(destinationUri).path.base;
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
if (errorMessage) {
dialogContent = {
message: nls.localize(
'arduino/sketch/invalidSketchFolderNameMessage',
"Invalid sketch folder name: '{0}'",
sketchFolderName
),
details: errorMessage,
question: nls.localize(
'arduino/sketch/editInvalidSketchFolderQuestion',
'Do you want to try saving the sketch with a different name?'
),
};
}
}
if (dialogContent) {
const message = `
${dialogContent.message}
${dialogContent.details}
${dialogContent.question}`.trim();
defaultPath = filePath;
const { response } = await this.dialogService.showMessageBox({
message,
buttons: [Dialog.CANCEL, Dialog.YES],
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
// cancel
if (response === 0) {
return undefined;
}
} else {
sketchFolderDestinationUri = destinationUri;
if (workspaceUri) {
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
if (markAsRecentlyOpened) {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
}
return sketchFolderDestinationUri;
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (workspaceUri && openAfterMove) {
this.windowService.setSafeToShutDown();
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;
}
}
interface InvalidSketchFolderDialogContent {
readonly message: string;
readonly details: string;
readonly question: string;
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, object>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
const uriString = uri?.toString();
let relativePath: string;
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (mainFileUri === uriString) {
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketchUri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
}));
}
}
export namespace SaveAsSketch {
@@ -272,48 +196,3 @@ export namespace SaveAsSketch {
};
}
}
export async function saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string,
shell: ApplicationShell,
editorManager: EditorManager
): Promise<void> {
const widgets = shell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketch.uri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchFolderUri + path);
try {
const widget = await editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
})
);
}

View File

@@ -1,18 +1,16 @@
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
Command,
CommandRegistry,
KeybindingRegistry,
MenuModelRegistry,
SketchContribution,
KeybindingRegistry,
} from './contribution';
import { SaveAsSketch } from './save-as-sketch';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveSketch extends SketchContribution {
@@ -38,15 +36,11 @@ export class SaveSketch extends SketchContribution {
}
async saveSketch(): Promise<void> {
assertConnectedToBackend({
connectionStatusService: this.connectionStatusService,
messageService: this.messageService,
});
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
const isTemp = await this.sketchesService.isTemp(sketch);
const isTemp = await this.sketchService.isTemp(sketch);
if (isTemp) {
return this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
@@ -69,18 +63,3 @@ export namespace SaveSketch {
};
}
}
// https://github.com/arduino/arduino-ide/issues/2081
export function assertConnectedToBackend(param: {
connectionStatusService: ApplicationConnectionStatusContribution;
messageService: MessageService;
}): void {
if (param.connectionStatusService.offlineStatus === 'backend') {
const message = nls.localize(
'theia/core/couldNotSave',
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.'
);
param.messageService.error(message);
throw new Error(message);
}
}

View File

@@ -4,10 +4,7 @@ import {
} 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 type {
BoardList,
BoardListItem,
} from '../../common/protocol/board-list';
import { BoardsConfig } from '../boards/boards-config';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution } from './contribution';
@@ -15,21 +12,21 @@ import { Contribution } from './contribution';
export class SelectedBoard extends Contribution {
@inject(StatusBar)
private readonly statusBar: StatusBar;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
override onStart(): void {
this.boardsServiceProvider.onBoardListDidChange(() =>
this.update(this.boardsServiceProvider.boardList)
this.boardsServiceProvider.onBoardsConfigChanged((config) =>
this.update(config)
);
}
override onReady(): void {
this.update(this.boardsServiceProvider.boardList);
this.update(this.boardsServiceProvider.boardsConfig);
}
private update(boardList: BoardList): void {
const { selectedBoard, selectedPort } = boardList.boardsConfig;
private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard
@@ -41,30 +38,17 @@ export class SelectedBoard extends Contribution {
className: 'arduino-selected-board',
});
if (selectedBoard) {
const notConnectedLabel = nls.localize(
'arduino/common/notConnected',
'[not connected]'
);
let portLabel = notConnectedLabel;
if (selectedPort) {
portLabel = nls.localize(
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort
? nls.localize(
'arduino/common/selectedOn',
'on {0}',
selectedPort.address
);
const selectedItem: BoardListItem | undefined =
boardList.items[boardList.selectedIndex];
if (!selectedItem) {
portLabel += ` ${notConnectedLabel}`; // append ` [not connected]` when the port is selected but it's not detected by the CLI
}
}
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: portLabel,
)
: nls.localize('arduino/common/notConnected', '[not connected]'),
className: 'arduino-selected-port',
});
} else {
this.statusBar.removeElement('arduino-selected-port');
}
}
}

View File

@@ -1,34 +1,32 @@
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { Settings } from '../dialogs/settings/settings';
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Command,
CommandRegistry,
KeybindingRegistry,
MenuModelRegistry,
CommandRegistry,
SketchContribution,
KeybindingRegistry,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences } from '../dialogs/settings/settings';
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
import { nls } from '@theia/core/lib/common';
@injectable()
export class OpenSettings extends SketchContribution {
export class Settings extends SketchContribution {
@inject(SettingsDialog)
private readonly settingsDialog: SettingsDialog;
protected readonly settingsDialog: SettingsDialog;
private settingsOpened = false;
protected settingsOpened = false;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSettings.Commands.OPEN, {
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Settings | undefined = undefined;
let settings: Preferences | undefined = undefined;
try {
this.settingsOpened = true;
this.menuManager.update();
settings = await this.settingsDialog.open();
} finally {
this.settingsOpened = false;
this.menuManager.update();
}
if (settings) {
await this.settingsService.update(settings);
@@ -43,7 +41,7 @@ export class OpenSettings extends SketchContribution {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: OpenSettings.Commands.OPEN.id,
commandId: Settings.Commands.OPEN.id,
label:
nls.localize(
'vscode/preferences.contribution/preferences',
@@ -59,13 +57,13 @@ export class OpenSettings extends SketchContribution {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSettings.Commands.OPEN.id,
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',
});
}
}
export namespace OpenSettings {
export namespace Settings {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-settings-open',

View File

@@ -1,34 +1,50 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import {
URI,
SketchContribution,
Command,
CommandRegistry,
KeybindingRegistry,
MenuModelRegistry,
open,
SketchContribution,
KeybindingRegistry,
TabBarToolbarRegistry,
URI,
open,
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { nls } from '@theia/core/lib/common';
@injectable()
export class SketchControl extends SketchContribution {
@inject(ApplicationShell)
private readonly shell: ApplicationShell;
protected readonly shell: ApplicationShell;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
private readonly contextMenuRenderer: ContextMenuRenderer;
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection();
@@ -41,23 +57,39 @@ export class SketchControl extends SketchContribution {
this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
let parentElement: HTMLElement | undefined = undefined;
const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
);
if (target instanceof HTMLElement) {
parentElement = target.parentElement ?? undefined;
}
if (!parentElement) {
return;
}
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
);
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
const parentSketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentSketch = await this.sketchService.getSketchFolder(
parentSketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowRename(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
@@ -73,11 +105,32 @@ export class SketchControl extends SketchContribution {
)
)
);
} else {
const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
nls.localize('vscode/fileActions/rename', 'Rename')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
renamePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
)
);
}
if (
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowDelete(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_DELETE.id,
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: nls.localize('vscode/fileActions/delete', 'Delete'),
order: '2',
}
@@ -89,9 +142,22 @@ export class SketchControl extends SketchContribution {
)
)
);
} else {
const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
nls.localize('vscode/fileActions/delete', 'Delete')
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
deletePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
)
);
}
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
@@ -127,7 +193,6 @@ export class SketchControl extends SketchContribution {
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
showDisabled: true,
};
this.contextMenuRenderer.render(options);
},
@@ -170,7 +235,7 @@ export class SketchControl extends SketchContribution {
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left',
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
@@ -184,6 +249,27 @@ export class SketchControl extends SketchContribution {
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected isCloudSketch(uri: string): boolean {
try {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
}
return false;
} catch {
return false;
}
}
protected allowRename(uri: string): boolean {
return !this.isCloudSketch(uri);
}
protected allowDelete(uri: string): boolean {
return !this.isCloudSketch(uri);
}
}
export namespace SketchControl {

View File

@@ -3,7 +3,7 @@ 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 '../sketches-service-client-impl';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { Sketch, SketchContribution } from './contribution';
import { OpenSketchFiles } from './open-sketch-files';
@@ -38,7 +38,7 @@ export class SketchFilesTracker extends SketchContribution {
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch = await this.sketchesService.loadSketch(
const reloadedSketch = await this.sketchService.loadSketch(
sketch.uri
);
if (Sketch.isInSketch(resource, reloadedSketch)) {

View File

@@ -11,7 +11,6 @@ import { nls } from '@theia/core/lib/common/nls';
export class Sketchbook extends Examples {
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
this.configService.onDidChangeSketchDirUri(() => this.update());
}
override async onReady(): Promise<void> {
@@ -19,7 +18,7 @@ export class Sketchbook extends Examples {
}
protected override update(): void {
this.sketchesService.getSketches({}).then((container) => {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.menuManager.update();
});

View File

@@ -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));
}
}

View File

@@ -1,65 +0,0 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import {
hasStartupTasks,
StartupTasks,
} from '../../electron-common/startup-task';
import { AppService } from '../app-service';
import { Contribution } from './contribution';
@injectable()
export class StartupTasksExecutor extends Contribution {
@inject(AppService)
private readonly appService: AppService;
private readonly toDispose = new DisposableCollection();
@postConstruct()
protected override init(): void {
super.init();
this.toDispose.push(
this.appService.registerStartupTasksHandler((tasks) =>
this.handleStartupTasks(tasks)
)
);
}
onStop(): void {
this.toDispose.dispose();
}
private async handleStartupTasks(tasks: StartupTasks): Promise<void> {
console.debug(
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
tasks
)}`
);
if (!hasStartupTasks(tasks)) {
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
return;
}
await this.appStateService.reachedState('ready');
console.log(`Executing startup tasks:`);
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
)
);
});
}
}

View File

@@ -1,176 +0,0 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import type { ArduinoState } from 'vscode-arduino-api';
import {
BoardsService,
CompileSummary,
isCompileSummary,
BoardsConfig,
PortIdentifier,
resolveDetectedPort,
} from '../../common/protocol';
import {
toApiBoardDetails,
toApiCompileSummary,
toApiPort,
} from '../../common/protocol/arduino-context-mapper';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution } from './contribution';
interface UpdateStateParams<T extends ArduinoState> {
readonly key: keyof T;
readonly value: T[keyof T];
}
/**
* Contribution for updating the Arduino state, such as the FQBN, selected port, and sketch path changes via commands, so other VS Code extensions can access it.
* See [`vscode-arduino-api`](https://github.com/dankeboy36/vscode-arduino-api#api) for more details.
*/
@injectable()
export class UpdateArduinoState extends SketchContribution {
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
private readonly toDispose = new DisposableCollection();
override onStart(): void {
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange(() =>
this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig)
),
this.sketchServiceClient.onCurrentSketchDidChange((sketch) =>
this.updateSketchPath(sketch)
),
this.configService.onDidChangeDataDirUri((dataDirUri) =>
this.updateDataDirPath(dataDirUri)
),
this.configService.onDidChangeSketchDirUri((userDirUri) =>
this.updateUserDirPath(userDirUri)
),
this.commandService.onDidExecuteCommand(({ commandId, args }) => {
if (
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
isCompileSummary(args[0])
) {
this.updateCompileSummary(args[0]);
}
}),
this.boardsDataStore.onChanged((fqbn) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
if (selectedFqbn && fqbn.includes(selectedFqbn)) {
this.updateBoardDetails(selectedFqbn);
}
}),
]);
}
override onReady(): void {
this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig); // TODO: verify!
this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch());
this.updateUserDirPath(this.configService.tryGetSketchDirUri());
this.updateDataDirPath(this.configService.tryGetDataDirUri());
}
onStop(): void {
this.toDispose.dispose();
}
private async updateSketchPath(
sketch: CurrentSketch | undefined
): Promise<void> {
const sketchPath = CurrentSketch.isValid(sketch)
? new URI(sketch.uri).path.fsPath()
: undefined;
return this.updateState({ key: 'sketchPath', value: sketchPath });
}
private async updateCompileSummary(
compileSummary: CompileSummary
): Promise<void> {
const apiCompileSummary = toApiCompileSummary(compileSummary);
return this.updateState({
key: 'compileSummary',
value: apiCompileSummary,
});
}
private async updateBoardsConfig(boardsConfig: BoardsConfig): Promise<void> {
const fqbn = boardsConfig.selectedBoard?.fqbn;
const port = boardsConfig.selectedPort;
await this.updateFqbn(fqbn);
await this.updateBoardDetails(fqbn);
await this.updatePort(port);
}
private async updateFqbn(fqbn: string | undefined): Promise<void> {
await this.updateState({ key: 'fqbn', value: fqbn });
}
private async updateBoardDetails(fqbn: string | undefined): Promise<void> {
const unset = () =>
this.updateState({ key: 'boardDetails', value: undefined });
if (!fqbn) {
return unset();
}
const [details, persistedData] = await Promise.all([
this.boardsService.getBoardDetails({ fqbn }),
this.boardsDataStore.getData(fqbn),
]);
if (!details) {
return unset();
}
const apiBoardDetails = toApiBoardDetails({
...details,
configOptions:
BoardsDataStore.Data.EMPTY === persistedData
? details.configOptions
: persistedData.configOptions.slice(),
});
return this.updateState({
key: 'boardDetails',
value: apiBoardDetails,
});
}
private async updatePort(port: PortIdentifier | undefined): Promise<void> {
const resolvedPort =
port &&
resolveDetectedPort(port, this.boardsServiceProvider.detectedPorts);
const apiPort = resolvedPort && toApiPort(resolvedPort);
return this.updateState({ key: 'port', value: apiPort });
}
private async updateUserDirPath(userDirUri: URI | undefined): Promise<void> {
const userDirPath = userDirUri?.path.fsPath();
return this.updateState({
key: 'userDirPath',
value: userDirPath,
});
}
private async updateDataDirPath(dataDirUri: URI | undefined): Promise<void> {
const dataDirPath = dataDirUri?.path.fsPath();
return this.updateState({
key: 'dataDirPath',
value: dataDirPath,
});
}
private async updateState<T extends ArduinoState>(
params: UpdateStateParams<T>
): Promise<void> {
await this.hostedPluginSupport.didStart;
return this.commandService.executeCommand('arduinoAPI.updateState', params);
}
}

View File

@@ -12,14 +12,12 @@ import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoPreferences } from '../arduino-preferences';
import {
arduinoCert,
certificateList,
} from '../dialogs/certificate-uploader/utils';
import {
ArduinoFirmwareUploader,
UploadCertificateParams,
} from '../../common/protocol/arduino-firmware-uploader';
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
@injectable()
@@ -33,29 +31,22 @@ export class UploadCertificate extends Contribution {
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected dialogOpened = false;
override onStart(): void {
this.preferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.board.certificates') {
this.menuManager.update();
}
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
this.menuManager.update();
await this.dialog.open();
} finally {
this.dialogOpened = false;
this.menuManager.update();
}
},
isEnabled: () => !this.dialogOpened,
@@ -63,7 +54,7 @@ export class UploadCertificate extends Contribution {
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
execute: async (certToRemove) => {
const certs = this.preferences.get('arduino.board.certificates');
const certs = this.arduinoPreferences.get('arduino.board.certificates');
this.preferenceService.set(
'arduino.board.certificates',
@@ -77,9 +68,14 @@ export class UploadCertificate extends Contribution {
});
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
execute: async (params: UploadCertificateParams) => {
return this.arduinoFirmwareUploader.uploadCertificates(params);
execute: async ({ fqbn, address, urls }) => {
return this.arduinoFirmwareUploader.uploadCertificates(
`-b ${fqbn} -a ${address} ${urls
.map((url: string) => `-u ${url}`)
.join(' ')}`
);
},
isEnabled: () => true,
});
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
@@ -93,6 +89,7 @@ export class UploadCertificate extends Contribution {
args: [args.cert],
});
},
isEnabled: () => true,
});
}

View File

@@ -21,11 +21,9 @@ export class UploadFirmware extends Contribution {
execute: async () => {
try {
this.dialogOpened = true;
this.menuManager.update();
await this.dialog.open();
} finally {
this.dialogOpened = false;
this.menuManager.update();
}
},
isEnabled: () => !this.dialogOpened,
@@ -47,7 +45,7 @@ export namespace UploadFirmware {
id: 'arduino-upload-firmware-open',
label: nls.localize(
'arduino/firmware/updater',
'Firmware Updater'
'WiFi101 / WiFiNINA Firmware Updater'
),
category: 'Arduino',
};

View File

@@ -1,30 +1,30 @@
import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CoreService, sanitizeFqbn } from '../../common/protocol';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService, Port } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
Command,
CommandRegistry,
CoreServiceContribution,
KeybindingRegistry,
MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry,
CoreServiceContribution,
} from './contribution';
import { UserFields } from './user-fields';
import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields';
@injectable()
export class UploadSketch extends CoreServiceContribution {
@inject(UserFields)
private readonly userFields: UserFields;
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;
@inject(UserFields)
private readonly userFields: UserFields;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
@@ -106,7 +106,7 @@ export class UploadSketch extends CoreServiceContribution {
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.menuManager.update();
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
this.onDidChangeEmitter.fire();
this.clearVisibleNotification();
@@ -134,14 +134,12 @@ export class UploadSketch extends CoreServiceContribution {
return;
}
const uploadResponse = await this.doWithProgress({
await this.doWithProgress({
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
task: (progressId, coreService) =>
coreService.upload({ ...uploadOptions, progressId }),
keepOutput: true,
});
// the port update is NOOP if nothing has changed
this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload);
this.messageService.info(
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
@@ -151,10 +149,8 @@ export class UploadSketch extends CoreServiceContribution {
this.userFields.notifyFailedWithError(e);
this.handleError(e);
} finally {
// TODO: here comes the port change if happened during the upload
// https://github.com/arduino/arduino-cli/issues/2245
this.uploadInProgress = false;
this.menuManager.update();
this.boardsServiceProvider.attemptPostUploadAutoSelect();
this.onDidChangeEmitter.fire();
}
}
@@ -172,11 +168,11 @@ export class UploadSketch extends CoreServiceContribution {
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const port = boardsConfig.selectedPort;
const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
return {
sketch,
fqbn,
@@ -187,6 +183,41 @@ export class UploadSketch extends CoreServiceContribution {
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;
}
/**
* 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 {

View File

@@ -1,9 +1,9 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { nls } from '@theia/core/lib/common';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardUserField, CoreError } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MenuModelRegistry, Contribution } from './contribution';
import { UploadSketch } from './upload-sketch';
@@ -12,6 +12,7 @@ export class UserFields extends Contribution {
private boardRequiresUserFields = false;
private userFieldsSet = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;
@@ -19,22 +20,42 @@ export class UserFields extends Contribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigDidChange(async () => {
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.menuManager.update();
this.registerMenus(this.menuRegistry);
});
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
});
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
}
private selectedFqbnAddress(): string | undefined {
@@ -43,7 +64,10 @@ export class UserFields extends Contribution {
if (!fqbn) {
return undefined;
}
const address = boardsConfig.selectedPort?.address || '';
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address ||
'';
return fqbn + '|' + address;
}

View File

@@ -1,198 +0,0 @@
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
import { injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CloudSketchContribution } from './cloud-contribution';
import { Sketch, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';
@injectable()
export class ValidateSketch extends CloudSketchContribution {
override onReady(): void {
this.validate();
}
private async validate(): Promise<void> {
const result = await this.promptFixActions();
if (!result) {
const yes = await this.prompt(
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
nls.localize(
'arduino/validateSketch/abortFixMessage',
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
Dialog.NO
),
[Dialog.NO, Dialog.YES]
);
if (yes) {
return this.validate();
}
const sketch = await this.sketchesService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), {
preserveWindow: true,
});
}
}
/**
* Returns with an array of actions the user has to perform to fix the invalid sketch.
*/
private validateSketch(
sketch: Sketch,
dataDirUri: URI | undefined
): FixAction[] {
// sketch code file validation errors first as they do not require window reload
const actions = Sketch.uris(sketch)
.filter((uri) => uri !== sketch.mainFileUri)
.map((uri) => new URI(uri))
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
.map((uri) => ({
uri,
error: this.doValidate(sketch, dataDirUri, uri.path.name),
}))
.filter(({ error }) => Boolean(error))
.map((object) => <{ uri: URI; error: string }>object)
.map(({ uri, error }) => ({
execute: async () => {
const unknown =
(await this.promptRenameSketchFile(uri, error)) &&
(await this.commandService.executeCommand(
WorkspaceCommands.FILE_RENAME.id,
uri
));
return !!unknown;
},
}));
// sketch folder + main sketch file last as it requires a `Save as...` and the window reload
const sketchFolderName = new URI(sketch.uri).path.base;
const sketchFolderNameError = this.doValidate(
sketch,
dataDirUri,
sketchFolderName
);
if (sketchFolderNameError) {
actions.push({
execute: async () => {
const unknown =
(await this.promptRenameSketch(sketch, sketchFolderNameError)) &&
(await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
<SaveAsSketch.Options>{
markAsRecentlyOpened: true,
openAfterMove: true,
wipeOriginal: true,
}
));
return !!unknown;
},
});
}
return actions;
}
private doValidate(
sketch: Sketch,
dataDirUri: URI | undefined,
toValidate: string
): string | undefined {
const cloudUri = this.createFeatures.isCloud(sketch, dataDirUri);
return cloudUri
? Sketch.validateCloudSketchFolderName(toValidate)
: Sketch.validateSketchFolderName(toValidate);
}
private async currentSketch(): Promise<Sketch> {
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
if (CurrentSketch.isValid(sketch)) {
return sketch;
}
const deferred = new Deferred<Sketch>();
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
(sketch) => {
if (CurrentSketch.isValid(sketch)) {
disposable.dispose();
deferred.resolve(sketch);
}
}
);
return deferred.promise;
}
private async promptFixActions(): Promise<boolean> {
const maybeDataDirUri = this.configService.tryGetDataDirUri();
const [sketch, dataDirUri] = await Promise.all([
this.currentSketch(),
maybeDataDirUri ??
waitForEvent(this.configService.onDidChangeDataDirUri, 5_000),
]);
const fixActions = this.validateSketch(sketch, dataDirUri);
for (const fixAction of fixActions) {
const result = await fixAction.execute();
if (!result) {
return false;
}
}
return true;
}
private async promptRenameSketch(
sketch: Sketch,
error: string
): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFolderTitle',
'Invalid sketch name'
),
nls.localize(
'arduino/validateSketch/renameSketchFolderMessage',
"The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
sketch.name,
error
)
);
}
private async promptRenameSketchFile(
uri: URI,
error: string
): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFileTitle',
'Invalid sketch filename'
),
nls.localize(
'arduino/validateSketch/renameSketchFileMessage',
"The sketch file '{0}' cannot be used. {1} Do you want to rename the sketch file now?",
uri.path.base,
error
)
);
}
private async prompt(
title: string,
message: string,
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
): Promise<boolean> {
const { response } = await this.dialogService.showMessageBox({
title,
message,
type: 'warning',
buttons,
});
// cancel
if (response === 0) {
return false;
}
return true;
}
}
interface FixAction {
execute(): Promise<boolean>;
}

View File

@@ -11,7 +11,7 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler';
@@ -21,18 +21,11 @@ export interface VerifySketchParams {
*/
readonly exportBinaries?: boolean;
/**
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
*/
readonly silent?: boolean;
}
/**
* - `"idle"` when neither verify, nor upload is running,
* - `"explicit-verify"` when only verify is running triggered by the user, and
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
*/
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
@injectable()
export class VerifySketch extends CoreServiceContribution {
@inject(CoreErrorHandler)
@@ -40,24 +33,22 @@ export class VerifySketch extends CoreServiceContribution {
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private verifyProgress: VerifyProgress = 'idle';
private verifyInProgress = false;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: (params?: VerifySketchParams) => this.verifySketch(params),
isEnabled: () => this.verifyProgress === 'idle',
isEnabled: () => !this.verifyInProgress,
});
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch({ exportBinaries: true }),
isEnabled: () => this.verifyProgress === 'idle',
isEnabled: () => !this.verifyInProgress,
});
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => this.verifyProgress !== 'explicit-verify',
// toggled only when verify is running, but not toggled when automatic verify is running before the upload
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
isToggled: () => this.verifyProgress === 'explicit-verify',
isEnabled: () => !this.verifyInProgress,
isToggled: () => this.verifyInProgress,
execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
});
@@ -108,16 +99,15 @@ export class VerifySketch extends CoreServiceContribution {
private async verifySketch(
params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> {
if (this.verifyProgress !== 'idle') {
if (this.verifyInProgress) {
return undefined;
}
try {
this.verifyProgress = params?.silent
? 'automatic-verify'
: 'explicit-verify';
if (!params?.silent) {
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
this.menuManager.update();
}
this.clearVisibleNotification();
this.coreErrorHandler.reset();
@@ -149,9 +139,10 @@ export class VerifySketch extends CoreServiceContribution {
this.handleError(e);
return undefined;
} finally {
this.verifyProgress = 'idle';
this.verifyInProgress = false;
if (!params?.silent) {
this.onDidChangeEmitter.fire();
this.menuManager.update();
}
}
}

View File

@@ -1,37 +1,80 @@
import { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import { fetch } from 'cross-fetch';
import { SketchesService } from '../../common/protocol';
import { uint8ArrayToString } from '../../common/utils';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import { injectable, inject } from '@theia/core/shared/inversify';
import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import { Create, CreateError } from './typings';
interface ResponseResultProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ResponseResultProvider {
(response: Response): Promise<any>;
}
namespace ResponseResultProvider {
export namespace ResponseResultProvider {
export const NOOP: ResponseResultProvider = async () => undefined;
export const TEXT: ResponseResultProvider = (response) => response.text();
export const JSON: ResponseResultProvider = (response) => response.json();
}
export function Utf8ArrayToStr(array: Uint8Array): string {
let out, i, c;
let char2, char3;
out = '';
const len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12:
case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
);
break;
}
}
return out;
}
type ResourceType = 'f' | 'd';
@injectable()
export class CreateApi {
@inject(SketchCache)
readonly sketchCache: SketchCache;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesService)
private readonly sketchesService: SketchesService;
protected sketchCache: SketchCache;
protected authenticationService: AuthenticationClientService;
protected arduinoPreferences: ArduinoPreferences;
public init(
authenticationService: AuthenticationClientService,
arduinoPreferences: ArduinoPreferences
): CreateApi {
this.authenticationService = authenticationService;
this.arduinoPreferences = arduinoPreferences;
return this;
}
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
return {
@@ -57,74 +100,43 @@ export class CreateApi {
return result;
}
/**
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
* See [Create.Resource#path](./typings.ts). If `cache` is `true` and a sketch exists with the path,
* the cache will be updated with the new state of the sketch.
*/
// TODO: no nulls in API
async sketchByPath(
sketchPath: string,
cache = false
): Promise<Create.Sketch | null> {
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
const headers = await this.headers();
const sketch = await this.run<Create.Sketch>(url, {
method: 'GET',
headers,
});
if (sketch && cache) {
this.sketchCache.addSketch(sketch);
const posixPath = createPaths.toPosixPath(sketch.path);
this.sketchCache.purgeByPath(posixPath);
}
return sketch;
}
async sketches(limit = 50): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me');
url.searchParams.set('limit', limit.toString());
const headers = await this.headers();
const allSketches: Create.Sketch[] = [];
const result: { sketches: Create.Sketch[] } = { sketches: [] };
let partialSketches: Create.Sketch[] = [];
let currentOffset = 0;
while (true) {
do {
url.searchParams.set('offset', currentOffset.toString());
const { sketches } = await this.run<{ sketches: Create.Sketch[] }>(url, {
partialSketches = (
await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
});
allSketches.push(...sketches);
if (sketches.length < limit) {
break;
})
).sketches;
if (partialSketches.length !== 0) {
result.sketches = result.sketches.concat(partialSketches);
}
currentOffset += limit;
// The create API doc show that there is `next` and `prev` pages, but it does not work
// https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_search
// IF sketchCount mod limit === 0, an extra fetch must happen to detect the end of the pagination.
}
allSketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return allSketches;
currentOffset = currentOffset + limit;
} while (partialSketches.length !== 0);
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return result.sketches;
}
async createSketch(
posixPath: string,
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent(),
payloadOverride: Record<
string,
string | boolean | number | Record<string, unknown>
> = {}
content: string = CreateApi.defaultInoContent
): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`);
const [headers, content] = await Promise.all([
this.headers(),
contentProvider,
]);
const headers = await this.headers();
const payload = {
ino: btoa(content),
path: posixPath,
user_id: 'me',
...payloadOverride,
};
const init = {
method: 'PUT',
@@ -240,17 +252,7 @@ export class CreateApi {
return data;
}
const posixPath = createPaths.parentPosix(path);
let sketch = this.sketchCache.getSketch(posixPath);
// Workaround for https://github.com/arduino/arduino-ide/issues/1999.
if (!sketch) {
// Convert the ordinary sketch POSIX path to the Create path.
// For example, `/sketch_apr6a` will be transformed to `8a694e4b83878cc53472bd75ee928053:kittaakos/sketches_v2/sketch_apr6a`.
const createPathPrefix = this.sketchCache.createPathPrefix;
if (createPathPrefix) {
sketch = await this.sketchByPath(createPathPrefix + posixPath, true);
}
}
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
if (
sketch &&
@@ -289,7 +291,7 @@ export class CreateApi {
this.sketchCache.addSketch(sketch);
let file = '';
if (sketch.secrets) {
if (sketch && sketch.secrets) {
for (const item of sketch.secrets) {
file += `#define ${item.name} "${item.value}"\r\n`;
}
@@ -326,9 +328,10 @@ export class CreateApi {
if (sketch) {
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
const headers = await this.headers();
// parse the secret file
const secrets = (
typeof content === 'string' ? content : uint8ArrayToString(content)
typeof content === 'string' ? content : Utf8ArrayToStr(content)
)
.split(/\r?\n/)
.reduce((prev, curr) => {
@@ -378,7 +381,7 @@ export class CreateApi {
return;
}
// do not upload "do_not_sync" files/directories and their descendants
// do not upload "do_not_sync" files/directoris and their descendants
const segments = posixPath.split(posix.sep) || [];
if (
segments.some((segment) => Create.do_not_sync_files.includes(segment))
@@ -392,7 +395,7 @@ export class CreateApi {
const headers = await this.headers();
let data: string =
typeof content === 'string' ? content : uint8ArrayToString(content);
typeof content === 'string' ? content : Utf8ArrayToStr(content);
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
const payload = { data: btoa(data) };
@@ -412,21 +415,6 @@ export class CreateApi {
await this.delete(posixPath, 'd');
}
/**
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
* See [Create.Resource#path](./typings.ts). Unlike other endpoints, it does not support the `$HOME`
* variable substitution. The DELETE directory endpoint is bogus and responses with HTTP 500
* instead of 404 when deleting a non-existing resource.
*/
async deleteSketch(sketchPath: string): Promise<void> {
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
const headers = await this.headers();
await this.run(url, {
method: 'DELETE',
headers,
});
}
private async delete(posixPath: string, type: ResourceType): Promise<void> {
const url = new URL(
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
@@ -486,18 +474,15 @@ export class CreateApi {
await this.run(url, init, ResponseResultProvider.NOOP);
}
private fetchCounter = 0;
private async run<T>(
requestInfo: URL,
requestInfo: RequestInfo | URL,
init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> {
const fetchCount = `[${++this.fetchCounter}]`;
const fetchStart = performance.now();
const method = init?.method ? `${init.method}: ` : '';
const url = requestInfo.toString();
const response = await fetch(requestInfo.toString(), init);
const fetchEnd = performance.now();
const response = await fetch(
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
init
);
if (!response.ok) {
let details: string | undefined = undefined;
try {
@@ -508,18 +493,7 @@ export class CreateApi {
const { statusText, status } = response;
throw new CreateError(statusText, status, details);
}
const parseStart = performance.now();
const result = await resultProvider(response);
const parseEnd = performance.now();
console.debug(
`HTTP ${fetchCount} ${method}${url} [fetch: ${(
fetchEnd - fetchStart
).toFixed(2)} ms, parse: ${(parseEnd - parseStart).toFixed(
2
)} ms] body: ${
typeof result === 'string' ? result : JSON.stringify(result)
}`
);
return result;
}
@@ -542,3 +516,19 @@ export class CreateApi {
return this.authenticationService.session?.accessToken || '';
}
}
export namespace CreateApi {
export const defaultInoContent = `/*
*/
void setup() {
}
void loop() {
}
`;
}

View File

@@ -1,146 +0,0 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Sketch } from '../../common/protocol';
import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import {
ARDUINO_CLOUD_FOLDER,
REMOTE_SKETCHBOOK_FOLDER,
} from '../utils/constants';
import { CreateUri } from './create-uri';
export type CloudSketchState = 'push' | 'pull';
@injectable()
export class CreateFeatures implements FrontendApplicationContribution {
@inject(ArduinoPreferences)
private readonly preferences: ArduinoPreferences;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
/**
* The keys are the Create URI of the sketches.
*/
private readonly _cloudSketchStates = new Map<string, CloudSketchState>();
private readonly onDidChangeSessionEmitter = new Emitter<
AuthenticationSession | undefined
>();
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
private readonly onDidChangeCloudSketchStateEmitter = new Emitter<{
uri: URI;
state: CloudSketchState | undefined;
}>();
private readonly toDispose = new DisposableCollection(
this.onDidChangeSessionEmitter,
this.onDidChangeEnabledEmitter,
this.onDidChangeCloudSketchStateEmitter
);
private _enabled: boolean;
private _session: AuthenticationSession | undefined;
onStart(): void {
this.toDispose.pushAll([
this.authenticationService.onSessionDidChange((session) => {
const oldSession = this._session;
this._session = session;
if (!!oldSession !== !!this._session) {
this.onDidChangeSessionEmitter.fire(this._session);
}
}),
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.cloud.enabled') {
const oldEnabled = this._enabled;
this._enabled = Boolean(newValue);
if (this._enabled !== oldEnabled) {
this.onDidChangeEnabledEmitter.fire(this._enabled);
}
}
}),
]);
this._enabled = this.preferences['arduino.cloud.enabled'];
this._session = this.authenticationService.session;
}
onStop(): void {
this.toDispose.dispose();
}
get onDidChangeSession(): Event<AuthenticationSession | undefined> {
return this.onDidChangeSessionEmitter.event;
}
get onDidChangeEnabled(): Event<boolean> {
return this.onDidChangeEnabledEmitter.event;
}
get onDidChangeCloudSketchState(): Event<{
uri: URI;
state: CloudSketchState | undefined;
}> {
return this.onDidChangeCloudSketchStateEmitter.event;
}
get session(): AuthenticationSession | undefined {
return this._session;
}
get enabled(): boolean {
return this._enabled;
}
cloudSketchState(uri: URI): CloudSketchState | undefined {
return this._cloudSketchStates.get(uri.toString());
}
setCloudSketchState(uri: URI, state: CloudSketchState | undefined): void {
if (uri.scheme !== CreateUri.scheme) {
throw new Error(
`Expected a URI with '${uri.scheme}' scheme. Got: ${uri.toString()}`
);
}
const key = uri.toString();
if (!state) {
if (!this._cloudSketchStates.delete(key)) {
console.warn(
`Could not reset the cloud sketch state of ${key}. No state existed for the the cloud sketch.`
);
} else {
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state: undefined });
}
} else {
this._cloudSketchStates.set(key, state);
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state });
}
}
/**
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
* Returns with `undefined` if `dataDirUri` is `undefined`.
*/
isCloud(sketch: Sketch, dataDirUri: URI | undefined): boolean | undefined {
if (!dataDirUri) {
console.warn(
`Could not decide whether the sketch ${sketch.uri} is cloud or local. The 'directories.data' location was not available from the CLI config.`
);
return undefined;
}
return dataDirUri
.resolve(REMOTE_SKETCHBOOK_FOLDER)
.resolve(ARDUINO_CLOUD_FOLDER)
.isEqualOrParent(new URI(sketch.uri));
}
cloudUri(sketch: Sketch): URI | undefined {
if (!this.session) {
return undefined;
}
return this.localCacheFsProvider.from(new URI(sketch.uri));
}
}

View File

@@ -29,7 +29,6 @@ import { CreateUri } from './create-uri';
import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { Create } from './typings';
import { stringToUint8Array } from '../../common/utils';
@injectable()
export class CreateFsProvider
@@ -155,7 +154,7 @@ export class CreateFsProvider
async readFile(uri: URI): Promise<Uint8Array> {
const content = await this.getCreateApi.readFile(uri.path.toString());
return stringToUint8Array(content);
return new TextEncoder().encode(content);
}
async writeFile(
@@ -190,6 +189,10 @@ export class CreateFsProvider
FileSystemProviderErrorCode.NoPermissions
);
}
return this.createApi;
return this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
}
}

View File

@@ -1,4 +1,4 @@
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { toPosixPath, parentPosix, posix } from './create-paths';
import { Create } from './typings';

View File

@@ -71,23 +71,3 @@ export class CreateError extends Error {
Object.setPrototypeOf(this, CreateError.prototype);
}
}
export type ConflictError = CreateError & { status: 409 };
export function isConflict(err: unknown): err is ConflictError {
return isErrorWithStatusOf(err, 409);
}
export type NotFoundError = CreateError & { status: 404 };
export function isNotFound(err: unknown): err is NotFoundError {
return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
err: unknown,
status: number
): err is CreateError & { status: number } {
if (err instanceof CreateError) {
return err.status === status;
}
return false;
}

View File

@@ -14,7 +14,7 @@
"editor.foreground": "#dae3e3",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#00818480",
"editorCursor.foreground": "#dae3e3",
"editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#171e21",
"editorWidget.foreground": "#dae3e3",
@@ -67,8 +67,7 @@
"tree.indentGuidesStroke": "#374146",
"tab.unfocusedActiveForeground": "#dae3e3",
"tab.inactiveBackground": "#171e21",
"textLink.foreground": "#0ca1a6",
"errorForeground": "#df7365"
"textLink.foreground": "#0ca1a6"
},
"tokenColors": [
{

View File

@@ -14,7 +14,7 @@
"editor.foreground": "#4e5b61",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#7fcbcdb3",
"editorCursor.foreground": "#4e5b61",
"editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#f7f9f9",
"editorWidget.foreground": "#4e5b61",
@@ -67,8 +67,7 @@
"tree.indentGuidesStroke": "#dae3e3",
"tab.unfocusedActiveForeground": "#4e5b61",
"tab.inactiveBackground": "#ecf1f1",
"textLink.foreground": "#008184",
"errorForeground": "#df7365"
"textLink.foreground": "#008184"
},
"tokenColors": [
{

View File

@@ -1,15 +0,0 @@
import type {
MessageBoxOptions,
MessageBoxReturnValue,
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue,
} from '../electron-common/electron-arduino';
export const DialogService = Symbol('DialogService');
export interface DialogService {
showMessageBox(options: MessageBoxOptions): Promise<MessageBoxReturnValue>;
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogReturnValue>;
showSaveDialog(options: SaveDialogOptions): Promise<SaveDialogReturnValue>;
}

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import React from '@theia/core/shared/react';
import * as React from '@theia/core/shared/react';
export const CertificateAddComponent = ({
addCertificate,

View File

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

View File

@@ -1,27 +1,20 @@
import { nls } from '@theia/core/lib/common/nls';
import React from '@theia/core/shared/react';
import * as React from '@theia/core/shared/react';
import Tippy from '@tippyjs/react';
import type { BoardList } from '../../../common/protocol/board-list';
import {
boardIdentifierEquals,
portIdentifierEquals,
} from '../../../common/protocol/boards-service';
import { CertificateAddComponent } from './certificate-add-new';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';
import {
BoardOptionValue,
SelectBoardComponent,
} from './select-board-components';
import { SelectBoardComponent } from './select-board-components';
import { CertificateAddComponent } from './certificate-add-new';
import { nls } from '@theia/core/lib/common';
export const CertificateUploaderComponent = ({
boardList,
availableBoards,
certificates,
addCertificate,
updatableFqbns,
uploadCertificates,
openContextMenu,
}: {
boardList: BoardList;
availableBoards: AvailableBoard[];
certificates: string[];
addCertificate: (cert: string) => void;
updatableFqbns: string[];
@@ -40,15 +33,11 @@ export const CertificateUploaderComponent = ({
const [selectedCerts, setSelectedCerts] = React.useState<string[]>([]);
const [selectedItem, setSelectedItem] =
React.useState<BoardOptionValue | null>(null);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const installCertificates = async () => {
if (!selectedItem) {
return;
}
const board = selectedItem.board;
if (!board.fqbn) {
if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) {
return;
}
@@ -56,8 +45,8 @@ export const CertificateUploaderComponent = ({
try {
await uploadCertificates(
board.fqbn,
selectedItem.port.address,
selectedBoard.fqbn,
selectedBoard.port.address,
selectedCerts
);
setInstallFeedback('ok');
@@ -66,25 +55,17 @@ export const CertificateUploaderComponent = ({
}
};
const onItemSelect = React.useCallback(
(item: BoardOptionValue | null) => {
if (!item) {
return;
}
const board = item.board;
const port = item.port;
const selectedBoard = selectedItem?.board;
const selectedPort = selectedItem?.port;
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (
!boardIdentifierEquals(board, selectedBoard) ||
!portIdentifierEquals(port, selectedPort)
) {
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setSelectedItem(item);
setSelectedBoard(board);
}
},
[selectedItem]
[selectedBoard]
);
return (
@@ -144,10 +125,10 @@ export const CertificateUploaderComponent = ({
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
boardList={boardList}
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onItemSelect={onItemSelect}
selectedItem={selectedItem}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
@@ -186,7 +167,7 @@ export const CertificateUploaderComponent = ({
type="button"
className="theia-button primary install-cert-btn"
onClick={installCertificates}
disabled={selectedCerts.length === 0 || !selectedItem}
disabled={selectedCerts.length === 0 || !selectedBoard}
>
{nls.localize('arduino/certificate/upload', 'Upload')}
</button>

View File

@@ -1,51 +1,62 @@
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { nls } from '@theia/core/lib/common/nls';
import type { Message } from '@theia/core/shared/@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import * as React from '@theia/core/shared/react';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import React from '@theia/core/shared/react';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
import { createBoardList } from '../../../common/protocol/board-list';
import { ArduinoPreferences } from '../../arduino-preferences';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import { CertificateUploaderComponent } from './certificate-uploader-component';
import { ArduinoPreferences } from '../../arduino-preferences';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { certificateList, sanifyCertString } from './utils';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadCertificateDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
private readonly preferenceService: PreferenceService;
protected readonly preferenceService: PreferenceService;
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
protected readonly commandRegistry: CommandRegistry;
@inject(ArduinoFirmwareUploader)
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
private certificates: string[] = [];
private updatableFqbns: string[] = [];
private boardList = createBoardList({});
protected certificates: string[] = [];
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
busyCallback = (busy: boolean) => {
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoPreferences.ready.then(() => {
@@ -70,8 +81,8 @@ export class UploadCertificateDialogWidget extends ReactWidget {
})
);
this.boardsServiceProvider.onBoardListDidChange((boardList) => {
this.boardList = boardList;
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
@@ -115,7 +126,7 @@ export class UploadCertificateDialogWidget extends ReactWidget {
protected render(): React.ReactNode {
return (
<CertificateUploaderComponent
boardList={this.boardList}
availableBoards={this.availableBoards}
certificates={this.certificates}
updatableFqbns={this.updatableFqbns}
addCertificate={this.addCertificate.bind(this)}
@@ -132,7 +143,7 @@ export class UploadCertificateDialogProps extends DialogProps {}
@injectable()
export class UploadCertificateDialog extends AbstractDialog<void> {
@inject(UploadCertificateDialogWidget)
private readonly widget: UploadCertificateDialogWidget;
protected readonly widget: UploadCertificateDialogWidget;
private busy = false;

View File

@@ -1,36 +1,37 @@
import { nls } from '@theia/core/lib/common';
import React from '@theia/core/shared/react';
import type {
BoardList,
BoardListItemWithBoard,
} from '../../../common/protocol/board-list';
import * as React from '@theia/core/shared/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
export type BoardOptionValue = BoardListItemWithBoard;
type BoardOption = { value: BoardOptionValue | undefined; label: string };
type BoardOption = { value: string; label: string };
export const SelectBoardComponent = ({
boardList,
availableBoards,
updatableFqbns,
onItemSelect,
selectedItem,
onBoardSelect,
selectedBoard,
busy,
}: {
boardList: BoardList;
availableBoards: AvailableBoard[];
updatableFqbns: string[];
onItemSelect: (item: BoardOptionValue | null) => void;
selectedItem: BoardOptionValue | null;
onBoardSelect: (board: AvailableBoard | null) => void;
selectedBoard: AvailableBoard | null;
busy: boolean;
}): React.ReactElement => {
const [selectOptions, setSelectOptions] = React.useState<BoardOption[]>([]);
const [selectItemPlaceholder, setSelectBoardPlaceholder] = React.useState('');
const [selectBoardPlaceholder, setSelectBoardPlaceholder] =
React.useState('');
const selectOption = React.useCallback(
(boardOpt: BoardOption | null) => {
onItemSelect(boardOpt?.value ?? null);
(boardOpt: BoardOption) => {
onBoardSelect(
(boardOpt &&
availableBoards.find((board) => board.fqbn === boardOpt.value)) ||
null
);
},
[onItemSelect]
[availableBoards, onBoardSelect]
);
React.useEffect(() => {
@@ -43,28 +44,26 @@ export const SelectBoardComponent = ({
'arduino/certificate/selectBoard',
'Select a board...'
);
const updatableBoards = boardList.boards.filter((item) => {
const fqbn = item.board.fqbn;
return fqbn && updatableFqbns.includes(fqbn);
});
let selBoard = -1;
const boardOptions: BoardOption[] = updatableBoards.map((item, i) => {
if (selectedItem === item) {
const updatableBoards = availableBoards.filter(
(board) => board.port && board.fqbn && updatableFqbns.includes(board.fqbn)
);
const boardsList: BoardOption[] = updatableBoards.map((board, i) => {
if (board.selected) {
selBoard = i;
}
return {
label: nls.localize(
'arduino/certificate/boardAtPort',
'{0} at {1}',
item.board.name,
item.port.address ?? ''
board.name,
board.port?.address ?? ''
),
value: item,
value: board.fqbn || '',
};
});
if (boardOptions.length === 0) {
if (boardsList.length === 0) {
placeholderTxt = nls.localize(
'arduino/certificate/noSupportedBoardConnected',
'No supported board connected'
@@ -72,29 +71,32 @@ export const SelectBoardComponent = ({
}
setSelectBoardPlaceholder(placeholderTxt);
setSelectOptions(boardOptions);
setSelectOptions(boardsList);
if (selectedItem) {
selBoard = updatableBoards.indexOf(selectedItem);
if (selectedBoard) {
selBoard = boardsList
.map((boardOpt) => boardOpt.value)
.indexOf(selectedBoard.fqbn || '');
}
selectOption(boardOptions[selBoard] || null);
}, [busy, boardList, selectOption, updatableFqbns, selectedItem]);
selectOption(boardsList[selBoard] || null);
}, [busy, availableBoards, selectOption, updatableFqbns, selectedBoard]);
return (
<ArduinoSelect
id="board-select"
menuPosition="fixed"
isDisabled={selectOptions.length === 0 || busy}
placeholder={selectItemPlaceholder}
placeholder={selectBoardPlaceholder}
options={selectOptions}
value={
(selectedItem && {
value: selectedItem,
(selectedBoard && {
value: selectedBoard.fqbn,
label: nls.localize(
'arduino/certificate/boardAtPort',
'{0} at {1}',
selectedItem.board.name,
selectedItem.port.address ?? ''
selectedBoard.name,
selectedBoard.port?.address ?? ''
),
}) ||
null

View File

@@ -1,14 +1,12 @@
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { nls } from '@theia/core/lib/common/nls';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import React from '@theia/core/shared/react';
import { CreateApi } from '../create/create-api';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { clipboard } from 'electron';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { CreateApi } from '../create/create-api';
import { nls } from '@theia/core/lib/common';
const RadioButton = (props: {
id: string;
@@ -37,18 +35,15 @@ export const ShareSketchComponent = ({
treeNode,
createApi,
domain = 'https://create.arduino.cc',
writeClipboard,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
treeNode: any;
createApi: CreateApi;
domain?: string;
writeClipboard: (text: string) => MaybePromise<void>;
}): React.ReactElement => {
const [loading, setLoading] = React.useState<boolean>(false);
const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
setLoading(true);
setloading(true);
const sketch = await createApi.editSketch({
id: treeNode.sketchId,
params: {
@@ -57,7 +52,7 @@ export const ShareSketchComponent = ({
});
// setPublicVisibility(sketch.is_public);
treeNode.isPublic = sketch.is_public;
setLoading(false);
setloading(false);
};
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
@@ -105,7 +100,7 @@ export const ShareSketchComponent = ({
className="theia-input"
/>
<button
onClick={() => writeClipboard(sketchLink)}
onClick={() => clipboard.writeText(sketchLink)}
value="copy"
className="theia-button secondary"
>
@@ -126,52 +121,44 @@ export const ShareSketchComponent = ({
);
};
@injectable()
export class ShareSketchWidget extends ReactWidget {
private readonly writeClipboard = (text: string) =>
this.clipboardService.writeText(text);
constructor(
private treeNode: TreeNode,
private createApi: CreateApi,
private clipboardService: ClipboardService
) {
constructor(private treeNode: any, private createApi: CreateApi) {
super();
}
protected override render(): React.ReactNode {
protected render(): React.ReactNode {
return (
<ShareSketchComponent
treeNode={this.treeNode}
createApi={this.createApi}
writeClipboard={this.writeClipboard}
/>
);
}
}
@injectable()
export class ShareSketchDialogProps extends DialogProps {
readonly node: TreeNode;
readonly node: any;
readonly createApi: CreateApi;
readonly clipboardService: ClipboardService;
}
@injectable()
export class ShareSketchDialog extends AbstractDialog<void> {
protected widget: ShareSketchWidget;
constructor(protected override readonly props: ShareSketchDialogProps) {
constructor(
@inject(ShareSketchDialogProps)
protected override readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
this.widget = new ShareSketchWidget(
props.node,
props.createApi,
props.clipboardService
);
this.widget = new ShareSketchWidget(props.node, props.createApi);
}
override get value(): void {
get value(): void {
return;
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);

View File

@@ -1,31 +1,24 @@
import { nls } from '@theia/core/lib/common';
import React from '@theia/core/shared/react';
import {
boardIdentifierEquals,
Port,
portIdentifierEquals,
} from '../../../common/protocol';
import * as React from '@theia/core/shared/react';
import { Port } from '../../../common/protocol';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import type {
BoardList,
BoardListItemWithBoard,
} from '../../../common/protocol/board-list';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SelectBoardComponent } from '../certificate-uploader/select-board-components';
type FirmwareOption = { value: string; label: string };
export const FirmwareUploaderComponent = ({
boardList,
availableBoards,
firmwareUploader,
updatableFqbns,
flashFirmware,
isOpen,
}: {
boardList: BoardList;
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
@@ -38,8 +31,8 @@ export const FirmwareUploaderComponent = ({
'ok' | 'fail' | 'installing' | null
>(null);
const [selectedItem, setSelectedItem] =
React.useState<BoardListItemWithBoard | null>(null);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const [availableFirmwares, setAvailableFirmwares] = React.useState<
FirmwareInfo[]
@@ -57,14 +50,13 @@ export const FirmwareUploaderComponent = ({
const fetchFirmwares = React.useCallback(async () => {
setInstallFeedback(null);
setFirmwaresFetching(true);
if (!selectedItem) {
if (!selectedBoard) {
return;
}
// fetch the firmwares for the selected board
const board = selectedItem.board;
const firmwaresForFqbn = await firmwareUploader.availableFirmwares(
board.fqbn || ''
selectedBoard.fqbn || ''
);
setAvailableFirmwares(firmwaresForFqbn);
@@ -77,7 +69,7 @@ export const FirmwareUploaderComponent = ({
if (firmwaresForFqbn.length > 0) setSelectedFirmware(firmwaresOpts[0]);
setFirmwaresFetching(false);
}, [firmwareUploader, selectedItem]);
}, [firmwareUploader, selectedBoard]);
const installFirmware = React.useCallback(async () => {
setInstallFeedback('installing');
@@ -86,41 +78,30 @@ export const FirmwareUploaderComponent = ({
(firmware) => firmware.firmware_version === selectedFirmware?.value
);
const selectedBoard = selectedItem?.board;
const selectedPort = selectedItem?.port;
try {
const installStatus =
firmwareToFlash &&
selectedBoard &&
selectedPort &&
(await flashFirmware(firmwareToFlash, selectedPort));
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {
setInstallFeedback('fail');
}
}, [selectedItem, selectedFirmware, availableFirmwares, flashFirmware]);
}, [firmwareUploader, selectedBoard, selectedFirmware, availableFirmwares]);
const onItemSelect = React.useCallback(
(item: BoardListItemWithBoard | null) => {
if (!item) {
return;
}
const board = item.board;
const port = item.port;
const selectedBoard = selectedItem?.board;
const selectedPort = selectedItem?.port;
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (
!boardIdentifierEquals(board, selectedBoard) ||
!portIdentifierEquals(port, selectedPort)
) {
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setAvailableFirmwares([]);
setSelectedItem(item);
setSelectedBoard(board);
}
},
[selectedItem]
[selectedBoard]
);
return (
@@ -134,10 +115,10 @@ export const FirmwareUploaderComponent = ({
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
boardList={boardList}
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onItemSelect={onItemSelect}
selectedItem={selectedItem}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
@@ -145,7 +126,7 @@ export const FirmwareUploaderComponent = ({
type="button"
className="theia-button secondary"
disabled={
selectedItem === null ||
selectedBoard === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
@@ -169,14 +150,14 @@ export const FirmwareUploaderComponent = ({
id="firmware-select"
menuPosition="fixed"
isDisabled={
!selectedItem ||
!selectedBoard ||
firmwaresFetching ||
installFeedback === 'installing'
}
options={firmwareOptions}
value={selectedFirmware}
tabSelectsValue={false}
onChange={(value: FirmwareOption | null) => {
onChange={(value) => {
if (value) {
setInstallFeedback(null);
setSelectedFirmware(value);

View File

@@ -1,36 +1,99 @@
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import type { Message } from '@theia/core/shared/@phosphor/messaging';
import * as React from '@theia/core/shared/react';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import React from '@theia/core/shared/react';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import type { Port } from '../../../common/protocol/boards-service';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { UploadFirmware } from '../../contributions/upload-firmware';
import { ReactDialog } from '../../theia/dialogs/dialogs';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.appStatusService.reachedState('ready').then(async () => {
const fqbns = await this.arduinoFirmwareUploader.updatableBoards();
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
protected render(): React.ReactNode {
return (
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
);
}
}
@injectable()
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends ReactDialog<void> {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
export class UploadFirmwareDialog extends AbstractDialog<void> {
@inject(UploadFirmwareDialogWidget)
protected readonly widget: UploadFirmwareDialogWidget;
private updatableFqbns: string[] = [];
private isOpen = new Object();
private busy = false;
constructor(
@@ -43,44 +106,32 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
this.acceptButton = undefined;
}
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(async () => {
const fqbns = await this.arduinoFirmwareUploader.updatableBoards();
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceProvider.onBoardListDidChange(() => this.update());
}
get value(): void {
return;
}
protected override render(): React.ReactNode {
return (
<div>
<form>
<FirmwareUploaderComponent
boardList={this.boardsServiceProvider.boardList}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
</div>
);
}
protected override onAfterAttach(msg: Message): void {
const firstButton = this.node.querySelector('button');
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();
}
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
@@ -89,11 +140,11 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
if (this.busy) {
return;
}
this.widget.close();
super.close();
this.isOpen = new Object();
}
private busyCallback(busy: boolean): void {
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
@@ -101,11 +152,4 @@ export class UploadFirmwareDialog extends ReactDialog<void> {
this.closeCrossNode.classList.remove('disabled');
}
}
private flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
}

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