Compare commits

..

6 Commits
main ... 2.3.1

Author SHA1 Message Date
github-actions[bot]
c54fbc80e4 Updated translation files 2024-02-14 13:36:54 +01:00
Akos Kitta
bb1dc74e33 fix(security): update all vulnerable dependencies
Resolutions:
 - `nano@^10.1.3`,
 - `msgpackr@^1.10.1`,
 - `axios@^1.6.7`

Fixes:
 - [GHSA-wf5p-g6vw-rhxx](https://github.com/advisories/GHSA-wf5p-g6vw-rhxx) (`CVE-2023-45857`)
 - [GHSA-jchw-25xp-jwwc](https://github.com/advisories/GHSA-jchw-25xp-jwwc) (`CVE-2023-26159`)
 - [GHSA-7hpj-7hhx-2fgx](https://github.com/advisories/GHSA-7hpj-7hhx-2fgx) (`CVE-2023-52079`)

Ref: nrwl/nx#20493
Ref: eclipse-theia/theia#13365

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-14 13:32:04 +01:00
Akos Kitta
470db5606e fix: debug widget layout updates
Use customized `PanelLayout#removeWidget` and
`PanelLayout#insertWidget` logic for the layout
updates. The customized functions ensure no side
effect if adding/removing the widget to/from the
layout but it's already present/absent.

Unlike the default [`PanelLayout#removeWidget`](9f5e11025b/packages/widgets/src/panellayout.ts (L154-L156))
behavior, do not try to remove the widget if it's
not present (has a negative index). Otherwise,
required widgets might be removed based on the
default [`ArrayExt#removeAt`](9f5e11025b/packages/algorithm/src/array.ts (L1075-L1077))
behavior.

Closes: arduino/arduino-ide#2354

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-14 13:31:37 +01:00
per1234
8c1ffe9c5f Use arduino/setup-task@v1 action for Linux build job
Unfortunately the latest v2 version of the arduino/setup-task action used to install the Task task runner tool in the
runner machine has a dependency on a higher version of glibc than is provided by the Linux container. For this reason,
the workflow is configured to use arduino/setup-task@v1 for the Linux build job.

We will receive pull requests from Dependabot offering to update this outdated action dependency at each subsequent
major version release of the action (which are not terribly frequent). We must decline the bump of the action in that
specific step, but we can accept the bumps of all other usages of the action in the workflows. Dependabot remembers when
you decline a bump so this should not be too bothersome.
2024-02-14 13:31:27 +01:00
dependabot[bot]
958e1591cf build(deps): Bump arduino/setup-task from 1 to 2
Bumps [arduino/setup-task](https://github.com/arduino/setup-task) from 1 to 2.
- [Release notes](https://github.com/arduino/setup-task/releases)
- [Commits](https://github.com/arduino/setup-task/compare/v1...v2)

---
updated-dependencies:
- dependency-name: arduino/setup-task
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-14 13:31:18 +01:00
Akos Kitta
11da49ac18 chore: switch to version 2.3.1 after the release
To produce a correctly versioned nightly build.
See the [docs](1b9c7e93e0/docs/internal/release-procedure.md (7-%EF%B8%8F-bump-version-metadata-of-packages)) for more details.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2024-02-14 13:31:11 +01:00
212 changed files with 11479 additions and 28173 deletions

View File

@ -1,28 +1,43 @@
# The Arduino IDE Linux build workflow job runs in this container. # The Arduino IDE Linux build workflow job runs in this container.
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# See: https://hub.docker.com/_/ubuntu/tags FROM ubuntu:18.04
FROM ubuntu:18.10
# This is required in order to use the Ubuntu package repositories for EOL Ubuntu versions: # See: https://unofficial-builds.nodejs.org/download/release/
# https://help.ubuntu.com/community/EOLUpgrades#Update_sources.list ARG node_version="18.17.1"
RUN \
sed \
--in-place \
--regexp-extended \
--expression='s/([a-z]{2}\.)?archive.ubuntu.com|security.ubuntu.com/old-releases.ubuntu.com/g' \
"/etc/apt/sources.list"
RUN \ RUN \
apt-get \ apt-get \
--yes \ --yes \
update update
# This is required to get add-apt-repository
RUN \ RUN \
apt-get \ apt-get \
--yes \ --yes \
install \ install \
"git" "software-properties-common=0.96.24.32.22"
# Install Git
# The PPA is required to get a modern version of Git. The version in the Ubuntu 18.04 package repository is 2.17.1,
# while action/checkout@v3 requires 2.18 or higher.
RUN \
add-apt-repository \
--yes \
"ppa:git-core/ppa" && \
apt-get \
--yes \
update && \
\
apt-get \
--yes \
install \
"git" && \
\
apt-get \
--yes \
purge \
"software-properties-common"
# The repository path must be added to safe.directory, otherwise any Git operations on it would fail with a # The repository path must be added to safe.directory, otherwise any Git operations on it would fail with a
# "dubious ownership" error. actions/checkout configures this, but it is not applied to containers. # "dubious ownership" error. actions/checkout configures this, but it is not applied to containers.
@ -36,12 +51,18 @@ ENV \
# Install Python # Install Python
# The Python installed by actions/setup-python has dependency on a higher version of glibc than available in the # The Python installed by actions/setup-python has dependency on a higher version of glibc than available in the
# container. # ubuntu:18.04 container.
RUN \ RUN \
apt-get \ apt-get \
--yes \ --yes \
install \ install \
"python3.7-minimal=3.7.3-2~18.10" "python3.8-minimal=3.8.0-3ubuntu1~18.04.2" && \
\
ln \
--symbolic \
--force \
"$(which python3.8)" \
"/usr/bin/python3"
# Install Theia's package dependencies # Install Theia's package dependencies
# These are pre-installed in the GitHub Actions hosted runner machines. # These are pre-installed in the GitHub Actions hosted runner machines.
@ -49,15 +70,43 @@ RUN \
apt-get \ apt-get \
--yes \ --yes \
install \ install \
"libsecret-1-dev=0.18.6-3" \ "libsecret-1-dev=0.18.6-1" \
"libx11-dev=2:1.6.7-1" \ "libx11-dev=2:1.6.4-3ubuntu0.4" \
"libxkbfile-dev=1:1.0.9-2" "libxkbfile-dev=1:1.0.9-2"
# Target python3 symlink to Python 3.7 installation. It would otherwise target version 3.6 due to the installation of # Install Node.js
# the `python3` package as a transitive dependency. # It is necessary to use the "unofficial" linux-x64-glibc-217 build because the official Node.js 18.x is dynamically
# linked against glibc 2.28, while Ubuntu 18.04 has glibc 2.27.
ARG node_installation_path="/tmp/node-installation"
ARG artifact_name="node-v${node_version}-linux-x64-glibc-217"
RUN \ RUN \
ln \ mkdir "$node_installation_path" && \
--symbolic \ cd "$node_installation_path" && \
--force \ \
"$(which python3.7)" \ apt-get \
"/usr/bin/python3" --yes \
install \
"wget=1.19.4-1ubuntu2.2" && \
\
archive_name="${artifact_name}.tar.xz" && \
wget \
"https://unofficial-builds.nodejs.org/download/release/v${node_version}/${archive_name}" && \
\
apt-get \
--yes \
purge \
"wget" && \
\
tar \
--file="$archive_name" \
--extract && \
rm "$archive_name"
ENV PATH="${PATH}:${node_installation_path}/${artifact_name}/bin"
# Install Yarn
# Yarn is pre-installed in the GitHub Actions hosted runner machines.
RUN \
npm \
install \
--global \
"yarn@1.22.19"

View File

@ -45,41 +45,31 @@ on:
env: env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml # See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21' GO_VERSION: '1.19'
# See: https://github.com/actions/setup-node/#readme # See: https://github.com/actions/setup-node/#readme
NODE_VERSION: '18.17' NODE_VERSION: '18.17'
YARN_VERSION: '1.22' JOB_TRANSFER_ARTIFACT: build-artifacts
JOB_TRANSFER_ARTIFACT_PREFIX: build-artifacts-
CHANGELOG_ARTIFACTS: changelog CHANGELOG_ARTIFACTS: changelog
STAGED_CHANNEL_FILE_ARTIFACT_PREFIX: staged-channel-file- STAGED_CHANNEL_FILES_ARTIFACT: staged-channel-files
BASE_BUILD_DATA: | BASE_BUILD_DATA: |
- config: - config:
# Human identifier for the job. # Human identifier for the job.
name: Windows name: Windows
runs-on: [self-hosted, windows-sign-pc] runs-on: windows-2019
# The value is a string representing a JSON document. # The value is a string representing a JSON document.
# Setting this to null causes the job to run directly in the runner machine instead of in a container. # Setting this to null causes the job to run directly in the runner machine instead of in a container.
container: | container: |
null null
# Name of the secret that contains the certificate. # Name of the secret that contains the certificate.
certificate-secret: INSTALLER_CERT_WINDOWS_CER certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
# Name of the secret that contains the certificate password. # Name of the secret that contains the certificate password.
certificate-password-secret: INSTALLER_CERT_WINDOWS_PASSWORD certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
# File extension for the certificate. # File extension for the certificate.
certificate-extension: pfx certificate-extension: pfx
# Container for windows cert signing
certificate-container: INSTALLER_CERT_WINDOWS_CONTAINER
# Arbitrary identifier used to give the workflow artifact uploaded by each "build" matrix job a unique name.
job-transfer-artifact-suffix: Windows_64bit
# Quoting on the value is required here to allow the same comparison expression syntax to be used for this # Quoting on the value is required here to allow the same comparison expression syntax to be used for this
# and the companion needs.select-targets.outputs.merge-channel-files property (output values always have string # and the companion needs.select-targets.outputs.merge-channel-files property (output values always have string
# type). # type).
mergeable-channel-file: 'false' mergeable-channel-file: 'false'
# as this runs on a self hosted runner, we need to avoid building with the default working directory path,
# otherwise paths in the build job will be too long for `light.exe`
# we use the below as a Symbolic link (just changing the wd will break the checkout action)
# this is a work around (see: https://github.com/actions/checkout/issues/197).
working-directory: 'C:\a'
artifacts: artifacts:
- path: '*Windows_64bit.exe' - path: '*Windows_64bit.exe'
name: Windows_X86-64_interactive_installer name: Windows_X86-64_interactive_installer
@ -94,7 +84,6 @@ env:
{ {
\"image\": \"ghcr.io/arduino/arduino-ide/linux:main\" \"image\": \"ghcr.io/arduino/arduino-ide/linux:main\"
} }
job-transfer-artifact-suffix: Linux_64bit
mergeable-channel-file: 'false' mergeable-channel-file: 'false'
artifacts: artifacts:
- path: '*Linux_64bit.zip' - path: '*Linux_64bit.zip'
@ -103,7 +92,7 @@ env:
name: Linux_X86-64_app_image name: Linux_X86-64_app_image
- config: - config:
name: macOS x86 name: macOS x86
runs-on: macos-13 runs-on: macos-latest
container: | container: |
null null
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from: # APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
@ -111,32 +100,27 @@ env:
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12 certificate-extension: p12
job-transfer-artifact-suffix: macOS_64bit
mergeable-channel-file: 'true' mergeable-channel-file: 'true'
artifacts: artifacts:
- path: '*macOS_64bit.dmg' - path: '*macOS_64bit.dmg'
name: macOS_X86-64_dmg name: macOS_X86-64_dmg
- path: '*macOS_64bit.zip' - path: '*macOS_64bit.zip'
name: macOS_X86-64_zip name: macOS_X86-64_zip
PAID_RUNNER_BUILD_DATA: |
- config: - config:
name: macOS ARM name: macOS ARM
runs-on: macos-latest runs-on: macos-latest-xlarge
container: | container: |
null null
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
certificate-password-secret: KEYCHAIN_PASSWORD certificate-password-secret: KEYCHAIN_PASSWORD
certificate-extension: p12 certificate-extension: p12
job-transfer-artifact-suffix: macOS_arm64
mergeable-channel-file: 'true' mergeable-channel-file: 'true'
artifacts: artifacts:
- path: '*macOS_arm64.dmg' - path: '*macOS_arm64.dmg'
name: macOS_arm64_dmg name: macOS_arm64_dmg
- path: '*macOS_arm64.zip' - path: '*macOS_arm64.zip'
name: macOS_arm64_zip name: macOS_arm64_zip
PAID_RUNNER_BUILD_DATA: |
# This system was implemented to allow selective use of paid GitHub-hosted runners, due to the Apple Silicon runner
# incurring a charge at that time. Free Apple Silicon runners are now available so the configuration was moved to
# `BASE_BUILD_DATA`, but the system was left in place for future use.
jobs: jobs:
run-determination: run-determination:
@ -172,7 +156,6 @@ jobs:
is-nightly: ${{ steps.determination.outputs.is-nightly }} is-nightly: ${{ steps.determination.outputs.is-nightly }}
channel-name: ${{ steps.determination.outputs.channel-name }} channel-name: ${{ steps.determination.outputs.channel-name }}
publish-to-s3: ${{ steps.determination.outputs.publish-to-s3 }} publish-to-s3: ${{ steps.determination.outputs.publish-to-s3 }}
environment: production
permissions: {} permissions: {}
steps: steps:
- name: Determine the type of build - name: Determine the type of build
@ -204,7 +187,7 @@ jobs:
echo "is-nightly=$is_nightly" >> $GITHUB_OUTPUT echo "is-nightly=$is_nightly" >> $GITHUB_OUTPUT
echo "channel-name=$channel_name" >> $GITHUB_OUTPUT echo "channel-name=$channel_name" >> $GITHUB_OUTPUT
# Only attempt upload to Amazon S3 if the credentials are available. # Only attempt upload to Amazon S3 if the credentials are available.
echo "publish-to-s3=${{ secrets.AWS_ROLE_ARN != '' }}" >> $GITHUB_OUTPUT echo "publish-to-s3=${{ secrets.AWS_SECRET_ACCESS_KEY != '' }}" >> $GITHUB_OUTPUT
select-targets: select-targets:
needs: build-type-determination needs: build-type-determination
@ -240,7 +223,7 @@ jobs:
) | \ ) | \
yq \ yq \
--output-format json \ --output-format json \
'map(.artifacts[] + (.config | pick(["job-transfer-artifact-suffix"])))' '[.[].artifacts.[]]'
)" )"
# The build matrix produces two macOS jobs (x86 and ARM) so the "channel update info files" # The build matrix produces two macOS jobs (x86 and ARM) so the "channel update info files"
@ -259,7 +242,7 @@ jobs:
echo "${{ env.BASE_BUILD_DATA }}" | \ echo "${{ env.BASE_BUILD_DATA }}" | \
yq \ yq \
--output-format json \ --output-format json \
'map(.artifacts[] + (.config | pick(["job-transfer-artifact-suffix"])))' '[.[].artifacts.[]]'
)" )"
merge_channel_files="false" merge_channel_files="false"
@ -287,16 +270,6 @@ jobs:
env: env:
# Location of artifacts generated by build. # Location of artifacts generated by build.
BUILD_ARTIFACTS_PATH: electron-app/dist/build-artifacts BUILD_ARTIFACTS_PATH: electron-app/dist/build-artifacts
# to skip passing signing credentials to electron-builder
IS_WINDOWS_CONFIG: ${{ matrix.config.name == 'Windows' }}
INSTALLER_CERT_WINDOWS_CER: "/tmp/cert.cer"
# We are hardcoding the path for signtool because is not present on the windows PATH env var by default.
# Keep in mind that this path could change when upgrading to a new runner version
SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x86/signtool.exe"
WIN_CERT_PASSWORD: ${{ secrets[matrix.config.certificate-password-secret] }}
WIN_CERT_CONTAINER_NAME: ${{ secrets[matrix.config.certificate-container] }}
PUPPETEER_SKIP_DOWNLOAD: true
strategy: strategy:
matrix: matrix:
config: ${{ fromJson(needs.select-targets.outputs.build-matrix) }} config: ${{ fromJson(needs.select-targets.outputs.build-matrix) }}
@ -310,66 +283,76 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- name: Symlink custom working directory
shell: cmd
if: runner.os == 'Windows' && matrix.config.working-directory
run: |
if not exist "${{ matrix.config.working-directory }}" mklink /d "${{ matrix.config.working-directory }}" "C:\actions-runner\_work\arduino-ide\arduino-ide"
- name: Checkout - name: Checkout
if: fromJSON(matrix.config.container) == null
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Checkout
# actions/checkout@v4 has dependency on a higher version of glibc than available in the Linux container.
if: fromJSON(matrix.config.container) != null
uses: actions/checkout@v3
- name: Install Node.js - name: Install Node.js
if: runner.name != 'WINDOWS-SIGN-PC' if: fromJSON(matrix.config.container) == null
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
# Yarn is a prerequisite for the action's cache feature, so caching should be disabled when running in the cache: 'yarn'
# container where Yarn is not pre-installed.
cache: ${{ fromJSON(matrix.config.container) == null && 'yarn' || null }}
- name: Install Yarn
if: runner.name != 'WINDOWS-SIGN-PC'
run: |
npm \
install \
--global \
"yarn@${{ env.YARN_VERSION }}"
- name: Install Python 3.x - name: Install Python 3.x
if: fromJSON(matrix.config.container) == null && runner.name != 'WINDOWS-SIGN-PC' if: fromJSON(matrix.config.container) == null
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.11.x' python-version: '3.11.x'
- name: Install Go - name: Install Go
if: runner.name != 'WINDOWS-SIGN-PC' if: fromJSON(matrix.config.container) == null
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Install Go
# actions/setup-go@v5 has dependency on a higher version of glibc than available in the Linux container.
if: fromJSON(matrix.config.container) != null
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile - name: Install Taskfile
if: runner.name != 'WINDOWS-SIGN-PC' if: fromJSON(matrix.config.container) == null
uses: arduino/setup-task@v2 uses: arduino/setup-task@v2
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x version: 3.x
- name: Install Taskfile
# actions/setup-task@v2 has dependency on a higher version of glibc than available in the Linux container.
if: fromJSON(matrix.config.container) != null
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Package - name: Package
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }} AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }} AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }} AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }} IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }}
IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }} IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }}
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }} CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
working-directory: ${{ matrix.config.working-directory || './' }} # The CREATE_* environment vars are only used to run tests. These secrets are optional. Dependent tests will
# be skipped if not available.
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
run: | run: |
# See: https://www.electron.build/code-signing # See: https://www.electron.build/code-signing
if [ $CAN_SIGN = false ] || [ $IS_WINDOWS_CONFIG = true ]; then if [ $CAN_SIGN = false ]; then
echo "Skipping the app signing: certificate not provided." echo "Skipping the app signing: certificate not provided."
else else
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}" export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
@ -379,9 +362,13 @@ jobs:
fi fi
npx node-gyp install npx node-gyp install
yarn install yarn install --immutable
yarn --cwd arduino-ide-extension build 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 rebuild
yarn --cwd electron-app build yarn --cwd electron-app build
yarn --cwd electron-app package yarn --cwd electron-app package
@ -392,7 +379,6 @@ jobs:
if: > if: >
needs.select-targets.outputs.merge-channel-files == 'true' && needs.select-targets.outputs.merge-channel-files == 'true' &&
matrix.config.mergeable-channel-file == 'true' matrix.config.mergeable-channel-file == 'true'
working-directory: ${{ matrix.config.working-directory || './' }}
run: | run: |
staged_channel_files_path="${{ runner.temp }}/staged-channel-files" staged_channel_files_path="${{ runner.temp }}/staged-channel-files"
mkdir "$staged_channel_files_path" mkdir "$staged_channel_files_path"
@ -405,26 +391,20 @@ jobs:
echo "STAGED_CHANNEL_FILES_PATH=$staged_channel_files_path" >> "$GITHUB_ENV" echo "STAGED_CHANNEL_FILES_PATH=$staged_channel_files_path" >> "$GITHUB_ENV"
- name: Upload staged-for-merge channel file artifact - name: Upload staged-for-merge channel file artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
if: > if: >
needs.select-targets.outputs.merge-channel-files == 'true' && needs.select-targets.outputs.merge-channel-files == 'true' &&
matrix.config.mergeable-channel-file == 'true' matrix.config.mergeable-channel-file == 'true'
with: with:
if-no-files-found: error if-no-files-found: error
name: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}${{ matrix.config.job-transfer-artifact-suffix }} name: ${{ env.STAGED_CHANNEL_FILES_ARTIFACT }}
path: ${{ matrix.config.working-directory && format('{0}/{1}', matrix.config.working-directory, env.STAGED_CHANNEL_FILES_PATH) || env.STAGED_CHANNEL_FILES_PATH }} path: ${{ env.STAGED_CHANNEL_FILES_PATH }}
- name: Upload builds to job transfer artifact - name: Upload [GitHub Actions]
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}${{ matrix.config.job-transfer-artifact-suffix }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ matrix.config.working-directory && format('{0}/{1}', matrix.config.working-directory, env.BUILD_ARTIFACTS_PATH) || env.BUILD_ARTIFACTS_PATH }} path: ${{ env.BUILD_ARTIFACTS_PATH }}
- name: Manual Clean up for self-hosted runners
if: runner.os == 'Windows' && matrix.config.working-directory
shell: cmd
run: |
rmdir /s /q "${{ matrix.config.working-directory }}\${{ env.BUILD_ARTIFACTS_PATH }}"
merge-channel-files: merge-channel-files:
needs: needs:
@ -443,17 +423,16 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download staged-for-merge channel file artifacts - name: Download staged-for-merge channel files artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
merge-multiple: true name: ${{ env.STAGED_CHANNEL_FILES_ARTIFACT }}
path: ${{ env.CHANNEL_FILES_PATH }} path: ${{ env.CHANNEL_FILES_PATH }}
pattern: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}*
- name: Remove no longer needed artifacts - name: Remove no longer needed artifact
uses: geekyeggo/delete-artifact@v5 uses: geekyeggo/delete-artifact@v2
with: with:
name: ${{ env.STAGED_CHANNEL_FILE_ARTIFACT_PREFIX }}* name: ${{ env.STAGED_CHANNEL_FILES_ARTIFACT }}
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -473,12 +452,6 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
@ -489,11 +462,11 @@ jobs:
--channel "${{ needs.build-type-determination.outputs.channel-name }}" \ --channel "${{ needs.build-type-determination.outputs.channel-name }}" \
--input "${{ env.CHANNEL_FILES_PATH }}" --input "${{ env.CHANNEL_FILES_PATH }}"
- name: Upload merged channel files job transfer artifact - name: Upload merged channel files to job transfer artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
if-no-files-found: error if-no-files-found: error
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}channel-files name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.CHANNEL_FILES_PATH }} path: ${{ env.CHANNEL_FILES_PATH }}
artifacts: artifacts:
@ -504,25 +477,22 @@ jobs:
if: always() && needs.build.result != 'skipped' if: always() && needs.build.result != 'skipped'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
BUILD_ARTIFACTS_FOLDER: build-artifacts
strategy: strategy:
matrix: matrix:
artifact: ${{ fromJson(needs.select-targets.outputs.artifact-matrix) }} artifact: ${{ fromJson(needs.select-targets.outputs.artifact-matrix) }}
steps: steps:
- name: Download job transfer artifact that contains ${{ matrix.artifact.name }} tester build - name: Download job transfer artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}${{ matrix.artifact.job-transfer-artifact-suffix }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.BUILD_ARTIFACTS_FOLDER }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}
- name: Upload tester build artifact - name: Upload tester build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.artifact.name }} name: ${{ matrix.artifact.name }}
path: ${{ env.BUILD_ARTIFACTS_FOLDER }}/${{ matrix.artifact.path }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
changelog: changelog:
needs: needs:
@ -565,11 +535,11 @@ jobs:
echo "$BODY" > CHANGELOG.txt echo "$BODY" > CHANGELOG.txt
- name: Upload changelog job transfer artifact - name: Upload Changelog [GitHub Actions]
if: needs.build-type-determination.outputs.is-nightly == 'true' if: needs.build-type-determination.outputs.is-nightly == 'true'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}changelog name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: CHANGELOG.txt path: CHANGELOG.txt
publish: publish:
@ -588,33 +558,22 @@ jobs:
needs.build-type-determination.outputs.publish-to-s3 == 'true' && needs.build-type-determination.outputs.publish-to-s3 == 'true' &&
needs.build-type-determination.outputs.is-nightly == 'true' needs.build-type-determination.outputs.is-nightly == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
ARTIFACTS_FOLDER: build-artifacts
environment: production
permissions:
id-token: write
contents: read
steps: steps:
- name: Download all job transfer artifacts - name: Download [GitHub Actions]
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
merge-multiple: true name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.ARTIFACTS_FOLDER }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}
pattern: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*
- name: Configure AWS Credentials for Nightly [S3]
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Nightly [S3] - name: Publish Nightly [S3]
run: | uses: docker://plugins/s3
aws s3 sync ${{ env.ARTIFACTS_FOLDER }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide/nightly env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide/nightly'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
release: release:
needs: needs:
@ -631,23 +590,12 @@ jobs:
needs.changelog.result == 'success' && needs.changelog.result == 'success' &&
needs.build-type-determination.outputs.is-release == 'true' needs.build-type-determination.outputs.is-release == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
ARTIFACTS_FOLDER: build-artifacts
environment: production
permissions:
id-token: write
contents: write
steps: steps:
- name: Download all job transfer artifacts - name: Download [GitHub Actions]
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
merge-multiple: true name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.ARTIFACTS_FOLDER }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}
pattern: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}*
- name: Get Tag - name: Get Tag
id: tag_name id: tag_name
@ -655,26 +603,25 @@ jobs:
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub] - name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.9.0 uses: svenstaro/upload-release-action@2.7.0
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }} release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
file: ${{ env.ARTIFACTS_FOLDER }}/* file: ${{ env.JOB_TRANSFER_ARTIFACT }}/*
tag: ${{ github.ref }} tag: ${{ github.ref }}
file_glob: true file_glob: true
body: ${{ needs.changelog.outputs.BODY }} body: ${{ needs.changelog.outputs.BODY }}
- name: Configure AWS Credentials for Release [S3]
if: needs.build-type-determination.outputs.publish-to-s3 == 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Release [S3] - name: Publish Release [S3]
if: needs.build-type-determination.outputs.publish-to-s3 == 'true' if: needs.build-type-determination.outputs.publish-to-s3 == 'true'
run: | uses: docker://plugins/s3
aws s3 sync ${{ env.ARTIFACTS_FOLDER }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide env:
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
PLUGIN_TARGET: '/arduino-ide'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
clean: clean:
# This job must run after all jobs that use the transfer artifact. # This job must run after all jobs that use the transfer artifact.
@ -688,7 +635,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Remove unneeded job transfer artifacts - name: Remove unneeded job transfer artifact
uses: geekyeggo/delete-artifact@v5 uses: geekyeggo/delete-artifact@v2
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT_PREFIX }}* name: ${{ env.JOB_TRANSFER_ARTIFACT }}

View File

@ -74,11 +74,9 @@ jobs:
- identifier: macOS signing certificate # Text used to identify certificate in notifications. - identifier: macOS signing certificate # Text used to identify certificate in notifications.
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate. certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate.
password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password. password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password.
type: pkcs12
- identifier: Windows signing certificate - identifier: Windows signing certificate
certificate-secret: INSTALLER_CERT_WINDOWS_CER certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
# The password for the Windows certificate is not needed, because its not a container, but a single certificate. password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
type: x509
steps: steps:
- name: Set certificate path environment variable - name: Set certificate path environment variable
@ -97,7 +95,7 @@ jobs:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }} CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
run: | run: |
( (
openssl ${{ matrix.certificate.type }} \ openssl pkcs12 \
-in "${{ env.CERTIFICATE_PATH }}" \ -in "${{ env.CERTIFICATE_PATH }}" \
-legacy \ -legacy \
-noout \ -noout \
@ -124,43 +122,26 @@ jobs:
CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }} CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
id: get-days-before-expiration id: get-days-before-expiration
run: | run: |
if [[ ${{ matrix.certificate.type }} == "pkcs12" ]]; then EXPIRATION_DATE="$(
EXPIRATION_DATE="$( (
( openssl pkcs12 \
openssl pkcs12 \ -in "${{ env.CERTIFICATE_PATH }}" \
-in "${{ env.CERTIFICATE_PATH }}" \ -clcerts \
-clcerts \ -legacy \
-legacy \ -nodes \
-nodes \ -passin env:CERTIFICATE_PASSWORD
-passin env:CERTIFICATE_PASSWORD ) | (
) | ( openssl x509 \
openssl x509 \ -noout \
-noout \ -enddate
-enddate ) | (
) | ( grep \
grep \ --max-count=1 \
--max-count=1 \ --only-matching \
--only-matching \ --perl-regexp \
--perl-regexp \ 'notAfter=(\K.*)'
'notAfter=(\K.*)' )
) )"
)"
elif [[ ${{ matrix.certificate.type }} == "x509" ]]; then
EXPIRATION_DATE="$(
(
openssl x509 \
-in ${{ env.CERTIFICATE_PATH }} \
-noout \
-enddate
) | (
grep \
--max-count=1 \
--only-matching \
--perl-regexp \
'notAfter=(\K.*)'
)
)"
fi
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))" DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"

View File

@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Build and push to local registry - name: Build and push to local registry
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ matrix.image.path }} file: ${{ matrix.image.path }}

View File

@ -2,7 +2,7 @@ name: Check Internationalization
env: env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml # See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21' GO_VERSION: '1.19'
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows # See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on: on:
@ -76,12 +76,6 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
env: env:

View File

@ -1,94 +0,0 @@
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-javascript-task.md
name: Check JavaScript
env:
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 18.17
# See: https://docs.github.com/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows
on:
create:
push:
paths:
- '.github/workflows/check-javascript.ya?ml'
- '**/.eslintignore'
- '**/.eslintrc*'
- '**/.npmrc'
- '**/package.json'
- '**/package-lock.json'
- '**/yarn.lock'
- '**.jsx?'
pull_request:
paths:
- '.github/workflows/check-javascript.ya?ml'
- '**/.eslintignore'
- '**/.eslintrc*'
- '**/.npmrc'
- '**/package.json'
- '**/package-lock.json'
- '**/yarn.lock'
- '**.jsx?'
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
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"
# 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
check:
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn install
- name: Lint
run: |
yarn \
--cwd arduino-ide-extension \
lint

View File

@ -1,97 +0,0 @@
name: Check Yarn
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
create:
push:
paths:
- ".github/workflows/check-yarn.ya?ml"
- "**/.yarnrc"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
pull_request:
paths:
- ".github/workflows/check-yarn.ya?ml"
- "**/.yarnrc"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
schedule:
# Run every Tuesday at 8 AM UTC to catch breakage resulting from changes to the JSON schema.
- cron: "0 8 * * TUE"
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
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"
# 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
check-sync:
name: check-sync (${{ matrix.project.path }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
project:
- path: .
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn \
install \
--ignore-scripts
- name: Check yarn.lock
run: |
git \
diff \
--color \
--exit-code \
"${{ matrix.project.path }}/yarn.lock"

View File

@ -14,11 +14,6 @@ jobs:
create-changelog: create-changelog:
if: github.repository == 'arduino/arduino-ide' if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
environment: production
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -29,12 +24,6 @@ jobs:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Get Tag - name: Get Tag
id: tag_name id: tag_name
run: | run: |
@ -55,12 +44,12 @@ jobs:
# Compose changelog # Compose changelog
yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME" yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME"
- name: Configure AWS Credentials for Changelog [S3]
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Publish Changelog [S3] - name: Publish Changelog [S3]
run: | uses: docker://plugins/s3
aws s3 sync ${{ env.CHANGELOG_ARTIFACTS }} s3://${{ secrets.DOWNLOADS_BUCKET }}/arduino-ide/changelog env:
PLUGIN_SOURCE: '${{ env.CHANGELOG_ARTIFACTS }}/*'
PLUGIN_STRIP_PREFIX: '${{ env.CHANGELOG_ARTIFACTS }}/'
PLUGIN_TARGET: '/arduino-ide/changelog'
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@ -2,7 +2,7 @@ name: i18n-nightly-push
env: env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml # See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21' GO_VERSION: '1.19'
on: on:
schedule: schedule:
@ -34,12 +34,6 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable

View File

@ -2,7 +2,7 @@ name: i18n-weekly-pull
env: env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml # See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21' GO_VERSION: '1.19'
on: on:
schedule: schedule:
@ -34,12 +34,6 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
@ -52,7 +46,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }} TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: Updated translation files commit-message: Updated translation files
title: Update translation files title: Update translation files

View File

@ -59,7 +59,7 @@ jobs:
images: ${{ matrix.image.registry }}/${{ matrix.image.name }} images: ${{ matrix.image.registry }}/${{ matrix.image.name }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ matrix.image.path }} file: ${{ matrix.image.path }}

View File

@ -19,7 +19,7 @@ on:
env: env:
CONFIGURATIONS_FOLDER: .github/label-configuration-files CONFIGURATIONS_FOLDER: .github/label-configuration-files
CONFIGURATIONS_ARTIFACT_PREFIX: label-configuration-file- CONFIGURATIONS_ARTIFACT: label-configuration-files
jobs: jobs:
check: check:
@ -71,13 +71,13 @@ jobs:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }} file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
- name: Pass configuration files to next job via workflow artifact - name: Pass configuration files to next job via workflow artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
path: | path: |
*.yaml *.yaml
*.yml *.yml
if-no-files-found: error if-no-files-found: error
name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}${{ matrix.filename }} name: ${{ env.CONFIGURATIONS_ARTIFACT }}
sync: sync:
needs: download needs: download
@ -108,17 +108,16 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download configuration file artifacts - name: Download configuration files artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
merge-multiple: true name: ${{ env.CONFIGURATIONS_ARTIFACT }}
pattern: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}*
path: ${{ env.CONFIGURATIONS_FOLDER }} path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifacts - name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v5 uses: geekyeggo/delete-artifact@v2
with: with:
name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}* name: ${{ env.CONFIGURATIONS_ARTIFACT }}
- name: Merge label configuration files - name: Merge label configuration files
run: | run: |

View File

@ -1,140 +0,0 @@
name: Test JavaScript
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21'
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 18.17
on:
push:
paths:
- ".github/workflows/test-javascript.ya?ml"
- "**/.mocharc.js"
- "**/.mocharc.jsonc?"
- "**/.mocharc.ya?ml"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "tests/testdata/**"
- "**/tsconfig.json"
- "**.[jt]sx?"
pull_request:
paths:
- ".github/workflows/test-javascript.ya?ml"
- "**/.mocharc.js"
- "**/.mocharc.jsonc?"
- "**/.mocharc.ya?ml"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "tests/testdata/**"
- "**/tsconfig.json"
- "**.[jt]sx?"
workflow_dispatch:
repository_dispatch:
jobs:
run-determination:
runs-on: ubuntu-latest
permissions: {}
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"
# 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
test:
name: test (${{ matrix.project.path }}, ${{ matrix.operating-system }})
needs: run-determination
if: needs.run-determination.outputs.result == 'true'
runs-on: ${{ matrix.operating-system }}
defaults:
run:
shell: bash
permissions:
contents: read
strategy:
fail-fast: false
matrix:
project:
- path: .
operating-system:
- macos-latest
- ubuntu-latest
- windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: ${{ env.NODE_VERSION }}
# See: https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.11.x'
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install Dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install npm package dependencies
env:
# Avoid failure of @vscode/ripgrep installation due to GitHub API rate limiting:
# https://github.com/microsoft/vscode-ripgrep#github-api-limit-note
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
yarn install
- name: Compile TypeScript
run: |
yarn \
--cwd arduino-ide-extension \
build
- name: Run tests
env:
# These secrets are optional. Dependent tests will be skipped if not available.
CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }}
CREATE_PASSWORD: ${{ secrets.CREATE_PASSWORD }}
CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }}
run: |
yarn test
yarn \
--cwd arduino-ide-extension \
test:slow

View File

@ -8,7 +8,7 @@ on:
env: env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml # See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: '1.21' GO_VERSION: '1.19'
NODE_VERSION: '18.17' NODE_VERSION: '18.17'
jobs: jobs:
@ -36,12 +36,6 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x version: 3.x
- name: Install dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
@ -61,7 +55,7 @@ jobs:
run: yarn run themes:generate run: yarn run themes:generate
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: Updated themes commit-message: Updated themes
title: Update themes title: Update themes

View File

@ -2,9 +2,7 @@
# Arduino IDE 2.x # Arduino IDE 2.x
[![Build status](https://github.com/arduino/arduino-ide/actions/workflows/build.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/build.yml) [![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
[![Check JavaScript status](https://github.com/arduino/arduino-ide/actions/workflows/check-javascript.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/check-javascript.yml)
[![Test JavaScript status](https://github.com/arduino/arduino-ide/actions/workflows/test-javascript.yml/badge.svg)](https://github.com/arduino/arduino-ide/actions/workflows/test-javascript.yml)
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the [repository of the 1.x version](https://github.com/arduino/Arduino). This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the [repository of the 1.x version](https://github.com/arduino/Arduino).

View File

@ -1,6 +1,6 @@
{ {
"name": "arduino-ide-extension", "name": "arduino-ide-extension",
"version": "2.3.7", "version": "2.3.1",
"description": "An extension for Theia building the Arduino IDE", "description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
@ -13,7 +13,7 @@
"download-ls": "node ./scripts/download-ls.js", "download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js", "download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js", "generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "eslint .", "lint": "eslint",
"prebuild": "rimraf lib", "prebuild": "rimraf lib",
"build": "tsc", "build": "tsc",
"build:dev": "yarn build", "build:dev": "yarn build",
@ -24,29 +24,27 @@
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.8.14", "@grpc/grpc-js": "^1.8.14",
"@theia/application-package": "1.57.0", "@theia/application-package": "1.41.0",
"@theia/core": "1.57.0", "@theia/core": "1.41.0",
"@theia/debug": "1.57.0", "@theia/debug": "1.41.0",
"@theia/editor": "1.57.0", "@theia/editor": "1.41.0",
"@theia/electron": "1.57.0", "@theia/electron": "1.41.0",
"@theia/filesystem": "1.57.0", "@theia/filesystem": "1.41.0",
"@theia/keymaps": "1.57.0", "@theia/keymaps": "1.41.0",
"@theia/markers": "1.57.0", "@theia/markers": "1.41.0",
"@theia/messages": "1.57.0", "@theia/messages": "1.41.0",
"@theia/monaco": "1.57.0", "@theia/monaco": "1.41.0",
"@theia/monaco-editor-core": "1.83.101", "@theia/monaco-editor-core": "1.72.3",
"@theia/navigator": "1.57.0", "@theia/navigator": "1.41.0",
"@theia/outline-view": "1.57.0", "@theia/outline-view": "1.41.0",
"@theia/output": "1.57.0", "@theia/output": "1.41.0",
"@theia/plugin-ext": "1.57.0", "@theia/plugin-ext": "1.41.0",
"@theia/plugin-ext-vscode": "1.57.0", "@theia/preferences": "1.41.0",
"@theia/preferences": "1.57.0", "@theia/scm": "1.41.0",
"@theia/scm": "1.57.0", "@theia/search-in-workspace": "1.41.0",
"@theia/search-in-workspace": "1.57.0", "@theia/terminal": "1.41.0",
"@theia/terminal": "1.57.0", "@theia/typehierarchy": "1.41.0",
"@theia/test": "1.57.0", "@theia/workspace": "1.41.0",
"@theia/typehierarchy": "1.57.0",
"@theia/workspace": "1.57.0",
"@tippyjs/react": "^4.2.5", "@tippyjs/react": "^4.2.5",
"@types/auth0-js": "^9.21.3", "@types/auth0-js": "^9.21.3",
"@types/btoa": "^1.2.3", "@types/btoa": "^1.2.3",
@ -58,6 +56,7 @@
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1", "@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0", "@types/ps-tree": "^1.1.0",
"@types/react-tabs": "^2.3.2",
"@types/temp": "^0.8.34", "@types/temp": "^0.8.34",
"arduino-serial-plotter-webapp": "0.2.0", "arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0", "async-mutex": "^0.3.0",
@ -67,15 +66,13 @@
"cross-fetch": "^3.1.5", "cross-fetch": "^3.1.5",
"dateformat": "^3.0.3", "dateformat": "^3.0.3",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"dompurify": "^2.4.7",
"drivelist": "^9.2.4", "drivelist": "^9.2.4",
"electron-updater": "^4.6.5", "electron-updater": "^4.6.5",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-json-stable-stringify": "^2.1.0", "fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1", "fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0", "filename-reserved-regex": "^2.0.0",
"fqbn": "^1.0.5", "glob": "^7.1.6",
"glob": "10.4.4",
"google-protobuf": "^3.20.1", "google-protobuf": "^3.20.1",
"hash.js": "^1.1.7", "hash.js": "^1.1.7",
"is-online": "^10.0.0", "is-online": "^10.0.0",
@ -99,7 +96,7 @@
"react-markdown": "^8.0.0", "react-markdown": "^8.0.0",
"react-perfect-scrollbar": "^1.5.8", "react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0", "react-select": "^5.6.0",
"react-tabs": "^6.1.0", "react-tabs": "^3.1.2",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"semver": "^7.3.2", "semver": "^7.3.2",
"string-natural-compare": "^2.0.3", "string-natural-compare": "^2.0.3",
@ -126,11 +123,11 @@
"mockdate": "^3.0.5", "mockdate": "^3.0.5",
"moment": "^2.24.0", "moment": "^2.24.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"rimraf": "^5.0.0" "rimraf": "^2.6.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"@pingghost/protoc": "^1.0.2", "grpc-tools": "^1.12.4",
"grpc-tools": "^1.12.4" "protoc": "^1.0.4"
}, },
"mocha": { "mocha": {
"require": [ "require": [
@ -172,17 +169,13 @@
], ],
"arduino": { "arduino": {
"arduino-cli": { "arduino-cli": {
"version": "1.2.0" "version": "0.35.2"
}, },
"arduino-fwuploader": { "arduino-fwuploader": {
"version": "2.4.1" "version": "2.4.1"
}, },
"arduino-language-server": { "arduino-language-server": {
"version": { "version": "0.7.6"
"owner": "arduino",
"repo": "arduino-language-server",
"commitish": "05ec308"
}
}, },
"clangd": { "clangd": {
"version": "14.0.0" "version": "14.0.0"

View File

@ -34,7 +34,7 @@
}, ''); }, '');
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length === 0) { if (args.length == 0) {
console.error('Missing argument to destination file'); console.error('Missing argument to destination file');
process.exit(1); process.exit(1);
} }

View File

@ -1,7 +1,7 @@
// @ts-check // @ts-check
// The version to use. // The version to use.
const version = '1.10.2'; const version = '1.10.1';
(async () => { (async () => {
const os = require('node:os'); const os = require('node:os');

View File

@ -3,15 +3,13 @@
(async () => { (async () => {
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const decompress = require('decompress'); const { mkdirSync, promises: fs } = require('node:fs');
const unzip = require('decompress-unzip');
const { mkdirSync, promises: fs, rmSync, existsSync } = require('node:fs');
const { exec } = require('./utils'); const { exec } = require('./utils');
const { glob } = require('glob'); const glob = require('glob');
const { SemVer, gte, valid: validSemVer, eq } = require('semver'); const { SemVer, gte, valid: validSemVer } = require('semver');
// Use a node-protoc fork until apple arm32 is supported const protoc = path.dirname(require('protoc/protoc'));
// https://github.com/YePpHa/node-protoc/pull/10
const protoc = path.dirname(require('@pingghost/protoc/protoc')); const repository = await fs.mkdtemp(path.join(os.tmpdir(), 'arduino-cli-'));
const { owner, repo, commitish } = (() => { const { owner, repo, commitish } = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json')); const pkg = require(path.join(__dirname, '..', 'package.json'));
@ -58,6 +56,11 @@
return { owner, repo, commitish }; return { owner, repo, commitish };
})(); })();
const url = `https://github.com/${owner}/${repo}.git`;
console.log(`>>> Cloning repository from '${url}'...`);
exec('git', ['clone', url, repository], { logStdout: true });
console.log(`<<< Repository cloned.`);
const { platform } = process; const { platform } = process;
const resourcesFolder = path.join( const resourcesFolder = path.join(
__dirname, __dirname,
@ -83,207 +86,108 @@
// - `git-snapshot` for local build executed via `task build`. We do not do this. // - `git-snapshot` for local build executed via `task build`. We do not do this.
// - rest, we assume it is a valid semver and has the corresponding tagged code, we use the tag to generate the APIs from the `proto` files. // - rest, we assume it is a valid semver and has the corresponding tagged code, we use the tag to generate the APIs from the `proto` files.
/* /*
{ {
"Application": "arduino-cli", "Application": "arduino-cli",
"VersionString": "nightly-20210126", "VersionString": "nightly-20210126",
"Commit": "079bb6c6", "Commit": "079bb6c6",
"Status": "alpha", "Status": "alpha",
"Date": "2021-01-26T01:46:31Z" "Date": "2021-01-26T01:46:31Z"
}
*/
const versionObject = JSON.parse(versionJson);
async function globProtos(folder, pattern = '**/*.proto') {
let protos = [];
try {
const matches = await glob(pattern, { cwd: folder });
protos = matches.map((filename) => path.join(folder, filename));
} catch (error) {
console.log(error.stack ?? error.message);
}
return protos;
}
async function getProtosFromRepo(
commitish = '',
version = '',
owner = 'arduino',
repo = 'arduino-cli'
) {
const repoFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'arduino-cli-'));
const url = `https://github.com/${owner}/${repo}.git`;
console.log(`>>> Cloning repository from '${url}'...`);
exec('git', ['clone', url, repoFolder], { logStdout: true });
console.log(`<<< Repository cloned.`);
if (validSemVer(version)) {
let versionTag = version;
// https://github.com/arduino/arduino-cli/pull/2374
if (
gte(new SemVer(version, { loose: true }), new SemVer('0.35.0-rc.1'))
) {
versionTag = `v${version}`;
} }
console.log(`>>> Checking out tagged version: '${versionTag}'...`); */
exec('git', ['-C', repoFolder, 'fetch', '--all', '--tags'], { const versionObject = JSON.parse(versionJson);
logStdout: true, let version = versionObject.VersionString;
}); if (validSemVer(version)) {
exec( // https://github.com/arduino/arduino-cli/pull/2374
'git', if (gte(new SemVer(version, { loose: true }), new SemVer('0.35.0-rc.1'))) {
['-C', repoFolder, 'checkout', `tags/${versionTag}`, '-b', versionTag], version = `v${version}`;
{ logStdout: true }
);
console.log(`<<< Checked out tagged version: '${versionTag}'.`);
} else if (commitish) {
console.log(`>>> Checking out commitish: '${commitish}'...`);
exec('git', ['-C', repoFolder, 'checkout', commitish], {
logStdout: true,
});
console.log(`<<< Checked out commitish: '${commitish}'.`);
} else {
console.log(
`WARN: no 'git checkout'. Generating from the HEAD revision.`
);
} }
console.log(`>>> Checking out tagged version: '${version}'...`);
const rpcFolder = await fs.mkdtemp( exec('git', ['-C', repository, 'fetch', '--all', '--tags'], {
path.join(os.tmpdir(), 'arduino-cli-rpc') logStdout: true,
);
// Copy the the repository rpc folder so we can remove the repository
await fs.cp(path.join(repoFolder, 'rpc'), path.join(rpcFolder), {
recursive: true,
}); });
rmSync(repoFolder, { recursive: true, maxRetries: 5, force: true }); exec(
'git',
// Patch for https://github.com/arduino/arduino-cli/issues/2755 ['-C', repository, 'checkout', `tags/${version}`, '-b', version],
// Google proto files are removed from source since v1.1.0 { logStdout: true }
if (!existsSync(path.join(rpcFolder, 'google'))) {
// Include packaged google proto files from v1.1.1
// See https://github.com/arduino/arduino-cli/pull/2761
console.log(`>>> Missing google proto files. Including from v1.1.1...`);
const v111ProtoFolder = await getProtosFromZip('1.1.1');
// Create an return a folder name google in rpcFolder
const googleFolder = path.join(rpcFolder, 'google');
await fs.cp(path.join(v111ProtoFolder, 'google'), googleFolder, {
recursive: true,
});
console.log(`<<< Included google proto files from v1.1.1.`);
}
return rpcFolder;
}
async function getProtosFromZip(version) {
if (!version) {
console.log(`Could not download proto files: CLI version not provided.`);
process.exit(1);
}
console.log(`>>> Downloading proto files from zip for ${version}.`);
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_proto.zip`;
const protos = await fs.mkdtemp(
path.join(os.tmpdir(), 'arduino-cli-proto')
); );
console.log(`<<< Checked out tagged version: '${version}'.`);
const { default: download } = await import('@xhmikosr/downloader'); } else if (commitish) {
/** @type {import('node:buffer').Buffer} */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data = await download(url);
await decompress(data, protos, {
plugins: [unzip()],
filter: (file) => file.path.endsWith('.proto'),
});
console.log( console.log(
`<<< Finished downloading and extracting proto files for ${version}.` `>>> Checking out commitish from 'package.json': '${commitish}'...`
);
exec('git', ['-C', repository, 'checkout', commitish], { logStdout: true });
console.log(
`<<< Checked out commitish from 'package.json': '${commitish}'.`
); );
return protos;
}
let protosFolder;
if (commitish) {
protosFolder = await getProtosFromRepo(commitish, undefined, owner, repo);
} else if (
versionObject.VersionString &&
validSemVer(versionObject.VersionString)
) {
const version = versionObject.VersionString;
// v1.1.0 does not contains google proto files in zip
// See https://github.com/arduino/arduino-cli/issues/2755
const isV110 = eq(new SemVer(version, { loose: true }), '1.1.0');
protosFolder = isV110
? await getProtosFromRepo(undefined, version)
: await getProtosFromZip(version);
} else if (versionObject.Commit) { } else if (versionObject.Commit) {
protosFolder = await getProtosFromRepo(versionObject.Commit); console.log(
} `>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`
);
if (!protosFolder) { exec('git', ['-C', repository, 'checkout', versionObject.Commit], {
console.log(`Could not get proto files: missing commitish or version.`); logStdout: true,
process.exit(1); });
} console.log(
`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`
const protos = await globProtos(protosFolder); );
} else {
if (!protos || protos.length === 0) { console.log(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true });
console.log(`Could not find any .proto files under ${protosFolder}.`);
process.exit(1);
} }
console.log('>>> Generating TS/JS API from:'); console.log('>>> Generating TS/JS API from:');
exec('git', ['-C', repository, 'rev-parse', '--abbrev-ref', 'HEAD'], {
logStdout: true,
});
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol'); const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
// Must wipe the gen output folder. Otherwise, dangling service implementation remain in IDE2 code,
// although it has been removed from the proto file.
// For example, https://github.com/arduino/arduino-cli/commit/50a8bf5c3e61d5b661ccfcd6a055e82eeb510859.
// rmSync(out, { recursive: true, maxRetries: 5, force: true });
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
try { const protos = await new Promise((resolve) =>
// Generate JS code from the `.proto` files. glob('**/*.proto', { cwd: rpc }, (error, matches) => {
exec( if (error) {
'grpc_tools_node_protoc', console.log(error.stack ?? error.message);
[ resolve([]);
`--js_out=import_style=commonjs,binary:${out}`, return;
`--grpc_out=generate_package_definition:${out}`, }
'-I', resolve(matches.map((filename) => path.join(rpc, filename)));
protosFolder, })
...protos, );
], if (!protos || protos.length === 0) {
{ logStdout: true } console.log(`Could not find any .proto files under ${rpc}.`);
); process.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',
protosFolder,
...protos,
],
{ logStdout: true }
);
} catch (error) {
console.log(error);
} finally {
rmSync(protosFolder, { recursive: true, maxRetries: 5, force: true });
} }
// 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,
],
{ logStdout: true }
);
// 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,
],
{ logStdout: true }
);
console.log('<<< Generation was successful.'); console.log('<<< Generation was successful.');
})(); })();

View File

@ -1,7 +1,7 @@
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { import {
TabBarToolbarContribution, TabBarToolbarContribution,

View File

@ -5,8 +5,10 @@ import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'; import {
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; FrontendApplicationContribution,
FrontendApplication as TheiaFrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import { LibraryListWidget } from './library/library-list-widget'; import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution'; import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { import {
@ -89,6 +91,7 @@ import {
ArduinoDaemonPath, ArduinoDaemonPath,
ArduinoDaemon, ArduinoDaemon,
} from '../common/protocol/arduino-daemon'; } from '../common/protocol/arduino-daemon';
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
import { import {
FrontendConnectionStatusService, FrontendConnectionStatusService,
ApplicationConnectionStatusContribution, ApplicationConnectionStatusContribution,
@ -122,10 +125,7 @@ import { OpenSketch } from './contributions/open-sketch';
import { Close } from './contributions/close'; import { Close } from './contributions/close';
import { SaveAsSketch } from './contributions/save-as-sketch'; import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch'; import { SaveSketch } from './contributions/save-sketch';
import { import { VerifySketch } from './contributions/verify-sketch';
CompileSummaryProvider,
VerifySketch,
} from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch'; import { UploadSketch } from './contributions/upload-sketch';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution'; import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions'; import { EditContributions } from './contributions/edit-contributions';
@ -177,6 +177,7 @@ import {
import { About } from './contributions/about'; import { About } from './contributions/about';
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service'; import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
import { TabBarRenderer } from './theia/core/tab-bars'; import { TabBarRenderer } from './theia/core/tab-bars';
import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator'; import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator'; import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug, DebugDisabledStatusMessageSource } from './contributions/debug'; import { Debug, DebugDisabledStatusMessageSource } from './contributions/debug';
@ -265,7 +266,7 @@ import {
IDEUpdaterDialog, IDEUpdaterDialog,
IDEUpdaterDialogProps, IDEUpdaterDialogProps,
} from './dialogs/ide-updater/ide-updater-dialog'; } from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source'; import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model'; import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl'; import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
@ -285,6 +286,10 @@ import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-gen
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator'; import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog'; import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog'; import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution'; import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution'; import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
import { CoreErrorHandler } from './contributions/core-error-handler'; import { CoreErrorHandler } from './contributions/core-error-handler';
@ -368,13 +373,6 @@ import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widg
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget'; import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget'; import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import {
VersionWelcomeDialog,
VersionWelcomeDialogProps,
} from './dialogs/version-welcome-dialog';
import { TestViewContribution as TheiaTestViewContribution } from '@theia/test/lib/browser/view/test-view-contribution';
import { TestViewContribution } from './theia/test/test-view-contribution';
// Hack to fix copy/cut/paste issue after electron version update in Theia. // Hack to fix copy/cut/paste issue after electron version update in Theia.
// https://github.com/eclipse-theia/theia/issues/12487 // https://github.com/eclipse-theia/theia/issues/12487
import('@theia/core/lib/browser/common-frontend-contribution.js').then( import('@theia/core/lib/browser/common-frontend-contribution.js').then(
@ -554,6 +552,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
WorkspaceVariableContribution WorkspaceVariableContribution
); );
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations. // Layout and shell customizations.
rebind(TheiaOutlineViewContribution) rebind(TheiaOutlineViewContribution)
.to(OutlineViewContribution) .to(OutlineViewContribution)
@ -762,8 +769,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, BoardsDataMenuUpdater); Contribution.configure(bind, BoardsDataMenuUpdater);
Contribution.configure(bind, AutoSelectProgrammer); Contribution.configure(bind, AutoSelectProgrammer);
bind(CompileSummaryProvider).toService(VerifySketch);
bindContributionProvider(bind, StartupTaskProvider); bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@ -827,6 +832,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
); );
}); });
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
// Silent the badge decoration in the Explorer view. // Silent the badge decoration in the Explorer view.
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope(); bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator); rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
@ -987,11 +999,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'IDEUpdater', title: 'IDEUpdater',
}); });
bind(VersionWelcomeDialog).toSelf().inSingletonScope();
bind(VersionWelcomeDialogProps).toConstantValue({
title: 'VersionWelcomeDialog',
});
bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({ bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields', title: 'UserFields',
@ -1075,8 +1082,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaTerminalFrontendContribution).toService( rebind(TheiaTerminalFrontendContribution).toService(
TerminalFrontendContribution TerminalFrontendContribution
); );
// Hides the Test Explorer from the side-bar
bind(TestViewContribution).toSelf().inSingletonScope();
rebind(TheiaTestViewContribution).toService(TestViewContribution);
}); });

View File

@ -137,18 +137,6 @@ const properties: ArduinoPreferenceSchemaProperties = {
'arduino.upload.verify': { 'arduino.upload.verify': {
type: 'boolean', type: 'boolean',
default: false, default: false,
description: nls.localize(
'arduino/preferences/upload.verify',
'After upload, verify that the contents of the memory on the board match the uploaded binary.'
),
},
'arduino.upload.autoVerify': {
type: 'boolean',
default: true,
description: nls.localize(
'arduino/preferences/upload.autoVerify',
"True if the IDE should automatically verify the code before the upload. True by default. When this value is false, IDE does not recompile the code before uploading the binary to the board. It's highly advised to only set this value to false if you know what you are doing."
),
}, },
'arduino.window.autoScale': { 'arduino.window.autoScale': {
type: 'boolean', type: 'boolean',
@ -240,14 +228,6 @@ const properties: ArduinoPreferenceSchemaProperties = {
), ),
default: 'https://api2.arduino.cc/create', default: 'https://api2.arduino.cc/create',
}, },
'arduino.cloud.sharedSpaceID': {
type: 'string',
description: nls.localize(
'arduino/preferences/cloud.sharedSpaceId',
'The ID of the Arduino Cloud shared space to load the sketchbook from. If empty, your private space is selected.'
),
default: '',
},
'arduino.auth.clientID': { 'arduino.auth.clientID': {
type: 'string', type: 'string',
description: nls.localize( description: nls.localize(
@ -280,6 +260,14 @@ const properties: ArduinoPreferenceSchemaProperties = {
), ),
default: 'https://auth.arduino.cc/login#/register', default: 'https://auth.arduino.cc/login#/register',
}, },
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': { 'arduino.cli.daemon.debug': {
type: 'boolean', type: 'boolean',
description: nls.localize( description: nls.localize(
@ -331,7 +319,6 @@ export interface ArduinoConfiguration {
'arduino.compile.warnings': CompilerWarnings; 'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean; 'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean; 'arduino.upload.verify': boolean;
'arduino.upload.autoVerify': boolean;
'arduino.window.autoScale': boolean; 'arduino.window.autoScale': boolean;
'arduino.ide.updateChannel': UpdateChannel; 'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string; 'arduino.ide.updateBaseUrl': string;
@ -342,11 +329,11 @@ export interface ArduinoConfiguration {
'arduino.cloud.push.warn': boolean; 'arduino.cloud.push.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean; 'arduino.cloud.pushpublic.warn': boolean;
'arduino.cloud.sketchSyncEndpoint': string; 'arduino.cloud.sketchSyncEndpoint': string;
'arduino.cloud.sharedSpaceID': string;
'arduino.auth.clientID': string; 'arduino.auth.clientID': string;
'arduino.auth.domain': string; 'arduino.auth.domain': string;
'arduino.auth.audience': string; 'arduino.auth.audience': string;
'arduino.auth.registerUri': string; 'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean; 'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string; 'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean; 'arduino.checkForUpdates': boolean;

View File

@ -3,7 +3,7 @@ import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
CommandRegistry, CommandRegistry,
CommandContribution, CommandContribution,

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { MessageType } from '@theia/core/lib/common/message-service-protocol';

View File

@ -98,7 +98,6 @@ export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
} }
override async open( override async open(
disposeOnResolve = true,
params?: EditBoardsConfigActionParams params?: EditBoardsConfigActionParams
): Promise<BoardsConfig | undefined> { ): Promise<BoardsConfig | undefined> {
this._searchSet = undefined; this._searchSet = undefined;
@ -120,7 +119,7 @@ export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
this._searchSet = params.searchSet.slice(); this._searchSet = params.searchSet.slice();
} }
} }
return super.open(disposeOnResolve); return super.open();
} }
protected override onAfterAttach(msg: Message): void { protected override onAfterAttach(msg: Message): void {

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { StorageService } from '@theia/core/lib/browser/storage-service'; import { StorageService } from '@theia/core/lib/browser/storage-service';
import type { import type {
@ -12,7 +12,6 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects'; import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
import type { Mutable } from '@theia/core/lib/common/types'; import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, named } from '@theia/core/shared/inversify'; import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FQBN } from 'fqbn';
import { import {
BoardDetails, BoardDetails,
BoardsService, BoardsService,
@ -21,7 +20,6 @@ import {
Programmer, Programmer,
isBoardIdentifierChangeEvent, isBoardIdentifierChangeEvent,
isProgrammer, isProgrammer,
sanitizeFqbn,
} from '../../common/protocol'; } from '../../common/protocol';
import { notEmpty } from '../../common/utils'; import { notEmpty } from '../../common/utils';
import type { import type {
@ -31,14 +29,6 @@ import type {
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
export interface SelectConfigOptionParams {
readonly fqbn: string;
readonly optionsToUpdate: readonly Readonly<{
option: string;
selectedValue: string;
}>[];
}
@injectable() @injectable()
export class BoardsDataStore export class BoardsDataStore
implements implements
@ -74,12 +64,7 @@ export class BoardsDataStore
this.toDispose.pushAll([ this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => { this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) { if (isBoardIdentifierChangeEvent(event)) {
this.updateSelectedBoardData( this.updateSelectedBoardData(event.selectedBoard?.fqbn);
event.selectedBoard?.fqbn,
// If the change event comes from toolbar and the FQBN contains custom board options, change the currently selected options
// https://github.com/arduino/arduino-ide/issues/1588
event.reason === 'toolbar'
);
} }
}), }),
this.notificationCenter.onPlatformDidInstall(async ({ item }) => { this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
@ -131,7 +116,7 @@ export class BoardsDataStore
if (!fqbn) { if (!fqbn) {
return undefined; return undefined;
} else { } else {
const data = await this.getData(sanitizeFqbn(fqbn)); const data = await this.getData(fqbn);
if (data === BoardsDataStore.Data.EMPTY) { if (data === BoardsDataStore.Data.EMPTY) {
return undefined; return undefined;
} }
@ -140,22 +125,9 @@ export class BoardsDataStore
} }
private async updateSelectedBoardData( private async updateSelectedBoardData(
fqbn: string | undefined, fqbn: string | undefined
updateConfigOptions = false
): Promise<void> { ): Promise<void> {
this._selectedBoardData = await this.getSelectedBoardData(fqbn); this._selectedBoardData = await this.getSelectedBoardData(fqbn);
if (fqbn && updateConfigOptions) {
const { options } = new FQBN(fqbn);
if (options) {
const optionsToUpdate = Object.entries(options).map(([key, value]) => ({
option: key,
selectedValue: value,
}));
const params = { fqbn, optionsToUpdate };
await this.selectConfigOption(params);
this._selectedBoardData = await this.getSelectedBoardData(fqbn); // reload the updated data
}
}
} }
onStop(): void { onStop(): void {
@ -196,7 +168,7 @@ export class BoardsDataStore
return undefined; return undefined;
} }
const { configOptions } = await this.getData(fqbn); const { configOptions } = await this.getData(fqbn);
return new FQBN(fqbn).withConfigOptions(...configOptions).toString(); return ConfigOption.decorate(fqbn, configOptions);
} }
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> { async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
@ -222,20 +194,6 @@ export class BoardsDataStore
return data; return data;
} }
async reloadBoardData(fqbn: string | undefined): Promise<void> {
if (!fqbn) {
return;
}
const key = this.getStorageKey(fqbn);
const details = await this.loadBoardDetails(fqbn, true);
if (!details) {
return;
}
const data = createDataStoreEntry(details);
await this.storageService.setData(key, data);
this.fireChanged({ fqbn, data });
}
async selectProgrammer({ async selectProgrammer({
fqbn, fqbn,
selectedProgrammer, selectedProgrammer,
@ -243,63 +201,48 @@ export class BoardsDataStore
fqbn: string; fqbn: string;
selectedProgrammer: Programmer; selectedProgrammer: Programmer;
}): Promise<boolean> { }): Promise<boolean> {
const sanitizedFQBN = sanitizeFqbn(fqbn); const storedData = deepClone(await this.getData(fqbn));
const storedData = deepClone(await this.getData(sanitizedFQBN));
const { programmers } = storedData; const { programmers } = storedData;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) { if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false; return false;
} }
const change: BoardsDataStoreChange = { const data = { ...storedData, selectedProgrammer };
fqbn: sanitizedFQBN, await this.setData({ fqbn, data });
data: { ...storedData, selectedProgrammer }, this.fireChanged({ fqbn, data });
};
await this.setData(change);
this.fireChanged(change);
return true; return true;
} }
async selectConfigOption(params: SelectConfigOptionParams): Promise<boolean> { async selectConfigOption({
const { fqbn, optionsToUpdate } = params; fqbn,
if (!optionsToUpdate.length) { option,
selectedValue,
}: {
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);
if (!configOption) {
return false; return false;
} }
let updated = false;
const sanitizedFQBN = sanitizeFqbn(fqbn); for (const value of configOption.values) {
const mutableData = deepClone(await this.getData(sanitizedFQBN)); const mutable: Mutable<ConfigValue> = value;
let didChange = false; if (mutable.value === selectedValue) {
mutable.selected = true;
for (const { option, selectedValue } of optionsToUpdate) { updated = true;
const { configOptions } = mutableData; } else {
const configOption = configOptions.find((c) => c.option === option); mutable.selected = false;
if (configOption) {
const configOptionValueIndex = configOption.values.findIndex(
(configOptionValue) => configOptionValue.value === selectedValue
);
if (configOptionValueIndex >= 0) {
// unselect all
configOption.values
.map((value) => value as Mutable<ConfigValue>)
.forEach((value) => (value.selected = false));
const mutableConfigValue: Mutable<ConfigValue> =
configOption.values[configOptionValueIndex];
// make the new value `selected`
mutableConfigValue.selected = true;
didChange = true;
}
} }
} }
if (!updated) {
if (!didChange) {
return false; return false;
} }
await this.setData({ fqbn, data });
const change: BoardsDataStoreChange = { this.fireChanged({ fqbn, data });
fqbn: sanitizedFQBN,
data: mutableData,
};
await this.setData(change);
this.fireChanged(change);
return true; return true;
} }
@ -313,15 +256,9 @@ export class BoardsDataStore
return `.arduinoIDE-configOptions-${fqbn}`; return `.arduinoIDE-configOptions-${fqbn}`;
} }
async loadBoardDetails( async loadBoardDetails(fqbn: string): Promise<BoardDetails | undefined> {
fqbn: string,
forceRefresh = false
): Promise<BoardDetails | undefined> {
try { try {
const details = await this.boardsService.getBoardDetails({ const details = await this.boardsService.getBoardDetails({ fqbn });
fqbn,
forceRefresh,
});
return details; return details;
} catch (err) { } catch (err) {
if ( if (

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { StorageService } from '@theia/core/lib/browser/storage-service'; import { StorageService } from '@theia/core/lib/browser/storage-service';
import { import {
@ -12,7 +12,6 @@ import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { deepClone } from '@theia/core/lib/common/objects';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import type { Mutable } from '@theia/core/lib/common/types'; import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, optional } from '@theia/core/shared/inversify'; import { inject, injectable, optional } from '@theia/core/shared/inversify';
@ -22,32 +21,31 @@ import {
} from '@theia/output/lib/browser/output-channel'; } from '@theia/output/lib/browser/output-channel';
import { import {
BoardIdentifier, BoardIdentifier,
BoardUserField, boardIdentifierEquals,
BoardWithPackage,
BoardsConfig, BoardsConfig,
BoardsConfigChangeEvent, BoardsConfigChangeEvent,
BoardsPackage, BoardsPackage,
BoardsService, BoardsService,
BoardUserField,
BoardWithPackage,
DetectedPorts, DetectedPorts,
Port,
PortIdentifier,
boardIdentifierEquals,
emptyBoardsConfig, emptyBoardsConfig,
isBoardIdentifier, isBoardIdentifier,
isBoardIdentifierChangeEvent, isBoardIdentifierChangeEvent,
isPortIdentifier, isPortIdentifier,
isPortIdentifierChangeEvent, isPortIdentifierChangeEvent,
Port,
PortIdentifier,
portIdentifierEquals, portIdentifierEquals,
sanitizeFqbn,
serializePlatformIdentifier, serializePlatformIdentifier,
} from '../../common/protocol'; } from '../../common/protocol';
import { import {
BoardList, BoardList,
BoardListHistory, BoardListHistory,
EditBoardsConfigActionParams,
SelectBoardsConfigActionParams,
createBoardList, createBoardList,
EditBoardsConfigActionParams,
isBoardListHistory, isBoardListHistory,
SelectBoardsConfigActionParams,
} from '../../common/protocol/board-list'; } from '../../common/protocol/board-list';
import type { Defined } from '../../common/types'; import type { Defined } from '../../common/types';
import type { import type {
@ -106,21 +104,6 @@ type BoardListHistoryUpdateResult =
type BoardToSelect = BoardIdentifier | undefined | 'ignore-board'; type BoardToSelect = BoardIdentifier | undefined | 'ignore-board';
type PortToSelect = PortIdentifier | undefined | 'ignore-port'; type PortToSelect = PortIdentifier | undefined | 'ignore-port';
function sanitizeBoardToSelectFQBN(board: BoardToSelect): BoardToSelect {
if (isBoardIdentifier(board)) {
return sanitizeBoardIdentifierFQBN(board);
}
return board;
}
function sanitizeBoardIdentifierFQBN(board: BoardIdentifier): BoardIdentifier {
if (board.fqbn) {
const copy: Mutable<BoardIdentifier> = deepClone(board);
copy.fqbn = sanitizeFqbn(board.fqbn);
return copy;
}
return board;
}
interface UpdateBoardListHistoryParams { interface UpdateBoardListHistoryParams {
readonly portToSelect: PortToSelect; readonly portToSelect: PortToSelect;
readonly boardToSelect: BoardToSelect; readonly boardToSelect: BoardToSelect;
@ -153,9 +136,6 @@ export interface BoardListUIActions {
} }
export type BoardListUI = BoardList & BoardListUIActions; export type BoardListUI = BoardList & BoardListUIActions;
export type BoardsConfigChangeEventUI = BoardsConfigChangeEvent &
Readonly<{ reason?: UpdateBoardsConfigReason }>;
@injectable() @injectable()
export class BoardListDumper implements Disposable { export class BoardListDumper implements Disposable {
@inject(OutputChannelManager) @inject(OutputChannelManager)
@ -210,7 +190,7 @@ export class BoardsServiceProvider
private _ready = new Deferred<void>(); private _ready = new Deferred<void>();
private readonly boardsConfigDidChangeEmitter = private readonly boardsConfigDidChangeEmitter =
new Emitter<BoardsConfigChangeEventUI>(); new Emitter<BoardsConfigChangeEvent>();
readonly onBoardsConfigDidChange = this.boardsConfigDidChangeEmitter.event; readonly onBoardsConfigDidChange = this.boardsConfigDidChangeEmitter.event;
private readonly boardListDidChangeEmitter = new Emitter<BoardListUI>(); private readonly boardListDidChangeEmitter = new Emitter<BoardListUI>();
@ -373,8 +353,7 @@ export class BoardsServiceProvider
portToSelect !== 'ignore-port' && portToSelect !== 'ignore-port' &&
!portIdentifierEquals(portToSelect, previousSelectedPort); !portIdentifierEquals(portToSelect, previousSelectedPort);
const boardDidChangeEvent = boardDidChange const boardDidChangeEvent = boardDidChange
? // The change event must always contain any custom board options. Hence the board to select is not sanitized. ? { selectedBoard: boardToSelect, previousSelectedBoard }
{ selectedBoard: boardToSelect, previousSelectedBoard }
: undefined; : undefined;
const portDidChangeEvent = portDidChange const portDidChangeEvent = portDidChange
? { selectedPort: portToSelect, previousSelectedPort } ? { selectedPort: portToSelect, previousSelectedPort }
@ -395,31 +374,16 @@ export class BoardsServiceProvider
return false; return false;
} }
// unlike for the board change event, every persistent state must not contain custom board config options in the FQBN this.maybeUpdateBoardListHistory({ portToSelect, boardToSelect });
const sanitizedBoardToSelect = sanitizeBoardToSelectFQBN(boardToSelect); this.maybeUpdateBoardsData({ boardToSelect, reason });
this.maybeUpdateBoardListHistory({
portToSelect,
boardToSelect: sanitizedBoardToSelect,
});
this.maybeUpdateBoardsData({
boardToSelect: sanitizedBoardToSelect,
reason,
});
if (isBoardIdentifierChangeEvent(event)) { if (isBoardIdentifierChangeEvent(event)) {
this._boardsConfig.selectedBoard = event.selectedBoard this._boardsConfig.selectedBoard = event.selectedBoard;
? sanitizeBoardIdentifierFQBN(event.selectedBoard)
: event.selectedBoard;
} }
if (isPortIdentifierChangeEvent(event)) { if (isPortIdentifierChangeEvent(event)) {
this._boardsConfig.selectedPort = event.selectedPort; this._boardsConfig.selectedPort = event.selectedPort;
} }
if (reason) {
event = Object.assign(event, { reason });
}
this.boardsConfigDidChangeEmitter.fire(event); this.boardsConfigDidChangeEmitter.fire(event);
this.refreshBoardList(); this.refreshBoardList();
this.saveState(); this.saveState();

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event'; import { Emitter, Event } from '@theia/core/lib/common/event';

View File

@ -20,7 +20,6 @@ import {
} from '../../common/protocol'; } from '../../common/protocol';
import type { BoardList } from '../../common/protocol/board-list'; import type { BoardList } from '../../common/protocol/board-list';
import { BoardsListWidget } from '../boards/boards-list-widget'; import { BoardsListWidget } from '../boards/boards-list-widget';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
ArduinoMenus, ArduinoMenus,
@ -40,8 +39,6 @@ export class BoardSelection extends SketchContribution {
private readonly menuModelRegistry: MenuModelRegistry; private readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter) @inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter; private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
@inject(BoardsService) @inject(BoardsService)
private readonly boardsService: BoardsService; private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
@ -77,29 +74,6 @@ SN: ${SN}
}); });
}, },
}); });
registry.registerCommand(BoardSelection.Commands.RELOAD_BOARD_DATA, {
execute: async () => {
const selectedFqbn =
this.boardsServiceProvider.boardList.boardsConfig.selectedBoard?.fqbn;
let message: string;
if (selectedFqbn) {
await this.boardsDataStore.reloadBoardData(selectedFqbn);
message = nls.localize(
'arduino/board/boardDataReloaded',
'Board data reloaded.'
);
} else {
message = nls.localize(
'arduino/board/selectBoardToReload',
'Please select a board first.'
);
}
this.messageService.info(message, { timeout: 2000 });
},
});
} }
override onStart(): void { override onStart(): void {
@ -177,21 +151,6 @@ SN: ${SN}
) )
); );
const reloadBoardData = {
commandId: BoardSelection.Commands.RELOAD_BOARD_DATA.id,
label: nls.localize('arduino/board/reloadBoardData', 'Reload Board Data'),
order: '102',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
reloadBoardData
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(reloadBoardData)
)
);
const getBoardInfo = { const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id, commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'), label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'),
@ -402,8 +361,5 @@ SN: ${SN}
export namespace BoardSelection { export namespace BoardSelection {
export namespace Commands { export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' }; export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
export const RELOAD_BOARD_DATA: Command = {
id: 'arduino-reload-board-data',
};
} }
} }

View File

@ -87,7 +87,8 @@ export class BoardsDataMenuUpdater extends Contribution {
execute: () => execute: () =>
this.boardsDataStore.selectConfigOption({ this.boardsDataStore.selectConfigOption({
fqbn, fqbn,
optionsToUpdate: [{ option, selectedValue: value.value }], option,
selectedValue: value.value,
}), }),
isToggled: () => value.selected, isToggled: () => value.selected,
}; };

View File

@ -37,15 +37,11 @@ export class BurnBootloader extends CoreServiceContribution {
'arduino/bootloader/burningBootloader', 'arduino/bootloader/burningBootloader',
'Burning bootloader...' 'Burning bootloader...'
), ),
task: (progressId, coreService, token) => task: (progressId, coreService) =>
coreService.burnBootloader( coreService.burnBootloader({
{ ...options,
...options, progressId,
progressId, }),
},
token
),
cancelable: true,
}); });
this.messageService.info( this.messageService.info(
nls.localize( nls.localize(

View File

@ -3,14 +3,10 @@ import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { import {
IDEUpdater, IDEUpdater,
LAST_USED_IDE_VERSION,
SKIP_IDE_VERSION, SKIP_IDE_VERSION,
} from '../../common/protocol/ide-updater'; } from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog'; import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
import { Contribution } from './contribution'; import { Contribution } from './contribution';
import { VersionWelcomeDialog } from '../dialogs/version-welcome-dialog';
import { AppService } from '../app-service';
import { SemVer } from 'semver';
@injectable() @injectable()
export class CheckForIDEUpdates extends Contribution { export class CheckForIDEUpdates extends Contribution {
@ -20,15 +16,9 @@ export class CheckForIDEUpdates extends Contribution {
@inject(IDEUpdaterDialog) @inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog; private readonly updaterDialog: IDEUpdaterDialog;
@inject(VersionWelcomeDialog)
private readonly versionWelcomeDialog: VersionWelcomeDialog;
@inject(LocalStorageService) @inject(LocalStorageService)
private readonly localStorage: LocalStorageService; private readonly localStorage: LocalStorageService;
@inject(AppService)
private readonly appService: AppService;
override onStart(): void { override onStart(): void {
this.preferences.onPreferenceChanged( this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => { ({ preferenceName, newValue, oldValue }) => {
@ -46,7 +36,7 @@ export class CheckForIDEUpdates extends Contribution {
); );
} }
override async onReady(): Promise<void> { override onReady(): void {
this.updater this.updater
.init( .init(
this.preferences.get('arduino.ide.updateChannel'), this.preferences.get('arduino.ide.updateChannel'),
@ -59,18 +49,12 @@ export class CheckForIDEUpdates extends Contribution {
return this.updater.checkForUpdates(true); return this.updater.checkForUpdates(true);
}) })
.then(async (updateInfo) => { .then(async (updateInfo) => {
if (!updateInfo) { if (!updateInfo) return;
const isNewVersion = await this.isNewStableVersion();
if (isNewVersion) {
this.versionWelcomeDialog.open();
}
return;
}
const versionToSkip = await this.localStorage.getData<string>( const versionToSkip = await this.localStorage.getData<string>(
SKIP_IDE_VERSION SKIP_IDE_VERSION
); );
if (versionToSkip === updateInfo.version) return; if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(true, updateInfo); this.updaterDialog.open(updateInfo);
}) })
.catch((e) => { .catch((e) => {
this.messageService.error( this.messageService.error(
@ -80,44 +64,6 @@ export class CheckForIDEUpdates extends Contribution {
e.message e.message
) )
); );
})
.finally(() => {
this.setCurrentIDEVersion();
}); });
} }
private async setCurrentIDEVersion(): Promise<void> {
try {
const { appVersion } = await this.appService.info();
const currSemVer = new SemVer(appVersion ?? '');
this.localStorage.setData(LAST_USED_IDE_VERSION, currSemVer.format());
} catch {
// ignore invalid versions
}
}
/**
* Check if user is running a new IDE version for the first time.
* @returns true if the current IDE version is greater than the last used version
* and both are non-prerelease versions.
*/
private async isNewStableVersion(): Promise<boolean> {
try {
const { appVersion } = await this.appService.info();
const prevVersion = await this.localStorage.getData<string>(
LAST_USED_IDE_VERSION
);
const prevSemVer = new SemVer(prevVersion ?? '');
const currSemVer = new SemVer(appVersion ?? '');
if (prevSemVer.prerelease.length || currSemVer.prerelease.length) {
return false;
}
return currSemVer.compare(prevSemVer) === 1;
} catch (e) {
return false;
}
}
} }

View File

@ -1,6 +1,8 @@
import { Dialog } from '@theia/core/lib/browser/dialogs'; import { Dialog } from '@theia/core/lib/browser/dialogs';
import type { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import type {
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application-contribution'; FrontendApplication,
OnWillStopAction,
} from '@theia/core/lib/browser/frontend-application';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import type { MaybePromise } from '@theia/core/lib/common/types'; import type { MaybePromise } from '@theia/core/lib/common/types';

View File

@ -779,7 +779,7 @@ export class CompilerErrors
return undefined; return undefined;
} else { } else {
return this.editorManager return this.editorManager
.getByUri(new URI(uriOrWidget.toString())) .getByUri(new URI(uriOrWidget))
.then((editor) => { .then((editor) => {
if (editor) { if (editor) {
return this.monacoEditor(editor); return this.monacoEditor(editor);

View File

@ -1,87 +1,83 @@
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
KeybindingContribution,
KeybindingRegistry,
} from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
import { Saveable } from '@theia/core/lib/browser/saveable';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ILogger } from '@theia/core/lib/common/logger';
import {
MenuContribution,
MenuModelRegistry,
} from '@theia/core/lib/common/menu';
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 { MaybePromise, isObject } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import { import {
inject, inject,
injectable, injectable,
interfaces, interfaces,
postConstruct, postConstruct,
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; 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 { FileService } from '@theia/filesystem/lib/browser/file-service';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { MaybePromise } from '@theia/core/lib/common/types';
import { OutputChannelSeverity } from '@theia/output/lib/browser/output-channel'; import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MainMenuManager } from '../../common/main-menu-manager'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { userAbort } from '../../common/nls'; 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 { import {
CoreError, MenuModelRegistry,
CoreService, MenuContribution,
FileSystemExt, } from '@theia/core/lib/common/menu';
ResponseServiceClient,
Sketch,
SketchesService,
} from '../../common/protocol';
import { import {
ExecuteWithProgress, KeybindingRegistry,
UserAbortApplicationError, KeybindingContribution,
} from '../../common/protocol/progressible'; } from '@theia/core/lib/browser/keybinding';
import { ArduinoPreferences } from '../arduino-preferences'; import {
import { BoardsDataStore } from '../boards/boards-data-store'; TabBarToolbarContribution,
import { BoardsServiceProvider } from '../boards/boards-service-provider'; TabBarToolbarRegistry,
import { ConfigServiceClient } from '../config/config-service-client'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { DialogService } from '../dialog-service'; import {
FrontendApplicationContribution,
FrontendApplication,
} from '@theia/core/lib/browser/frontend-application';
import {
Command,
CommandRegistry,
CommandContribution,
CommandService,
} from '@theia/core/lib/common/command';
import { SettingsService } from '../dialogs/settings/settings'; import { SettingsService } from '../dialogs/settings/settings';
import { import {
CurrentSketch, CurrentSketch,
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../sketches-service-client-impl'; } from '../sketches-service-client-impl';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service'; import {
SketchesService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
ResponseServiceClient,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel'; import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service'; 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 { export {
Command, Command,
CommandRegistry, CommandRegistry,
KeybindingRegistry,
MenuModelRegistry, MenuModelRegistry,
Sketch, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
URI, URI,
Sketch,
open, open,
}; };
@ -251,12 +247,6 @@ export abstract class CoreServiceContribution extends SketchContribution {
} }
protected handleError(error: unknown): void { protected handleError(error: unknown): void {
if (isObject(error) && UserAbortApplicationError.is(error)) {
this.outputChannelManager
.getChannel('Arduino')
.appendLine(userAbort, OutputChannelSeverity.Warning);
return;
}
this.tryToastErrorMessage(error); this.tryToastErrorMessage(error);
} }
@ -303,13 +293,7 @@ export abstract class CoreServiceContribution extends SketchContribution {
protected async doWithProgress<T>(options: { protected async doWithProgress<T>(options: {
progressText: string; progressText: string;
keepOutput?: boolean; keepOutput?: boolean;
task: ( task: (progressId: string, coreService: CoreService) => Promise<T>;
progressId: string,
coreService: CoreService,
cancellationToken?: CancellationToken
) => Promise<T>;
// false by default
cancelable?: boolean;
}): Promise<T> { }): Promise<T> {
const toDisposeOnComplete = new DisposableCollection( const toDisposeOnComplete = new DisposableCollection(
this.maybeActivateMonitorWidget() this.maybeActivateMonitorWidget()
@ -322,10 +306,8 @@ export abstract class CoreServiceContribution extends SketchContribution {
messageService: this.messageService, messageService: this.messageService,
responseService: this.responseService, responseService: this.responseService,
progressText, progressText,
run: ({ progressId, cancellationToken }) => run: ({ progressId }) => task(progressId, this.coreService),
task(progressId, this.coreService, cancellationToken),
keepOutput, keepOutput,
cancelable: options.cancelable,
}); });
toDisposeOnComplete.dispose(); toDisposeOnComplete.dispose();
return result; return result;

View File

@ -289,8 +289,8 @@ export class Debug
): Promise<boolean> { ): Promise<boolean> {
if (err instanceof Error) { if (err instanceof Error) {
try { try {
const buildPaths = await this.sketchesService.getBuildPath(sketch); const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
return buildPaths.some((tempBuildPath) => return tempBuildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath) err.message.includes(tempBuildPath)
); );
} catch { } catch {
@ -398,9 +398,12 @@ export async function isDebugEnabled(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}` `Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
); );
} }
if (!data.selectedProgrammer) {
throw new Error(noProgrammerSelectedFor(board.name));
}
const params = { const params = {
fqbn: fqbnWithConfig, fqbn: fqbnWithConfig,
programmer: data.selectedProgrammer?.id, programmer: data.selectedProgrammer.id,
}; };
try { try {
const debugFqbn = await checkDebugEnabled(params); const debugFqbn = await checkDebugEnabled(params);
@ -440,3 +443,13 @@ export function debuggingNotSupported(boardName: string): string {
boardName boardName
); );
} }
/**
* (non-API)
*/
export function noProgrammerSelectedFor(boardName: string): string {
return nls.localize(
'arduino/debug/noProgrammerSelectedFor',
"No programmer selected for '{0}'",
boardName
);
}

View File

@ -1,11 +1,7 @@
import { nls } from '@theia/core/lib/common';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import { import {
Contribution, Contribution,
Command, Command,
@ -14,11 +10,17 @@ import {
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072 // TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964 // Depends on https://github.com/eclipse-theia/theia/pull/7964
@injectable() @injectable()
export class EditContributions extends Contribution { export class EditContributions extends Contribution {
@inject(MonacoEditorService)
private readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService) @inject(ClipboardService)
private readonly clipboardService: ClipboardService; private readonly clipboardService: ClipboardService;
@ -206,10 +208,9 @@ ${value}
protected async current(): Promise< protected async current(): Promise<
ICodeEditor | StandaloneCodeEditor | undefined ICodeEditor | StandaloneCodeEditor | undefined
> { > {
const codeEditorService = StandaloneServices.get(ICodeEditorService);
return ( return (
codeEditorService.getFocusedCodeEditor() || this.codeEditorService.getFocusedCodeEditor() ||
codeEditorService.getActiveCodeEditor() || this.codeEditorService.getActiveCodeEditor() ||
undefined undefined
); );
} }

View File

@ -8,8 +8,8 @@ import {
ArduinoDaemon, ArduinoDaemon,
BoardIdentifier, BoardIdentifier,
BoardsService, BoardsService,
CompileSummary,
ExecutableService, ExecutableService,
assertSanitizedFqbn,
isBoardIdentifierChangeEvent, isBoardIdentifierChangeEvent,
sanitizeFqbn, sanitizeFqbn,
} from '../../common/protocol'; } from '../../common/protocol';
@ -24,7 +24,6 @@ import { HostedPluginEvents } from '../hosted/hosted-plugin-events';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution, URI } from './contribution'; import { SketchContribution, URI } from './contribution';
import { CompileSummaryProvider } from './verify-sketch';
interface DaemonAddress { interface DaemonAddress {
/** /**
@ -109,8 +108,6 @@ export class InoLanguage extends SketchContribution {
private readonly notificationCenter: NotificationCenter; private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore) @inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore; private readonly boardDataStore: BoardsDataStore;
@inject(CompileSummaryProvider)
private readonly compileSummaryProvider: CompileSummaryProvider;
private readonly toDispose = new DisposableCollection(); private readonly toDispose = new DisposableCollection();
private readonly languageServerStartMutex = new Mutex(); private readonly languageServerStartMutex = new Mutex();
@ -162,11 +159,14 @@ export class InoLanguage extends SketchContribution {
this.notificationCenter.onDidReinitialize(() => forceRestart()), this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onDidChange((event) => { this.boardDataStore.onDidChange((event) => {
if (this.languageServerFqbn) { if (this.languageServerFqbn) {
const sanitizedFQBN = sanitizeFqbn(this.languageServerFqbn); const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
// The incoming FQBNs might contain custom boards configs, sanitize them before the comparison. if (!sanitizeFqbn) {
// https://github.com/arduino/arduino-ide/pull/2113#pullrequestreview-1499998328 throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
);
}
const matchingChange = event.changes.find( const matchingChange = event.changes.find(
(change) => sanitizedFQBN === sanitizeFqbn(change.fqbn) (change) => change.fqbn === sanitizedFqbn
); );
const { boardsConfig } = this.boardsServiceProvider; const { boardsConfig } = this.boardsServiceProvider;
if ( if (
@ -177,13 +177,6 @@ export class InoLanguage extends SketchContribution {
} }
} }
}), }),
this.compileSummaryProvider.onDidChangeCompileSummary(
(compileSummary) => {
if (compileSummary) {
this.fireBuildDidComplete(compileSummary);
}
}
),
]); ]);
Promise.all([ Promise.all([
this.boardsServiceProvider.ready, this.boardsServiceProvider.ready,
@ -203,7 +196,11 @@ export class InoLanguage extends SketchContribution {
forceStart = false forceStart = false
): Promise<void> { ): Promise<void> {
const port = await this.daemon.tryGetPort(); const port = await this.daemon.tryGetPort();
if (typeof port !== 'number') { if (!port) {
return;
}
const portNumber = Number.parseInt(port, 10); // TODO: IDE2 APIs should provide a number and not string
if (Number.isNaN(portNumber)) {
return; return;
} }
const release = await this.languageServerStartMutex.acquire(); const release = await this.languageServerStartMutex.acquire();
@ -235,6 +232,7 @@ export class InoLanguage extends SketchContribution {
} }
return; return;
} }
assertSanitizedFqbn(fqbn);
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn); const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) { if (!fqbnWithConfig) {
throw new Error( throw new Error(
@ -282,7 +280,7 @@ export class InoLanguage extends SketchContribution {
lsPath, lsPath,
daemonAddress: { daemonAddress: {
hostname: 'localhost', hostname: 'localhost',
port, port: portNumber,
instance: 1, // TODO: get it from the backend instance: 1, // TODO: get it from the backend
}, },
clangdPath, clangdPath,
@ -328,32 +326,4 @@ export class InoLanguage extends SketchContribution {
params params
); );
} }
// Execute the a command contributed by the Arduino Tools VSIX to send the `ino/buildDidComplete` notification to the language server
private async fireBuildDidComplete(
compileSummary: CompileSummary
): Promise<void> {
const params = {
...compileSummary,
};
console.info(
`Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify(
params.buildOutputUri
)}`
);
try {
await this.commandService.executeCommand(
'arduino.languageserver.notifyBuildDidComplete',
params
);
} catch (err) {
console.error(
`Unexpected error when firing event on build did complete. ${JSON.stringify(
params.buildOutputUri
)}`,
err
);
}
}
} }

View File

@ -12,7 +12,7 @@ export class OpenBoardsConfig extends Contribution {
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, { registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
execute: async (params?: EditBoardsConfigActionParams) => execute: async (params?: EditBoardsConfigActionParams) =>
this.boardsConfigDialog.open(true, params), this.boardsConfigDialog.open(params),
}); });
} }
} }

View File

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

View File

@ -1,11 +1,13 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import type { ArduinoState } from 'vscode-arduino-api'; import type { ArduinoState } from 'vscode-arduino-api';
import { import {
BoardsConfig,
BoardsService, BoardsService,
CompileSummary, CompileSummary,
isCompileSummary,
BoardsConfig,
PortIdentifier, PortIdentifier,
resolveDetectedPort, resolveDetectedPort,
} from '../../common/protocol'; } from '../../common/protocol';
@ -16,10 +18,8 @@ import {
} from '../../common/protocol/arduino-context-mapper'; } from '../../common/protocol/arduino-context-mapper';
import { BoardsDataStore } from '../boards/boards-data-store'; import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution } from './contribution'; import { SketchContribution } from './contribution';
import { CompileSummaryProvider } from './verify-sketch';
/** /**
* (non-API) exported for tests * (non-API) exported for tests
@ -43,8 +43,6 @@ export class UpdateArduinoState extends SketchContribution {
private readonly boardsDataStore: BoardsDataStore; private readonly boardsDataStore: BoardsDataStore;
@inject(HostedPluginSupport) @inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport; private readonly hostedPluginSupport: HostedPluginSupport;
@inject(CompileSummaryProvider)
private readonly compileSummaryProvider: CompileSummaryProvider;
private readonly toDispose = new DisposableCollection(); private readonly toDispose = new DisposableCollection();
@ -62,13 +60,14 @@ export class UpdateArduinoState extends SketchContribution {
this.configService.onDidChangeSketchDirUri((userDirUri) => this.configService.onDidChangeSketchDirUri((userDirUri) =>
this.updateUserDirPath(userDirUri) this.updateUserDirPath(userDirUri)
), ),
this.compileSummaryProvider.onDidChangeCompileSummary( this.commandService.onDidExecuteCommand(({ commandId, args }) => {
(compilerSummary) => { if (
if (compilerSummary) { commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
this.updateCompileSummary(compilerSummary); isCompileSummary(args[0])
} ) {
this.updateCompileSummary(args[0]);
} }
), }),
this.boardsDataStore.onDidChange((event) => { this.boardsDataStore.onDidChange((event) => {
const selectedFqbn = const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
@ -89,10 +88,6 @@ export class UpdateArduinoState extends SketchContribution {
this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch()); this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch());
this.updateUserDirPath(this.configService.tryGetSketchDirUri()); this.updateUserDirPath(this.configService.tryGetSketchDirUri());
this.updateDataDirPath(this.configService.tryGetDataDirUri()); this.updateDataDirPath(this.configService.tryGetDataDirUri());
const { compileSummary } = this.compileSummaryProvider;
if (compileSummary) {
this.updateCompileSummary(compileSummary);
}
} }
onStop(): void { onStop(): void {

View File

@ -1,8 +1,7 @@
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { FQBN } from 'fqbn'; import { CoreService, sanitizeFqbn } from '../../common/protocol';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl'; import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@ -104,7 +103,6 @@ export class UploadSketch extends CoreServiceContribution {
} }
try { try {
const autoVerify = this.preferences['arduino.upload.autoVerify'];
// toggle the toolbar button and menu item state. // toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not // uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true; this.uploadInProgress = true;
@ -117,7 +115,7 @@ export class UploadSketch extends CoreServiceContribution {
'arduino-verify-sketch', 'arduino-verify-sketch',
<VerifySketchParams>{ <VerifySketchParams>{
exportBinaries: false, exportBinaries: false,
mode: autoVerify ? 'auto' : 'dry-run', silent: true,
} }
); );
if (!verifyOptions) { if (!verifyOptions) {
@ -128,7 +126,6 @@ export class UploadSketch extends CoreServiceContribution {
usingProgrammer, usingProgrammer,
verifyOptions verifyOptions
); );
if (!uploadOptions) { if (!uploadOptions) {
return; return;
} }
@ -139,37 +136,10 @@ export class UploadSketch extends CoreServiceContribution {
const uploadResponse = await this.doWithProgress({ const uploadResponse = await this.doWithProgress({
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'), progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
task: async (progressId, coreService, token) => { task: (progressId, coreService) =>
try { coreService.upload({ ...uploadOptions, progressId }),
return await coreService.upload(
{ ...uploadOptions, progressId },
token
);
} catch (err) {
if (err.code === 4005) {
const uploadWithProgrammerOptions = await this.uploadOptions(
true,
verifyOptions
);
if (uploadWithProgrammerOptions) {
return coreService.upload(
{ ...uploadWithProgrammerOptions, progressId },
token
);
}
} else {
throw err;
}
}
},
keepOutput: true, keepOutput: true,
cancelable: true,
}); });
if (!uploadResponse) {
return;
}
// the port update is NOOP if nothing has changed // the port update is NOOP if nothing has changed
this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload); this.boardsServiceProvider.updateConfig(uploadResponse.portAfterUpload);
@ -202,11 +172,7 @@ export class UploadSketch extends CoreServiceContribution {
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([ await Promise.all([
verifyOptions.fqbn, // already decorated FQBN verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData( this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
verifyOptions.fqbn
? new FQBN(verifyOptions.fqbn).toString(true)
: undefined
),
this.preferences.get('arduino.upload.verify'), this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'), this.preferences.get('arduino.upload.verbose'),
]); ]);

View File

@ -1,71 +1,46 @@
import { Emitter, Event } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import type { CompileSummary, CoreService } from '../../common/protocol'; import { Emitter } from '@theia/core/lib/common/event';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
CoreServiceContribution,
Command, Command,
CommandRegistry, CommandRegistry,
CoreServiceContribution,
KeybindingRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { CoreService } from '../../common/protocol';
import { CoreErrorHandler } from './core-error-handler'; import { CoreErrorHandler } from './core-error-handler';
export const CompileSummaryProvider = Symbol('CompileSummaryProvider');
export interface CompileSummaryProvider {
readonly compileSummary: CompileSummary | undefined;
readonly onDidChangeCompileSummary: Event<CompileSummary | undefined>;
}
export type VerifySketchMode =
/**
* When the user explicitly triggers the verify command from the primary UI: menu, toolbar, or keybinding. The UI shows the output, updates the toolbar items state, etc.
*/
| 'explicit'
/**
* When the verify phase automatically runs as part of the upload but there is no UI indication of the command: the toolbar items do not update.
*/
| 'auto'
/**
* The verify does not run. There is no UI indication of the command. For example, when the user decides to disable the auto verify (`'arduino.upload.autoVerify'`) to skips the code recompilation phase.
*/
| 'dry-run';
export interface VerifySketchParams { export interface VerifySketchParams {
/** /**
* Same as `CoreService.Options.Compile#exportBinaries` * Same as `CoreService.Options.Compile#exportBinaries`
*/ */
readonly exportBinaries?: boolean; readonly exportBinaries?: boolean;
/** /**
* The mode specifying how verify should run. It's `'explicit'` by default. * If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
*/ */
readonly mode?: VerifySketchMode; readonly silent?: boolean;
} }
/** /**
* - `"idle"` when neither verify, nor upload is running * - `"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' | VerifySketchMode; type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
@injectable() @injectable()
export class VerifySketch export class VerifySketch extends CoreServiceContribution {
extends CoreServiceContribution
implements CompileSummaryProvider
{
@inject(CoreErrorHandler) @inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler; private readonly coreErrorHandler: CoreErrorHandler;
private readonly onDidChangeEmitter = new Emitter<void>(); private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event; private readonly onDidChange = this.onDidChangeEmitter.event;
private readonly onDidChangeCompileSummaryEmitter = new Emitter<
CompileSummary | undefined
>();
private verifyProgress: VerifyProgress = 'idle'; private verifyProgress: VerifyProgress = 'idle';
private _compileSummary: CompileSummary | undefined;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
@ -79,10 +54,10 @@ export class VerifySketch
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => this.verifyProgress !== 'explicit', isEnabled: () => this.verifyProgress !== 'explicit-verify',
// toggled only when verify is running, but not toggled when automatic verify is running before the upload // 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 // https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
isToggled: () => this.verifyProgress === 'explicit', isToggled: () => this.verifyProgress === 'explicit-verify',
execute: () => execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id), registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
}); });
@ -130,21 +105,6 @@ export class VerifySketch
super.handleError(error); super.handleError(error);
} }
get compileSummary(): CompileSummary | undefined {
return this._compileSummary;
}
private updateCompileSummary(
compileSummary: CompileSummary | undefined
): void {
this._compileSummary = compileSummary;
this.onDidChangeCompileSummaryEmitter.fire(this._compileSummary);
}
get onDidChangeCompileSummary(): Event<CompileSummary | undefined> {
return this.onDidChangeCompileSummaryEmitter.event;
}
private async verifySketch( private async verifySketch(
params?: VerifySketchParams params?: VerifySketchParams
): Promise<CoreService.Options.Compile | undefined> { ): Promise<CoreService.Options.Compile | undefined> {
@ -153,44 +113,34 @@ export class VerifySketch
} }
try { try {
this.verifyProgress = params?.mode ?? 'explicit'; this.verifyProgress = params?.silent
? 'automatic-verify'
: 'explicit-verify';
this.onDidChangeEmitter.fire(); this.onDidChangeEmitter.fire();
this.menuManager.update(); this.menuManager.update();
this.clearVisibleNotification(); this.clearVisibleNotification();
this.coreErrorHandler.reset(); this.coreErrorHandler.reset();
const dryRun = this.verifyProgress === 'dry-run';
const options = await this.options(params?.exportBinaries); const options = await this.options(params?.exportBinaries);
if (!options) { if (!options) {
return undefined; return undefined;
} }
if (dryRun) { await this.doWithProgress({
return options;
}
const compileSummary = await this.doWithProgress({
progressText: nls.localize( progressText: nls.localize(
'arduino/sketch/compile', 'arduino/sketch/compile',
'Compiling sketch...' 'Compiling sketch...'
), ),
task: (progressId, coreService, token) => task: (progressId, coreService) =>
coreService.compile( coreService.compile({
{ ...options,
...options, progressId,
progressId, }),
},
token
),
cancelable: true,
}); });
this.messageService.info( this.messageService.info(
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'), nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
{ timeout: 3000 } { timeout: 3000 }
); );
this.updateCompileSummary(compileSummary);
// Returns with the used options for the compilation // Returns with the used options for the compilation
// so that follow-up tasks (such as upload) can reuse the compiled code. // so that follow-up tasks (such as upload) can reuse the compiled code.
// Note that the `fqbn` is already decorated with the board settings, if any. // Note that the `fqbn` is already decorated with the board settings, if any.

View File

@ -509,19 +509,11 @@ export class CreateApi {
private async headers(): Promise<Record<string, string>> { private async headers(): Promise<Record<string, string>> {
const token = await this.token(); const token = await this.token();
const headers: Record<string, string> = { return {
'content-type': 'application/json', 'content-type': 'application/json',
accept: 'application/json', accept: 'application/json',
authorization: `Bearer ${token}`, authorization: `Bearer ${token}`,
}; };
const sharedSpaceID =
this.arduinoPreferences['arduino.cloud.sharedSpaceID'];
if (sharedSpaceID) {
headers['x-organization'] = sharedSpaceID;
}
return headers;
} }
private domain(apiVersion = 'v2'): string { private domain(apiVersion = 'v2'): string {

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event'; import { Emitter, Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';

View File

@ -5,7 +5,7 @@ import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
Stat, Stat,
FileType, FileType,

View File

@ -17,7 +17,6 @@ import {
} from '../../../common/protocol/ide-updater'; } from '../../../common/protocol/ide-updater';
import { LocalStorageService } from '@theia/core/lib/browser'; import { LocalStorageService } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { sanitize } from 'dompurify';
@injectable() @injectable()
export class IDEUpdaterDialogProps extends DialogProps {} export class IDEUpdaterDialogProps extends DialogProps {}
@ -166,51 +165,6 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
goToDownloadPageButton.focus(); goToDownloadPageButton.focus();
} }
private appendDonateFooter() {
const footer = document.createElement('div');
footer.classList.add('ide-updater-dialog--footer');
const footerContent = document.createElement('div');
footerContent.classList.add('ide-updater-dialog--footer-content');
footer.appendChild(footerContent);
const footerLink = document.createElement('a');
footerLink.innerText = sanitize(
nls.localize('arduino/ide-updater/donateLinkText', 'donate to support us')
);
footerLink.classList.add('ide-updater-dialog--footer-link');
footerLink.onclick = () =>
this.openExternal('https://www.arduino.cc/en/donate');
const footerLinkIcon = document.createElement('span');
footerLinkIcon.title = nls.localize(
'arduino/ide-updater/donateLinkIconTitle',
'open donation page'
);
footerLinkIcon.classList.add('ide-updater-dialog--footer-link-icon');
footerLink.appendChild(footerLinkIcon);
const placeholderKey = '%%link%%';
const footerText = sanitize(
nls.localize(
'arduino/ide-updater/donateText',
'Open source is love, {0}',
placeholderKey
)
);
const placeholder = footerText.indexOf(placeholderKey);
if (placeholder !== -1) {
const parts = footerText.split(placeholderKey);
footerContent.appendChild(document.createTextNode(parts[0]));
footerContent.appendChild(footerLink);
footerContent.appendChild(document.createTextNode(parts[1]));
} else {
footerContent.appendChild(document.createTextNode(footerText));
footerContent.appendChild(footerLink);
}
this.controlPanel.insertAdjacentElement('afterend', footer);
}
private openDownloadPage(): void { private openDownloadPage(): void {
this.openExternal('https://www.arduino.cc/en/software'); this.openExternal('https://www.arduino.cc/en/software');
this.close(); this.close();
@ -233,7 +187,6 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
downloadStarted: true, downloadStarted: true,
}); });
this.clearButtons(); this.clearButtons();
this.appendDonateFooter();
this.updater.downloadUpdate(); this.updater.downloadUpdate();
} }
@ -261,7 +214,6 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
} }
override async open( override async open(
disposeOnResolve = true,
data: UpdateInfo | undefined = undefined data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> { ): Promise<UpdateInfo | undefined> {
if (data && data.version) { if (data && data.version) {
@ -272,7 +224,7 @@ export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
error: undefined, error: undefined,
}); });
this.updateInfo = data; this.updateInfo = data;
return super.open(disposeOnResolve); return super.open();
} }
} }

View File

@ -1,107 +0,0 @@
import React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactDialog } from '../theia/dialogs/dialogs';
import { nls } from '@theia/core';
import { DialogProps } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { AppService } from '../app-service';
import { sanitize } from 'dompurify';
@injectable()
export class VersionWelcomeDialogProps extends DialogProps {}
@injectable()
export class VersionWelcomeDialog extends ReactDialog<void> {
@inject(AppService)
private readonly appService: AppService;
@inject(WindowService)
private readonly windowService: WindowService;
constructor(
@inject(VersionWelcomeDialogProps)
protected override readonly props: VersionWelcomeDialogProps
) {
super({
title: nls.localize(
'arduino/versionWelcome/title',
'Welcome to a new version of the Arduino IDE!'
),
});
this.node.id = 'version-welcome-dialog-container';
this.contentNode.classList.add('version-welcome-dialog');
}
protected render(): React.ReactNode {
return (
<div>
<p>
{nls.localize(
'arduino/versionWelcome/donateMessage',
'Arduino is committed to keeping software free and open-source for everyone. Your donation helps us develop new features, improve libraries, and support millions of users worldwide.'
)}
</p>
<p className="bold">
{nls.localize(
'arduino/versionWelcome/donateMessage2',
'Please consider supporting our work on the free open source Arduino IDE.'
)}
</p>
</div>
);
}
override get value(): void {
return;
}
private appendButtons(): void {
const cancelButton = this.createButton(
nls.localize('arduino/versionWelcome/cancelButton', 'Maybe later')
);
cancelButton.classList.add('secondary');
cancelButton.classList.add('cancel-button');
this.addAction(cancelButton, this.close.bind(this), 'click');
this.controlPanel.appendChild(cancelButton);
const donateButton = this.createButton(
nls.localize('arduino/versionWelcome/donateButton', 'Donate now')
);
this.addAction(donateButton, this.onDonateButtonClick.bind(this), 'click');
this.controlPanel.appendChild(donateButton);
donateButton.focus();
}
private onDonateButtonClick(): void {
this.openDonationPage();
this.close();
}
private readonly openDonationPage = () => {
const url = 'https://www.arduino.cc/en/donate';
this.windowService.openNewWindow(url, { external: true });
};
private async updateTitleVersion(): Promise<void> {
const appInfo = await this.appService.info();
const { appVersion } = appInfo;
if (appVersion) {
this.titleNode.innerText = sanitize(
nls.localize(
'arduino/versionWelcome/titleWithVersion',
'Welcome to the new Arduino IDE {0}!',
appVersion
)
);
}
}
protected override onAfterAttach(msg: Message): void {
this.update();
this.appendButtons();
this.updateTitleVersion();
super.onAfterAttach(msg);
}
}

View File

@ -1,3 +0,0 @@
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.878141 10.6219C0.960188 10.7039 1.07147 10.75 1.1875 10.75H10.8125C10.9285 10.75 11.0398 10.7039 11.1219 10.6219C11.2039 10.5398 11.25 10.4285 11.25 10.3125V6.81252C11.25 6.69648 11.2039 6.5852 11.1219 6.50316C11.0398 6.42111 10.9285 6.37502 10.8125 6.37502C10.6965 6.37502 10.5852 6.42111 10.5031 6.50316C10.4211 6.5852 10.375 6.69648 10.375 6.81252V9.87502H1.625V1.12502H4.6875C4.80353 1.12502 4.91481 1.07892 4.99686 0.996874C5.07891 0.914827 5.125 0.803548 5.125 0.687515C5.125 0.571483 5.07891 0.460203 4.99686 0.378156C4.91481 0.296109 4.80353 0.250015 4.6875 0.250015H1.1875C1.07147 0.250015 0.960188 0.296109 0.878141 0.378156C0.796094 0.460203 0.75 0.571483 0.75 0.687515V10.3125C0.75 10.4285 0.796094 10.5398 0.878141 10.6219ZM11.25 4.62502V0.687515C11.25 0.571483 11.2039 0.460203 11.1219 0.378156C11.0398 0.296109 10.9285 0.250015 10.8125 0.250015H6.875C6.75897 0.250015 6.64769 0.296109 6.56564 0.378156C6.48359 0.460203 6.4375 0.571483 6.4375 0.687515C6.4375 0.803548 6.48359 0.914827 6.56564 0.996874C6.64769 1.07892 6.75897 1.12502 6.875 1.12502H9.75375L5.68937 5.18939C5.64837 5.23006 5.61582 5.27845 5.59361 5.33176C5.5714 5.38508 5.55996 5.44226 5.55996 5.50002C5.55996 5.55777 5.5714 5.61495 5.59361 5.66827C5.61582 5.72158 5.64837 5.76997 5.68937 5.81064C5.73005 5.85165 5.77843 5.88419 5.83175 5.90641C5.88506 5.92862 5.94224 5.94005 6 5.94005C6.05776 5.94005 6.11494 5.92862 6.16825 5.90641C6.22157 5.88419 6.26995 5.85165 6.31062 5.81064L10.375 1.74627V4.62502C10.375 4.74105 10.4211 4.85233 10.5031 4.93437C10.5852 5.01642 10.6965 5.06252 10.8125 5.06252C10.9285 5.06252 11.0398 5.01642 11.1219 4.93437C11.2039 4.85233 11.25 4.74105 11.25 4.62502Z" fill="#008184"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -30,7 +30,7 @@ export class IDEUpdaterCommands implements CommandContribution {
try { try {
const updateInfo = await this.updater.checkForUpdates(initialCheck); const updateInfo = await this.updater.checkForUpdates(initialCheck);
if (!!updateInfo) { if (!!updateInfo) {
this.updaterDialog.open(true, updateInfo); this.updaterDialog.open(updateInfo);
} else { } else {
this.messageService.info( this.messageService.info(
nls.localize( nls.localize(

View File

@ -12,13 +12,15 @@ import {
LibrarySearch, LibrarySearch,
LibraryService, LibraryService,
} from '../../common/protocol/library-service'; } from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget'; import {
ListWidget,
UserAbortError,
} from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol'; import { Installable } from '../../common/protocol';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer'; import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
import { findChildTheiaButton, splitByBoldTag } from '../utils/dom'; import { findChildTheiaButton, splitByBoldTag } from '../utils/dom';
import { UserAbortError } from '../../common/protocol/progressible';
@injectable() @injectable()
export class LibraryListWidget extends ListWidget< export class LibraryListWidget extends ListWidget<

View File

@ -6,7 +6,7 @@ import {
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
IndexUpdateDidCompleteParams, IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams, IndexUpdateDidFailParams,
@ -46,7 +46,7 @@ export class NotificationCenter
new Emitter<ProgressMessage>(); new Emitter<ProgressMessage>();
private readonly indexUpdateDidFailEmitter = private readonly indexUpdateDidFailEmitter =
new Emitter<IndexUpdateDidFailParams>(); new Emitter<IndexUpdateDidFailParams>();
private readonly daemonDidStartEmitter = new Emitter<number>(); private readonly daemonDidStartEmitter = new Emitter<string>();
private readonly daemonDidStopEmitter = new Emitter<void>(); private readonly daemonDidStopEmitter = new Emitter<void>();
private readonly configDidChangeEmitter = new Emitter<ConfigState>(); private readonly configDidChangeEmitter = new Emitter<ConfigState>();
private readonly platformDidInstallEmitter = new Emitter<{ private readonly platformDidInstallEmitter = new Emitter<{
@ -136,7 +136,7 @@ export class NotificationCenter
this.indexUpdateDidFailEmitter.fire(params); this.indexUpdateDidFailEmitter.fire(params);
} }
notifyDaemonDidStart(port: number): void { notifyDaemonDidStart(port: string): void {
this.daemonDidStartEmitter.fire(port); this.daemonDidStartEmitter.fire(port);
} }

View File

@ -9,7 +9,7 @@ import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { Sketch, SketchesService } from '../common/protocol'; import { Sketch, SketchesService } from '../common/protocol';
import { ConfigServiceClient } from './config/config-service-client'; import { ConfigServiceClient } from './config/config-service-client';
import { import {
@ -74,7 +74,6 @@ export class SketchesServiceClientImpl
const sketchDirUri = this.configService.tryGetSketchDirUri(); const sketchDirUri = this.configService.tryGetSketchDirUri();
this.watchSketchbookDir(sketchDirUri); this.watchSketchbookDir(sketchDirUri);
const refreshCurrentSketch = async () => { const refreshCurrentSketch = async () => {
await this.workspaceService.ready;
const currentSketch = await this.loadCurrentSketch(); const currentSketch = await this.loadCurrentSketch();
const ideTempFolderUri = await this.getIdeTempFolderUriForSketch( const ideTempFolderUri = await this.getIdeTempFolderUriForSketch(
currentSketch currentSketch
@ -288,7 +287,7 @@ export class SketchesServiceClientImpl
* `true` if the `uri` is not contained in any of the opened workspaces. Otherwise, `false`. * `true` if the `uri` is not contained in any of the opened workspaces. Otherwise, `false`.
*/ */
isReadOnly(uri: URI | monaco.Uri | string): boolean { isReadOnly(uri: URI | monaco.Uri | string): boolean {
const toCheck = uri instanceof URI ? uri : new URI(uri.toString()); const toCheck = uri instanceof URI ? uri : new URI(uri);
if (toCheck.scheme === 'user-storage') { if (toCheck.scheme === 'user-storage') {
return false; return false;
} }

View File

@ -98,12 +98,16 @@
color: var(--theia-textLink-foreground); color: var(--theia-textLink-foreground);
} }
img.arduino-account-picture { .account-icon {
width: var(--theia-private-sidebar-icon-size); width: var(--theia-private-sidebar-icon-size);
height: var(--theia-private-sidebar-icon-size); height: var(--theia-private-sidebar-icon-size);
border-radius: 50%;
overflow: hidden;
}
.account-icon > img {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
border-radius: 50%;
} }
.connected-status-icon { .connected-status-icon {

View File

@ -34,37 +34,6 @@
min-width: 0; min-width: 0;
} }
.ide-updater-dialog--footer {
display: inline-block;
margin-top: -16px;
padding: 12px 0 24px 0;
border-top: 1px solid var(--theia-editorWidget-border);
}
.ide-updater-dialog--footer-content {
float: right;
}
.ide-updater-dialog--footer-link {
display: inline-block;
color: var(--theia-textLink-foreground);
font-weight: 500;
line-height: 13px;
}
.ide-updater-dialog--footer-link:hover {
color: var(--theia-textLink-foreground);
cursor: pointer;
}
.ide-updater-dialog--footer-link-icon {
display: inline-block;
-webkit-mask: url(../icons/link-open-icon.svg) center no-repeat;
background-color: var(--theia-textLink-foreground);
height: 12px;
width: 12px;
cursor: pointer;
transform: translateY(2px);
margin-left: 4px;
}
.ide-updater-dialog .changelog { .ide-updater-dialog .changelog {
color: var(--theia-editor-foreground); color: var(--theia-editor-foreground);
background-color: var(--theia-editor-background); background-color: var(--theia-editor-background);
@ -140,7 +109,6 @@
max-height: 100%; max-height: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
padding-bottom: 20px !important;
} }
#ide-updater-dialog-container .skip-version-button { #ide-updater-dialog-container .skip-version-button {

View File

@ -10,7 +10,6 @@
@import "./settings-dialog.css"; @import "./settings-dialog.css";
@import "./firmware-uploader-dialog.css"; @import "./firmware-uploader-dialog.css";
@import "./ide-updater-dialog.css"; @import "./ide-updater-dialog.css";
@import "./version-welcome-dialog.css";
@import "./certificate-uploader-dialog.css"; @import "./certificate-uploader-dialog.css";
@import "./user-fields-dialog.css"; @import "./user-fields-dialog.css";
@import "./debug.css"; @import "./debug.css";

View File

@ -1,7 +0,0 @@
#version-welcome-dialog-container > .dialogBlock {
width: 546px;
.bold {
font-weight: bold;
}
}

View File

@ -13,8 +13,6 @@ import { MessageService } from '@theia/core/lib/common/message-service';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { ApplicationConnectionStatusContribution } from './connection-status-service'; import { ApplicationConnectionStatusContribution } from './connection-status-service';
import { ToolbarAwareTabBar } from './tab-bars'; import { ToolbarAwareTabBar } from './tab-bars';
import { find } from '@theia/core/shared/@phosphor/algorithm';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
@injectable() @injectable()
export class ApplicationShell extends TheiaApplicationShell { export class ApplicationShell extends TheiaApplicationShell {
@ -50,38 +48,6 @@ export class ApplicationShell extends TheiaApplicationShell {
return super.addWidget(widget, { ...options, ref }); return super.addWidget(widget, { ...options, ref });
} }
override doRevealWidget(id: string): Widget | undefined {
let widget = find(this.mainPanel.widgets(), (w) => w.id === id);
if (!widget) {
widget = find(this.bottomPanel.widgets(), (w) => w.id === id);
if (widget) {
this.expandBottomPanel();
}
}
if (widget) {
const tabBar = this.getTabBarFor(widget);
if (tabBar) {
tabBar.currentTitle = widget.title;
}
}
if (!widget) {
widget = this.leftPanelHandler.expand(id);
}
if (!widget) {
widget = this.rightPanelHandler.expand(id);
}
if (widget) {
// Prevent focusing the output widget when is updated
// See https://github.com/arduino/arduino-ide/issues/2679
if (!(widget instanceof OutputWidget)) {
this.windowService.focus();
}
return widget;
} else {
return this.secondaryWindowHandler.revealWidget(id);
}
}
override handleEvent(): boolean { override handleEvent(): boolean {
// NOOP, dragging has been disabled // NOOP, dragging has been disabled
return false; return false;

View File

@ -2,7 +2,7 @@ import {
CommonCommands, CommonCommands,
CommonFrontendContribution as TheiaCommonFrontendContribution, CommonFrontendContribution as TheiaCommonFrontendContribution,
} from '@theia/core/lib/browser/common-frontend-contribution'; } from '@theia/core/lib/browser/common-frontend-contribution';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application-contribution'; import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application';
import type { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import type { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import type { CommandRegistry } from '@theia/core/lib/common/command'; import type { CommandRegistry } from '@theia/core/lib/common/command';
import type { MenuModelRegistry } from '@theia/core/lib/common/menu'; import type { MenuModelRegistry } from '@theia/core/lib/common/menu';

View File

@ -1,10 +1,10 @@
import { import {
ConnectionStatus,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution, ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus,
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service'; } from '@theia/core/lib/browser/connection-status-service';
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { WebSocketConnectionSource } from '@theia/core/lib/browser/messaging/ws-connection-source'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/index';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar'; import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { Disposable } from '@theia/core/lib/common/disposable'; import { Disposable } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event'; import { Emitter, Event } from '@theia/core/lib/common/event';
@ -74,8 +74,8 @@ export class DaemonPort implements FrontendApplicationContribution {
@inject(NotificationCenter) @inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter; private readonly notificationCenter: NotificationCenter;
private readonly onPortDidChangeEmitter = new Emitter<number | undefined>(); private readonly onPortDidChangeEmitter = new Emitter<string | undefined>();
private _port: number | undefined; private _port: string | undefined;
onStart(): void { onStart(): void {
this.daemon.tryGetPort().then( this.daemon.tryGetPort().then(
@ -91,15 +91,15 @@ export class DaemonPort implements FrontendApplicationContribution {
this.onPortDidChangeEmitter.dispose(); this.onPortDidChangeEmitter.dispose();
} }
get port(): number | undefined { get port(): string | undefined {
return this._port; return this._port;
} }
get onDidChangePort(): Event<number | undefined> { get onDidChangePort(): Event<string | undefined> {
return this.onPortDidChangeEmitter.event; return this.onPortDidChangeEmitter.event;
} }
private setPort(port: number | undefined): void { private setPort(port: string | undefined): void {
const oldPort = this._port; const oldPort = this._port;
this._port = port; this._port = port;
if (this._port !== oldPort) { if (this._port !== oldPort) {
@ -114,8 +114,8 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat
private readonly daemonPort: DaemonPort; private readonly daemonPort: DaemonPort;
@inject(IsOnline) @inject(IsOnline)
private readonly isOnline: IsOnline; private readonly isOnline: IsOnline;
@inject(WebSocketConnectionSource) @inject(WebSocketConnectionProvider)
private readonly connectionSource: WebSocketConnectionSource; private readonly connectionProvider: WebSocketConnectionProvider;
@postConstruct() @postConstruct()
protected override init(): void { protected override init(): void {
@ -128,7 +128,7 @@ export class FrontendConnectionStatusService extends TheiaFrontendConnectionStat
} }
protected override async performPingRequest(): Promise<void> { protected override async performPingRequest(): Promise<void> {
if (!this.connectionSource['socket'].connected) { if (!this.connectionProvider['socket'].connected) {
this.updateStatus(false); this.updateStatus(false);
return; return;
} }
@ -171,8 +171,8 @@ export class ApplicationConnectionStatusContribution extends TheiaApplicationCon
private readonly notificationManager: NotificationManager; private readonly notificationManager: NotificationManager;
@inject(CreateFeatures) @inject(CreateFeatures)
private readonly createFeatures: CreateFeatures; private readonly createFeatures: CreateFeatures;
@inject(WebSocketConnectionSource) @inject(WebSocketConnectionProvider)
private readonly connectionSource: WebSocketConnectionSource; private readonly connectionProvider: WebSocketConnectionProvider;
private readonly offlineStatusDidChangeEmitter = new Emitter< private readonly offlineStatusDidChangeEmitter = new Emitter<
OfflineConnectionStatus | undefined OfflineConnectionStatus | undefined
@ -202,7 +202,7 @@ export class ApplicationConnectionStatusContribution extends TheiaApplicationCon
const params = <OfflineMessageParams>{ const params = <OfflineMessageParams>{
port: this.daemonPort.port, port: this.daemonPort.port,
online: this.isOnline.online, online: this.isOnline.online,
backendConnected: this.connectionSource['socket'].connected, // https://github.com/arduino/arduino-ide/issues/2081 backendConnected: this.connectionProvider['socket'].connected, // https://github.com/arduino/arduino-ide/issues/2081
}; };
this._offlineStatus = offlineConnectionStatusType(params); this._offlineStatus = offlineConnectionStatusType(params);
const { text, tooltip } = offlineMessage(params); const { text, tooltip } = offlineMessage(params);

View File

@ -1,5 +1,5 @@
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget'; import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import type { SidebarMenuItem } from '@theia/core/lib/browser/shell/sidebar-menu-widget'; import type { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
import type { MenuPath } from '@theia/core/lib/common/menu'; import type { MenuPath } from '@theia/core/lib/common/menu';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { import {
@ -46,45 +46,46 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
this.contextMenuRenderer.render(options); this.contextMenuRenderer.render(options);
} }
override renderItem(item: SidebarMenuItem): React.ReactNode { protected override render(): React.ReactNode {
return (
<React.Fragment>
{this.menus.map((menu) => this.renderMenu(menu))}
</React.Fragment>
);
}
private renderMenu(menu: SidebarMenu): React.ReactNode {
// Removes the _Settings_ (cog) icon from the left sidebar // Removes the _Settings_ (cog) icon from the left sidebar
if (item.menu.id === 'settings-menu') { if (menu.id === 'settings-menu') {
return undefined; return undefined;
} }
const arduinoAccount = item.menu.id === accountMenu.id; const arduinoAccount = menu.id === accountMenu.id;
const arduinoAccountPicture = const picture =
arduinoAccount && arduinoAccount &&
this.connectionStatue.offlineStatus !== 'internet' && this.connectionStatue.offlineStatus !== 'internet' &&
this.createFeatures.session?.account.picture; this.createFeatures.session?.account.picture;
const className = typeof picture === 'string' ? undefined : menu.iconClass;
return ( return (
<div <i
key={item.menu.id} key={menu.id}
className="theia-sidebar-menu-item" className={className}
title={item.menu.title} title={menu.title}
onClick={(e) => this.onClick(e, item.menu.menuPath)} onClick={(e) => this.onClick(e, menu.menuPath)}
onMouseDown={this.onMouseDown} onMouseDown={this.onMouseDown}
onMouseEnter={(e) => this.onMouseEnter(e, item.menu.title)}
onMouseOut={this.onMouseOut} onMouseOut={this.onMouseOut}
> >
{arduinoAccountPicture ? ( {picture && (
<i> <div className="account-icon">
<img <img
className="arduino-account-picture" src={picture}
src={arduinoAccountPicture}
alt={nls.localize( alt={nls.localize(
'arduino/cloud/profilePicture', 'arduino/cloud/profilePicture',
'Profile picture' 'Profile picture'
)} )}
/> />
</i> </div>
) : (
<i className={item.menu.iconClass} />
)} )}
{item.badge && ( </i>
<div className="theia-badge-decorator-sidebar">{item.badge}</div>
)}
</div>
); );
} }
} }

View File

@ -1,7 +1,6 @@
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution'; import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options'; import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
import { import {
DebugAdapterPath, DebugAdapterPath,
@ -13,7 +12,6 @@ import { DebugSession } from './debug-session';
@injectable() @injectable()
export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory { export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory {
override get( override get(
manager: DebugSessionManager,
sessionId: string, sessionId: string,
options: DebugConfigurationSessionOptions, options: DebugConfigurationSessionOptions,
parentSession?: DebugSession parentSession?: DebugSession
@ -22,12 +20,12 @@ export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory
sessionId, sessionId,
() => () =>
new Promise<DebugChannel>((resolve) => new Promise<DebugChannel>((resolve) =>
this.connectionProvider.listen( this.connectionProvider.openChannel(
`${DebugAdapterPath}/${sessionId}`, `${DebugAdapterPath}/${sessionId}`,
(_, wsChannel) => { (wsChannel) => {
resolve(new ForwardingDebugChannel(wsChannel)); resolve(new ForwardingDebugChannel(wsChannel));
}, },
false { reconnecting: false }
) )
), ),
this.getTraceOutputChannel() this.getTraceOutputChannel()
@ -37,9 +35,6 @@ export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory
sessionId, sessionId,
options, options,
parentSession, parentSession,
this.testService,
options.testRun,
manager,
connection, connection,
this.terminalService, this.terminalService,
this.editorManager, this.editorManager,

View File

@ -0,0 +1,21 @@
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser/editor-command';
@injectable()
export class EditorCommandContribution extends TheiaEditorCommandContribution {
@postConstruct()
protected override init(): void {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722.
this.editorPreferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (preferenceName === 'files.autoSave') {
const autoSaveWasOnBeforeChange = !oldValue || oldValue !== 'off';
const autoSaveIsOnAfterChange = !newValue || newValue !== 'off';
if (!autoSaveWasOnBeforeChange && autoSaveIsOnAfterChange) {
this.shell.saveAll();
}
}
}
);
}
}

View File

@ -1,18 +1,19 @@
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { TextEditor } from '@theia/editor/lib/browser';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution'; import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
@injectable() @injectable()
export class EditorContribution extends TheiaEditorContribution { export class EditorContribution extends TheiaEditorContribution {
protected override updateLanguageStatus( protected override updateLanguageStatus(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
..._: Parameters<TheiaEditorContribution['updateLanguageStatus']> editor: TextEditor | undefined
): void { ): void {
// NOOP // NOOP
} }
protected override updateEncodingStatus( protected override updateEncodingStatus(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
..._: Parameters<TheiaEditorContribution['updateEncodingStatus']> editor: TextEditor | undefined
): void { ): void {
// https://github.com/arduino/arduino-ide/issues/1393 // https://github.com/arduino/arduino-ide/issues/1393
// NOOP // NOOP

View File

@ -36,7 +36,7 @@ export class FileResourceResolver extends TheiaFileResourceResolver {
); );
} }
return new WriteQueuedFileResource(uri, this.fileService, { return new WriteQueuedFileResource(uri, this.fileService, {
readOnly: stat?.isReadonly ?? false, isReadonly: stat?.isReadonly ?? false,
shouldOverwrite: () => this.shouldOverwrite(uri), shouldOverwrite: () => this.shouldOverwrite(uri),
shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error), shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error),
}); });

View File

@ -2,7 +2,6 @@ import React from '@theia/core/shared/react';
import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component'; import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { codicon } from '@theia/core/lib/browser'; import { codicon } from '@theia/core/lib/browser';
import { sanitize } from 'dompurify';
export class NotificationComponent extends TheiaNotificationComponent { export class NotificationComponent extends TheiaNotificationComponent {
override render(): React.ReactNode { override render(): React.ReactNode {
@ -21,7 +20,7 @@ export class NotificationComponent extends TheiaNotificationComponent {
/> />
<div className="theia-notification-message"> <div className="theia-notification-message">
<span <span
dangerouslySetInnerHTML={{ __html: sanitize(message) }} dangerouslySetInnerHTML={{ __html: message }}
onClick={this.onMessageClick} onClick={this.onMessageClick}
/> />
</div> </div>

View File

@ -117,7 +117,7 @@ export function maybeUpdateReadOnlyState(
const model = editor.document; const model = editor.document;
const oldReadOnly = model.readOnly; const oldReadOnly = model.readOnly;
const resource = model['resource']; const resource = model['resource'];
const newReadOnly = Boolean(resource.readOnly) || isReadOnly(resource.uri); const newReadOnly = Boolean(resource.isReadonly) || isReadOnly(resource.uri);
if (oldReadOnly !== newReadOnly) { if (oldReadOnly !== newReadOnly) {
editor.getControl().updateOptions({ readOnly: newReadOnly }); editor.getControl().updateOptions({ readOnly: newReadOnly });
if (newReadOnly) { if (newReadOnly) {

View File

@ -20,7 +20,7 @@ export class MonacoTextModelService extends TheiaMonacoTextModelService {
.getContributions() .getContributions()
.find(({ scheme }) => resource.uri.scheme === scheme); .find(({ scheme }) => resource.uri.scheme === scheme);
const readOnly = const readOnly =
Boolean(resource.readOnly) || Boolean(resource.isReadonly) ||
this.sketchesServiceClient.isReadOnly(resource.uri); this.sketchesServiceClient.isReadOnly(resource.uri);
return factory return factory
? factory.createModel(resource) ? factory.createModel(resource)

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeService } from '@theia/core/lib/browser/theming';
import { import {
Disposable, Disposable,

View File

@ -1,9 +1,11 @@
import type { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event'; import { Emitter, Event } from '@theia/core/lib/common/event';
import { injectable, interfaces } from '@theia/core/shared/inversify'; import { injectable, interfaces } from '@theia/core/shared/inversify';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import {
import type { PluginContributions } from '@theia/plugin-ext/lib/hosted/common/hosted-plugin'; PluginContributions,
import type { HostedPluginSupport } from '../../hosted/hosted-plugin-support'; HostedPluginSupport as TheiaHostedPluginSupport,
} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { HostedPluginSupport } from '../../hosted/hosted-plugin-support';
@injectable() @injectable()
export class HostedPluginSupportImpl export class HostedPluginSupportImpl

View File

@ -1,9 +0,0 @@
import { TestViewContribution as TheiaTestViewContribution } from '@theia/test/lib/browser/view/test-view-contribution';
import { injectable } from 'inversify';
@injectable()
export class TestViewContribution extends TheiaTestViewContribution {
override async initializeLayout(): Promise<void> {
// NOOP
}
}

View File

@ -4,7 +4,6 @@ import {
TabBarToolbarRegistry, TabBarToolbarRegistry,
TabBarToolbarItem, TabBarToolbarItem,
ReactTabBarToolbarItem, ReactTabBarToolbarItem,
RenderedToolbarItem,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandRegistry } from '@theia/core/lib/common/command'; import { CommandRegistry } from '@theia/core/lib/common/command';
import { ReactWidget } from '@theia/core/lib/browser'; import { ReactWidget } from '@theia/core/lib/browser';
@ -15,7 +14,7 @@ export const ARDUINO_TOOLBAR_ITEM_CLASS = 'arduino-tool-item';
export namespace ArduinoToolbarComponent { export namespace ArduinoToolbarComponent {
export interface Props { export interface Props {
side: 'left' | 'right'; side: 'left' | 'right';
items: TabBarToolbarItem[]; items: (TabBarToolbarItem | ReactTabBarToolbarItem)[];
commands: CommandRegistry; commands: CommandRegistry;
labelParser: LabelParser; labelParser: LabelParser;
commandIsEnabled: (id: string) => boolean; commandIsEnabled: (id: string) => boolean;
@ -35,7 +34,7 @@ export class ArduinoToolbarComponent extends React.Component<
this.state = { tooltip: '' }; this.state = { tooltip: '' };
} }
protected renderItem = (item: RenderedToolbarItem) => { protected renderItem = (item: TabBarToolbarItem) => {
let innerText = ''; let innerText = '';
let className = `arduino-tool-icon ${item.id}-icon`; let className = `arduino-tool-icon ${item.id}-icon`;
if (item.text) { if (item.text) {
@ -47,8 +46,7 @@ export class ArduinoToolbarComponent extends React.Component<
} }
} }
} }
const command = const command = this.props.commands.getCommand(item.command);
item.command && this.props.commands.getCommand(item.command);
const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${ const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${
TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM
} ${command && this.props.commandIsEnabled(command.id) ? 'enabled' : ''} ${ } ${command && this.props.commandIsEnabled(command.id) ? 'enabled' : ''} ${
@ -82,9 +80,7 @@ export class ArduinoToolbarComponent extends React.Component<
const items = [ const items = [
<React.Fragment key={this.props.side + '-arduino-toolbar-tooltip'}> <React.Fragment key={this.props.side + '-arduino-toolbar-tooltip'}>
{[...this.props.items].map((item) => {[...this.props.items].map((item) =>
ReactTabBarToolbarItem.is(item) TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render()
? item.render()
: this.renderItem(item)
)} )}
</React.Fragment>, </React.Fragment>,
]; ];
@ -98,7 +94,10 @@ export class ArduinoToolbarComponent extends React.Component<
} }
export class ArduinoToolbar extends ReactWidget { export class ArduinoToolbar extends ReactWidget {
protected items = new Map<string, TabBarToolbarItem>(); protected items = new Map<
string,
TabBarToolbarItem | ReactTabBarToolbarItem
>();
constructor( constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry, protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
@ -113,7 +112,9 @@ export class ArduinoToolbar extends ReactWidget {
this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar()); this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar());
} }
protected updateItems(items: Array<TabBarToolbarItem>): void { protected updateItems(
items: Array<TabBarToolbarItem | ReactTabBarToolbarItem>
): void {
this.items.clear(); this.items.clear();
const revItems = items const revItems = items
.sort(TabBarToolbarItem.PRIORITY_COMPARATOR) .sort(TabBarToolbarItem.PRIORITY_COMPARATOR)
@ -162,7 +163,7 @@ export class ArduinoToolbar extends ReactWidget {
protected executeCommand = (e: React.MouseEvent<HTMLElement>) => { protected executeCommand = (e: React.MouseEvent<HTMLElement>) => {
const item = this.items.get(e.currentTarget.id); const item = this.items.get(e.currentTarget.id);
if (item && item.command) { if (TabBarToolbarItem.is(item)) {
this.commands.executeCommand(item.command, this, e.target); this.commands.executeCommand(item.command, this, e.target);
} }
}; };

View File

@ -2,7 +2,7 @@ import React from '@theia/core/shared/react';
import type { ArduinoComponent } from '../../../common/protocol/arduino-component'; import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable'; import { Installable } from '../../../common/protocol/installable';
import type { ListItemRenderer } from './list-item-renderer'; import type { ListItemRenderer } from './list-item-renderer';
import { UserAbortError } from '../../../common/protocol/progressible'; import { UserAbortError } from './list-widget';
export class ComponentListItem< export class ComponentListItem<
T extends ArduinoComponent T extends ArduinoComponent

View File

@ -5,10 +5,7 @@ import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable'; import { Searchable } from '../../../common/protocol/searchable';
import { import { ExecuteWithProgress } from '../../../common/protocol/progressible';
ExecuteWithProgress,
UserAbortError,
} from '../../../common/protocol/progressible';
import { import {
Installable, Installable,
libraryInstallFailed, libraryInstallFailed,
@ -16,7 +13,7 @@ import {
} from '../../../common/protocol/installable'; } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { SearchBar } from './search-bar'; import { SearchBar } from './search-bar';
import { ListWidget } from './list-widget'; import { ListWidget, UserAbortError } from './list-widget';
import { ComponentList } from './component-list'; import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer'; import { ListItemRenderer } from './list-item-renderer';
import { import {

View File

@ -1,4 +1,4 @@
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
OpenerOptions, OpenerOptions,
OpenHandler, OpenHandler,

View File

@ -192,3 +192,10 @@ export namespace ListWidget {
readonly defaultSearchOptions: S; readonly defaultSearchOptions: S;
} }
} }
export class UserAbortError extends Error {
constructor(message = 'User abort') {
super(message);
Object.setPrototypeOf(this, UserAbortError.prototype);
}
}

View File

@ -127,9 +127,6 @@ export class SketchbookTreeModel extends FileTreeModel {
if (preferenceName === 'arduino.sketchbook.showAllFiles') { if (preferenceName === 'arduino.sketchbook.showAllFiles') {
this.updateRoot(); this.updateRoot();
} }
if (preferenceName === 'arduino.cloud.sharedSpaceID') {
this.updateRoot();
}
}) })
); );

View File

@ -3,7 +3,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { MainMenuManager } from '../../../common/main-menu-manager'; import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoPreferences } from '../../arduino-preferences'; import { ArduinoPreferences } from '../../arduino-preferences';
import { SketchbookWidget } from './sketchbook-widget'; import { SketchbookWidget } from './sketchbook-widget';

View File

@ -39,5 +39,3 @@ export const noSketchOpened = nls.localize(
'arduino/common/noSketchOpened', 'arduino/common/noSketchOpened',
'No sketch opened' 'No sketch opened'
); );
export const userAbort = nls.localize('arduino/common/userAbort', 'User abort');

View File

@ -5,14 +5,14 @@ export interface ArduinoDaemon {
* Returns with a promise that resolves with the port * Returns with a promise that resolves with the port
* of the CLI daemon when it's up and running. * of the CLI daemon when it's up and running.
*/ */
getPort(): Promise<number>; getPort(): Promise<string>;
/** /**
* Unlike `getPort` this method returns with a promise * Unlike `getPort` this method returns with a promise
* that resolves to `undefined` when the daemon is not running. * that resolves to `undefined` when the daemon is not running.
* Otherwise resolves to the CLI daemon port. * Otherwise resolves to the CLI daemon port.
*/ */
tryGetPort(): Promise<number | undefined>; tryGetPort(): Promise<string | undefined>;
start(): Promise<number>; start(): Promise<string>;
stop(): Promise<void>; stop(): Promise<void>;
restart(): Promise<number>; restart(): Promise<string>;
} }

View File

@ -1,5 +1,4 @@
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { FQBN } from 'fqbn';
import type { MaybePromise } from '@theia/core/lib/common/types'; import type { MaybePromise } from '@theia/core/lib/common/types';
import type URI from '@theia/core/lib/common/uri'; import type URI from '@theia/core/lib/common/uri';
import { import {
@ -67,10 +66,7 @@ export interface BoardsService
skipPostInstall?: boolean; skipPostInstall?: boolean;
}): Promise<void>; }): Promise<void>;
getDetectedPorts(): Promise<DetectedPorts>; getDetectedPorts(): Promise<DetectedPorts>;
getBoardDetails(options: { getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined>;
fqbn: string;
forceRefresh?: boolean;
}): Promise<BoardDetails | undefined>;
getBoardPackage(options: { getBoardPackage(options: {
id: string /* TODO: change to PlatformIdentifier type? */; id: string /* TODO: change to PlatformIdentifier type? */;
}): Promise<BoardsPackage | undefined>; }): Promise<BoardsPackage | undefined>;
@ -79,9 +75,6 @@ export interface BoardsService
}): Promise<BoardsPackage | undefined>; }): Promise<BoardsPackage | undefined>;
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>; searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
getInstalledBoards(): Promise<BoardWithPackage[]>; getInstalledBoards(): Promise<BoardWithPackage[]>;
/**
* Returns with all installed platforms including the manually installed ones.
*/
getInstalledPlatforms(): Promise<BoardsPackage[]>; getInstalledPlatforms(): Promise<BoardsPackage[]>;
getBoardUserFields(options: { getBoardUserFields(options: {
fqbn: string; fqbn: string;
@ -102,7 +95,7 @@ export interface CheckDebugEnabledParams {
* The FQBN might contain custom board config options. For example, `arduino:esp32:nano_nora:USBMode=hwcdc,option2=value2`. * The FQBN might contain custom board config options. For example, `arduino:esp32:nano_nora:USBMode=hwcdc,option2=value2`.
*/ */
readonly fqbn: string; readonly fqbn: string;
readonly programmer?: string; readonly programmer: string;
} }
export interface BoardSearch extends Searchable.Options { export interface BoardSearch extends Searchable.Options {
@ -371,6 +364,40 @@ export interface ConfigOption {
readonly values: ConfigValue[]; readonly values: ConfigValue[];
} }
export namespace ConfigOption { export namespace ConfigOption {
/**
* Appends the configuration options to the `fqbn` argument.
* Throws an error if the `fqbn` does not have the `segment(':'segment)*` format.
* The provided output format is always segment(':'segment)*(':'option'='value(','option'='value)*)?
*/
export function decorate(
fqbn: string,
configOptions: ConfigOption[]
): string {
if (!configOptions.length) {
return fqbn;
}
const toValue = (values: ConfigValue[]) => {
const selectedValue = values.find(({ selected }) => selected);
if (!selectedValue) {
console.warn(
`None of the config values was selected. Values were: ${JSON.stringify(
values
)}`
);
return undefined;
}
return selectedValue.value;
};
const options = configOptions
.map(({ option, values }) => [option, toValue(values)])
.filter(([, value]) => !!value)
.map(([option, value]) => `${option}=${value}`)
.join(',');
return `${fqbn}:${options}`;
}
export class ConfigOptionError extends Error { export class ConfigOptionError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
@ -544,13 +571,28 @@ export namespace Board {
} }
} }
/**
* Throws an error if the `fqbn` argument is not sanitized. A sanitized FQBN has the `VENDOR:ARCHITECTURE:BOARD_ID` construct.
*/
export function assertSanitizedFqbn(fqbn: string): void {
if (fqbn.split(':').length !== 3) {
throw new Error(
`Expected a sanitized FQBN with three segments in the following format: 'VENDOR:ARCHITECTURE:BOARD_ID'. Got ${fqbn} instead.`
);
}
}
/** /**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to * Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format. * `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). * See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
*/ */
export function sanitizeFqbn(fqbn: string): string { export function sanitizeFqbn(fqbn: string | undefined): string | undefined {
return new FQBN(fqbn).sanitize().toString(); if (!fqbn) {
return undefined;
}
const [vendor, arch, id] = fqbn.split(':');
return `${vendor}:${arch}:${id}`;
} }
export type PlatformIdentifier = Readonly<{ vendorId: string; arch: string }>; export type PlatformIdentifier = Readonly<{ vendorId: string; arch: string }>;
@ -707,8 +749,8 @@ export function boardIdentifierEquals(
return false; // TODO: This a strict now. Maybe compare name in the future. return false; // TODO: This a strict now. Maybe compare name in the future.
} }
if (left.fqbn && right.fqbn) { if (left.fqbn && right.fqbn) {
const leftFqbn = new FQBN(left.fqbn).toString(options.looseFqbn); const leftFqbn = options.looseFqbn ? sanitizeFqbn(left.fqbn) : left.fqbn;
const rightFqbn = new FQBN(right.fqbn).toString(options.looseFqbn); const rightFqbn = options.looseFqbn ? sanitizeFqbn(right.fqbn) : right.fqbn;
return leftFqbn === rightFqbn; return leftFqbn === rightFqbn;
} }
// No more Genuino hack. // No more Genuino hack.

View File

@ -1,5 +1,4 @@
import { ApplicationError } from '@theia/core/lib/common/application-error'; import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import type { import type {
Location, Location,
@ -8,7 +7,7 @@ import type {
} from '@theia/core/shared/vscode-languageserver-protocol'; } from '@theia/core/shared/vscode-languageserver-protocol';
import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api'; import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api';
import type { BoardUserField, Installable } from '../../common/protocol/'; import type { BoardUserField, Installable } from '../../common/protocol/';
import { PortIdentifier, Programmer, isPortIdentifier } from './boards-service'; import { isPortIdentifier, PortIdentifier, Programmer } from './boards-service';
import type { IndexUpdateSummary } from './notification-service'; import type { IndexUpdateSummary } from './notification-service';
import type { Sketch } from './sketches-service'; import type { Sketch } from './sketches-service';
@ -71,7 +70,6 @@ export namespace CoreError {
Upload: 4002, Upload: 4002,
UploadUsingProgrammer: 4003, UploadUsingProgrammer: 4003,
BurnBootloader: 4004, BurnBootloader: 4004,
UploadRequiresProgrammer: 4005,
}; };
export const VerifyFailed = declareCoreError(Codes.Verify); export const VerifyFailed = declareCoreError(Codes.Verify);
export const UploadFailed = declareCoreError(Codes.Upload); export const UploadFailed = declareCoreError(Codes.Upload);
@ -79,10 +77,6 @@ export namespace CoreError {
Codes.UploadUsingProgrammer Codes.UploadUsingProgrammer
); );
export const BurnBootloaderFailed = declareCoreError(Codes.BurnBootloader); export const BurnBootloaderFailed = declareCoreError(Codes.BurnBootloader);
export const UploadRequiresProgrammer = declareCoreError(
Codes.UploadRequiresProgrammer
);
export function is( export function is(
error: unknown error: unknown
): error is ApplicationError<number, ErrorLocation[]> { ): error is ApplicationError<number, ErrorLocation[]> {
@ -168,18 +162,9 @@ export function isUploadResponse(arg: unknown): arg is UploadResponse {
export const CoreServicePath = '/services/core-service'; export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService'); export const CoreService = Symbol('CoreService');
export interface CoreService { export interface CoreService {
compile( compile(options: CoreService.Options.Compile): Promise<void>;
options: CoreService.Options.Compile, upload(options: CoreService.Options.Upload): Promise<UploadResponse>;
cancellationToken?: CancellationToken burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
): Promise<CompileSummary | undefined>;
upload(
options: CoreService.Options.Upload,
cancellationToken?: CancellationToken
): Promise<UploadResponse>;
burnBootloader(
options: CoreService.Options.Bootloader,
cancellationToken?: CancellationToken
): Promise<void>;
/** /**
* Refreshes the underling core gRPC client for the Arduino CLI. * Refreshes the underling core gRPC client for the Arduino CLI.
*/ */

View File

@ -71,4 +71,3 @@ export interface IDEUpdaterClient {
} }
export const SKIP_IDE_VERSION = 'skipIDEVersion'; export const SKIP_IDE_VERSION = 'skipIDEVersion';
export const LAST_USED_IDE_VERSION = 'lastUsedIDEVersion';

View File

@ -51,7 +51,7 @@ export interface NotificationServiceClient {
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void; notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void;
// Daemon // Daemon
notifyDaemonDidStart(port: number): void; notifyDaemonDidStart(port: string): void;
notifyDaemonDidStop(): void; notifyDaemonDidStop(): void;
// CLI config // CLI config

View File

@ -1,48 +1,22 @@
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { CancellationToken } from '@theia/core/lib/common/cancellation'; import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import type { MessageService } from '@theia/core/lib/common/message-service'; import type { MessageService } from '@theia/core/lib/common/message-service';
import type { Progress } from '@theia/core/lib/common/message-service-protocol'; import type { Progress } from '@theia/core/lib/common/message-service-protocol';
import { userAbort } from '../nls';
import type { ResponseServiceClient } from './response-service'; import type { ResponseServiceClient } from './response-service';
export const UserAbortApplicationError = ApplicationError.declare(
9999,
(message: string, uri: string) => {
return {
message,
data: { uri },
};
}
);
export class UserAbortError extends Error {
constructor() {
super(userAbort);
Object.setPrototypeOf(this, UserAbortError.prototype);
}
}
export namespace ExecuteWithProgress { export namespace ExecuteWithProgress {
export async function doWithProgress<T>(options: { export async function doWithProgress<T>(options: {
run: ({ run: ({ progressId }: { progressId: string }) => Promise<T>;
progressId,
cancellationToken,
}: {
progressId: string;
cancellationToken?: CancellationToken;
}) => Promise<T>;
messageService: MessageService; messageService: MessageService;
responseService: ResponseServiceClient; responseService: ResponseServiceClient;
progressText: string; progressText: string;
keepOutput?: boolean; keepOutput?: boolean;
cancelable?: boolean;
}): Promise<T> { }): Promise<T> {
return withProgress( return withProgress(
options.progressText, options.progressText,
options.messageService, options.messageService,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async (progress, token) => { async (progress, _token) => {
const progressId = progress.id; const progressId = progress.id;
const toDispose = options.responseService.onProgressDidChange( const toDispose = options.responseService.onProgressDidChange(
(progressMessage) => { (progressMessage) => {
@ -56,29 +30,24 @@ export namespace ExecuteWithProgress {
if (!options.keepOutput) { if (!options.keepOutput) {
options.responseService.clearOutput(); options.responseService.clearOutput();
} }
const result = await options.run({ const result = await options.run({ progressId });
progressId,
cancellationToken: token,
});
return result; return result;
} finally { } finally {
toDispose.dispose(); toDispose.dispose();
} }
}, }
options.cancelable
); );
} }
export async function withProgress<T>( export async function withProgress<T>(
text: string, text: string,
messageService: MessageService, messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<T>, cb: (progress: Progress, token: CancellationToken) => Promise<T>
cancelable = false
): Promise<T> { ): Promise<T> {
const cancellationSource = new CancellationTokenSource(); const cancellationSource = new CancellationTokenSource();
const { token } = cancellationSource; const { token } = cancellationSource;
const progress = await messageService.showProgress( const progress = await messageService.showProgress(
{ text, options: { cancelable } }, { text, options: { cancelable: false } },
() => cancellationSource.cancel() () => cancellationSource.cancel()
); );
try { try {

View File

@ -141,14 +141,13 @@ export interface SketchesService {
/** /**
* This is the JS/TS re-implementation of [`GenBuildPath`](https://github.com/arduino/arduino-cli/blob/c0d4e4407d80aabad81142693513b3306759cfa6/arduino/sketch/sketch.go#L296-L306) of the CLI. * This is the JS/TS re-implementation of [`GenBuildPath`](https://github.com/arduino/arduino-cli/blob/c0d4e4407d80aabad81142693513b3306759cfa6/arduino/sketch/sketch.go#L296-L306) of the CLI.
* Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. Can be multiple ones. This method does not check the existence of the sketch. * Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. Can be multiple ones. This method does not check the existence of the sketch.
* Since CLI v1.1.0 the default sketch folder is the os user cache dir. See https://github.com/arduino/arduino-cli/pull/2673/commits/d2ffeb06ca6360a211d5aa7ddd11505212ffb1b9
* *
* The case sensitivity of the drive letter on Windows matters when the CLI calculates the MD5 hash of the temporary build folder. * The case sensitivity of the drive letter on Windows matters when the CLI calculates the MD5 hash of the temporary build folder.
* IDE2 does not know and does not want to rely on how the CLI treats the paths: with lowercase or uppercase drive letters. * IDE2 does not know and does not want to rely on how the CLI treats the paths: with lowercase or uppercase drive letters.
* Hence, IDE2 has to provide multiple build paths on Windows. This hack will be obsolete when the CLI can provide error codes: * Hence, IDE2 has to provide multiple build paths on Windows. This hack will be obsolete when the CLI can provide error codes:
* https://github.com/arduino/arduino-cli/issues/1762. * https://github.com/arduino/arduino-cli/issues/1762.
*/ */
getBuildPath(sketch: SketchRef): Promise<string[]>; tempBuildPath(sketch: SketchRef): Promise<string[]>;
} }
export interface SketchRef { export interface SketchRef {

View File

@ -0,0 +1,7 @@
export const SurveyNotificationServicePath =
'/services/survey-notification-service';
export const SurveyNotificationService = Symbol('SurveyNotificationService');
export interface SurveyNotificationService {
isFirstInstance(): Promise<boolean>;
}

View File

@ -38,26 +38,3 @@ export function uint8ArrayToString(uint8Array: Uint8Array): string {
export function stringToUint8Array(text: string): Uint8Array { export function stringToUint8Array(text: string): Uint8Array {
return Uint8Array.from(text, (char) => char.charCodeAt(0)); return Uint8Array.from(text, (char) => char.charCodeAt(0));
} }
export function poolWhile(
whileCondition: () => boolean,
intervalMs: number,
timeoutMs: number
): Promise<void> {
return new Promise((resolve, reject) => {
if (!whileCondition) {
resolve();
}
const start = Date.now();
const interval = setInterval(() => {
if (!whileCondition()) {
clearInterval(interval);
resolve();
} else if (Date.now() - start > timeoutMs) {
clearInterval(interval);
reject(new Error('Timed out while polling.'));
}
}, intervalMs);
});
}

View File

@ -38,33 +38,33 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
this.preferencesService.onPreferenceChanged( this.preferencesService.onPreferenceChanged(
debounce((e) => { debounce((e) => {
if (e.preferenceName === 'window.menuBarVisibility') { if (e.preferenceName === 'window.menuBarVisibility') {
this.doSetMenuBar(); this.setMenuBar();
} }
if (this.menu) { if (this._menu) {
for (const cmd of this.toggledCommands) { for (const cmd of this._toggledCommands) {
const menuItem = this.findMenuById(this.menu, cmd); const menuItem = this.findMenuById(this._menu, cmd);
if (menuItem) { if (menuItem) {
menuItem.checked = this.commandRegistry.isToggled(cmd); menuItem.checked = this.commandRegistry.isToggled(cmd);
} }
} }
window.electronArduino.setMenu(this.menu); // calls the IDE2-specific implementation window.electronArduino.setMenu(this._menu); // calls the IDE2-specific implementation
} }
}, 10) }, 10)
); );
this.keybindingRegistry.onKeybindingsChanged(() => { this.keybindingRegistry.onKeybindingsChanged(() => {
this.doSetMenuBar(); this.setMenuBar();
}); });
// #endregion Theia `postConstruct` // #endregion Theia `postConstruct`
this.appStateService.reachedState('ready').then(() => { this.appStateService.reachedState('ready').then(() => {
this.appReady = true; this.appReady = true;
if (this.updateWhenReady) { if (this.updateWhenReady) {
this.doSetMenuBar(); this.setMenuBar();
} }
}); });
} }
override createElectronMenuBar(): MenuDto[] { override createElectronMenuBar(): MenuDto[] {
this.toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977 this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977
const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR);
const menu = this.fillMenuTemplate([], menuModel, [], { const menu = this.fillMenuTemplate([], menuModel, [], {
rootMenuPath: MAIN_MENU_BAR, rootMenuPath: MAIN_MENU_BAR,
@ -73,11 +73,11 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
menu.unshift(this.createOSXMenu()); menu.unshift(this.createOSXMenu());
} }
const escapedMenu = this.escapeAmpersand(menu); const escapedMenu = this.escapeAmpersand(menu);
this.menu = escapedMenu; this._menu = escapedMenu;
return escapedMenu; return escapedMenu;
} }
override async doSetMenuBar(): Promise<void> { override async setMenuBar(): Promise<void> {
// Avoid updating menu items when the app is not ready. // Avoid updating menu items when the app is not ready.
// Getting the current electron window is not free and synchronous. // Getting the current electron window is not free and synchronous.
// Here, we defer all menu update requests, and fire one when the app is ready. // Here, we defer all menu update requests, and fire one when the app is ready.
@ -124,17 +124,17 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
...args ...args
); );
if ( if (
this.menu && this._menu &&
this.menuCommandExecutor.isVisible(menuPath, commandId, ...args) this.menuCommandExecutor.isVisible(menuPath, commandId, ...args)
) { ) {
const item = this.findMenuById(this.menu, commandId); const item = this.findMenuById(this._menu, commandId);
if (item) { if (item) {
item.checked = this.menuCommandExecutor.isToggled( item.checked = this.menuCommandExecutor.isToggled(
menuPath, menuPath,
commandId, commandId,
...args ...args
); );
window.electronArduino.setMenu(this.menu); // overridden to call the IDE2-specific implementation. window.electronArduino.setMenu(this._menu); // overridden to call the IDE2-specific implementation.
} }
} }
} }
@ -342,7 +342,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
parentItems.push(menuItem); parentItems.push(menuItem);
if (this.commandRegistry.getToggledHandler(commandId, ...args)) { if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
this.toggledCommands.add(commandId); this._toggledCommands.add(commandId);
} }
} }
return parentItems; return parentItems;

View File

@ -7,7 +7,6 @@ import {
hasStartupTasks, hasStartupTasks,
StartupTasks, StartupTasks,
} from '../../../electron-common/startup-task'; } from '../../../electron-common/startup-task';
import { WindowReloadOptions } from '@theia/core/lib/browser/window/window-service';
@injectable() @injectable()
export class ElectronWindowService export class ElectronWindowService
@ -18,12 +17,8 @@ export class ElectronWindowService
@postConstruct() @postConstruct()
protected override init(): void { protected override init(): void {
// Overridden to avoid calling the zoom level listener in super. // NOOP
// IDE2 listens on the zoom level changes in `ArduinoFrontendContribution#onStart` // IDE2 listens on the zoom level changes in `ArduinoFrontendContribution#onStart`
window.electronTheiaCore.onAboutToClose(() => {
this.connectionCloseService.markForClose(this.frontendIdProvider.getId());
});
} }
async isFirstWindow(): Promise<boolean> { async isFirstWindow(): Promise<boolean> {
@ -43,11 +38,11 @@ export class ElectronWindowService
} }
// Overridden to support optional task owner params and make `tsc` happy. // Overridden to support optional task owner params and make `tsc` happy.
override reload(options?: StartupTasks | WindowReloadOptions): void { override reload(options?: StartupTasks): void {
if (hasStartupTasks(options)) { if (hasStartupTasks(options)) {
window.electronArduino.requestReload(options); window.electronArduino.requestReload(options);
} else { } else {
super.reload(options); window.electronTheiaCore.requestReload();
} }
} }

View File

@ -1,10 +1,10 @@
import { ElectronConnectionHandler } from '@theia/core/lib/electron-main/messaging/electron-connection-handler'; import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service'; import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { TheiaMainApi } from '@theia/core/lib/electron-main/electron-api-main'; import { TheiaMainApi } from '@theia/core/lib/electron-main/electron-api-main';
import { import {
ElectronMainApplicationContribution,
ElectronMainApplication as TheiaElectronMainApplication, ElectronMainApplication as TheiaElectronMainApplication,
ElectronMainApplicationContribution,
} from '@theia/core/lib/electron-main/electron-main-application'; } from '@theia/core/lib/electron-main/electron-main-application';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window'; import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { ContainerModule } from '@theia/core/shared/inversify'; import { ContainerModule } from '@theia/core/shared/inversify';
@ -36,12 +36,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronConnectionHandler) bind(ElectronConnectionHandler)
.toDynamicValue( .toDynamicValue(
(context) => (context) =>
new RpcConnectionHandler<IDEUpdaterClient>(IDEUpdaterPath, (client) => { new JsonRpcConnectionHandler<IDEUpdaterClient>(
const server = context.container.get<IDEUpdater>(IDEUpdater); IDEUpdaterPath,
server.setClient(client); (client) => {
client.onDidCloseConnection(() => server.disconnectClient(client)); const server = context.container.get<IDEUpdater>(IDEUpdater);
return server; server.setClient(client);
}) client.onDidCloseConnection(() => server.disconnectClient(client));
return server;
}
)
) )
.inSingletonScope(); .inSingletonScope();

View File

@ -11,16 +11,16 @@ import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import { isObject, MaybePromise, Mutable } from '@theia/core/lib/common/types'; import { isObject, MaybePromise, Mutable } from '@theia/core/lib/common/types';
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
import { import {
ElectronMainCommandOptions, ElectronMainExecutionParams,
ElectronMainApplication as TheiaElectronMainApplication, ElectronMainApplication as TheiaElectronMainApplication,
} from '@theia/core/lib/electron-main/electron-main-application'; } from '@theia/core/lib/electron-main/electron-main-application';
import type { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window'; import type { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { URI } from '@theia/core/shared/vscode-uri'; import { URI } from '@theia/core/shared/vscode-uri';
import { log as logToFile, setup as setupFileLog } from 'node-log-rotate'; import { log as logToFile, setup as setupFileLog } from 'node-log-rotate';
@ -28,9 +28,7 @@ import { fork } from 'node:child_process';
import { promises as fs, readFileSync, rm, rmSync } from 'node:fs'; import { promises as fs, readFileSync, rm, rmSync } from 'node:fs';
import type { AddressInfo } from 'node:net'; import type { AddressInfo } from 'node:net';
import { isAbsolute, join, resolve } from 'node:path'; import { isAbsolute, join, resolve } from 'node:path';
import type { Argv } from 'yargs';
import { Sketch } from '../../common/protocol'; import { Sketch } from '../../common/protocol';
import { poolWhile } from '../../common/utils';
import { import {
AppInfo, AppInfo,
appInfoPropertyLiterals, appInfoPropertyLiterals,
@ -131,11 +129,6 @@ const APP_STARTED_WITH_CONTENT_TRACE =
typeof process !== 'undefined' && typeof process !== 'undefined' &&
process.argv.indexOf('--content-trace') !== -1; process.argv.indexOf('--content-trace') !== -1;
const createYargs: (
argv?: string[],
cwd?: string
) => Argv = require('yargs/yargs');
@injectable() @injectable()
export class ElectronMainApplication extends TheiaElectronMainApplication { export class ElectronMainApplication extends TheiaElectronMainApplication {
@inject(IsTempSketch) @inject(IsTempSketch)
@ -178,59 +171,29 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
private readonly scheduledDeletions: Disposable[] = []; private readonly scheduledDeletions: Disposable[] = [];
override async start(config: FrontendApplicationConfig): Promise<void> { override async start(config: FrontendApplicationConfig): Promise<void> {
createYargs(this.argv, process.cwd()) // Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
.command( // See: https://github.com/electron-userland/electron-builder/issues/2468
'$0 [file]', // Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
false, console.log(`${config.applicationName} ${app.getVersion()}`);
(cmd) => app.on('ready', () => app.setName(config.applicationName));
cmd const cwd = process.cwd();
.option('electronUserData', { this.attachFileAssociations(cwd);
type: 'string', this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
describe: this._config = await updateFrontendApplicationConfigFromPackageJson(config);
'The area where the electron main process puts its data', this._appInfo = updateAppInfo(this._appInfo, this._config);
}) this.hookApplicationEvents();
.positional('file', { type: 'string' }), const [port] = await Promise.all([this.startBackend(), app.whenReady()]);
async (args) => { this.startContentTracing();
if (args.electronUserData) { this._backendPort.resolve(port);
console.info( await Promise.all([
`using electron user data area : '${args.electronUserData}'` this.attachElectronSecurityToken(port),
); this.startContributions(),
await fs.mkdir(args.electronUserData, { recursive: true }); ]);
app.setPath('userData', args.electronUserData); return this.launch({
} secondInstance: false,
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit") argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
// See: https://github.com/electron-userland/electron-builder/issues/2468 cwd,
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701 });
console.log(`${config.applicationName} ${app.getVersion()}`);
app.on('ready', () => app.setName(config.applicationName));
const cwd = process.cwd();
this.attachFileAssociations(cwd);
this.useNativeWindowFrame =
this.getTitleBarStyle(config) === 'native';
this._config = await updateFrontendApplicationConfigFromPackageJson(
config
);
this._appInfo = updateAppInfo(this._appInfo, this._config);
this.hookApplicationEvents();
this.showInitialWindow(undefined);
const [port] = await Promise.all([
this.startBackend(),
app.whenReady(),
]);
this.startContentTracing();
this._backendPort.resolve(port);
await Promise.all([
this.attachElectronSecurityToken(port),
this.startContributions(),
]);
this.handleMainCommand({
file: args.file,
cwd,
secondInstance: false,
});
}
)
.parse();
} }
private startContentTracing(): void { private startContentTracing(): void {
@ -293,16 +256,6 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
); );
if (sketchFolderPath) { if (sketchFolderPath) {
this.openFilePromise.reject(new InterruptWorkspaceRestoreError()); this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
// open-file event is triggered before the app is ready and initialWindow is created.
// Wait for initialWindow to be set before opening the sketch on the first instance.
// See https://github.com/arduino/arduino-ide/pull/2693
try {
await app.whenReady();
if (!this.firstWindowId) {
await poolWhile(() => !this.initialWindow, 100, 3000);
}
} catch {}
await this.openSketch(sketchFolderPath); await this.openSketch(sketchFolderPath);
} }
} }
@ -331,8 +284,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
} }
} }
protected override async handleMainCommand( protected override async launch(
options: ElectronMainCommandOptions params: ElectronMainExecutionParams
): Promise<void> { ): Promise<void> {
try { try {
// When running on MacOS, we either have to wait until // When running on MacOS, we either have to wait until
@ -347,7 +300,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
throw err; throw err;
} }
if (await this.launchFromArgs(options)) { if (await this.launchFromArgs(params)) {
// Application has received a file in its arguments and will skip the default application launch // Application has received a file in its arguments and will skip the default application launch
return; return;
} }
@ -361,10 +314,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
`Restoring workspace roots: ${workspaces.map(({ file }) => file)}` `Restoring workspace roots: ${workspaces.map(({ file }) => file)}`
); );
for (const workspace of workspaces) { for (const workspace of workspaces) {
const resolvedPath = await this.resolvePath( const resolvedPath = await this.resolvePath(workspace.file, params.cwd);
workspace.file,
options.cwd
);
if (!resolvedPath) { if (!resolvedPath) {
continue; continue;
} }
@ -387,20 +337,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
} }
this.startup = false; this.startup = false;
if (useDefault) { if (useDefault) {
super.handleMainCommand(options); super.launch(params);
} }
} }
private get argv(): string[] {
return this.processArgv.getProcessArgvWithoutBin(process.argv).slice();
}
private async launchFromArgs( private async launchFromArgs(
params: ElectronMainCommandOptions, params: ElectronMainExecutionParams
argv?: string[]
): Promise<boolean> { ): Promise<boolean> {
// Copy to prevent manipulation of original array // Copy to prevent manipulation of original array
const argCopy = [...(argv || this.argv)]; const argCopy = [...params.argv];
let path: string | undefined; let path: string | undefined;
for (const maybePath of argCopy) { for (const maybePath of argCopy) {
const resolvedPath = await this.resolvePath(maybePath, params.cwd); const resolvedPath = await this.resolvePath(maybePath, params.cwd);
@ -438,7 +383,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
} }
const [uri, electronWindow] = await Promise.all([ const [uri, electronWindow] = await Promise.all([
this.createWindowUri(), this.createWindowUri(),
this.reuseOrCreateWindow(options), this.createWindow(options),
]); ]);
electronWindow.loadURL(uri.withFragment(encodeURI(file)).toString(true)); electronWindow.loadURL(uri.withFragment(encodeURI(file)).toString(true));
return electronWindow; return electronWindow;
@ -538,7 +483,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
argv: string[], argv: string[],
cwd: string cwd: string
): Promise<void> { ): Promise<void> {
if (await this.launchFromArgs({ cwd, secondInstance: true }, argv)) { if (await this.launchFromArgs({ cwd, argv, secondInstance: true })) {
// Application has received a file in its arguments // Application has received a file in its arguments
return; return;
} }
@ -834,7 +779,7 @@ class InterruptWorkspaceRestoreError extends Error {
// but it's the `package.json` inside the `resources/app/` folder if it's the final bundled app. // but it's the `package.json` inside the `resources/app/` folder if it's the final bundled app.
// See https://github.com/arduino/arduino-ide/pull/2144#pullrequestreview-1556343430. // See https://github.com/arduino/arduino-ide/pull/2144#pullrequestreview-1556343430.
async function updateFrontendApplicationConfigFromPackageJson( async function updateFrontendApplicationConfigFromPackageJson(
config: Mutable<FrontendApplicationConfig> config: FrontendApplicationConfig
): Promise<FrontendApplicationConfig> { ): Promise<FrontendApplicationConfig> {
if (!isProductionMode) { if (!isProductionMode) {
console.debug( console.debug(
@ -901,11 +846,7 @@ const fallbackFrontendAppConfig: FrontendApplicationConfig = {
defaultIconTheme: 'none', defaultIconTheme: 'none',
validatePreferencesSchema: false, validatePreferencesSchema: false,
defaultLocale: '', defaultLocale: '',
electron: { electron: {},
showWindowEarly: true,
uriScheme: 'arduino-ide',
},
reloadOnReconnect: true,
}; };
// When the package.json must go from `./lib/backend/electron-main.js` to `./package.json` when the app is webpacked. // When the package.json must go from `./lib/backend/electron-main.js` to `./package.json` when the app is webpacked.

View File

@ -8,7 +8,7 @@ import { ElectronArduinoRenderer } from '../electron-arduino';
@injectable() @injectable()
export class TheiaElectronWindow extends DefaultTheiaElectronWindow { export class TheiaElectronWindow extends DefaultTheiaElectronWindow {
protected override reload(args?: unknown): void { protected override reload(args?: unknown): void {
this.handleStopRequest(async () => { this.handleStopRequest(() => {
this.applicationState = 'init'; this.applicationState = 'init';
if (hasStartupTasks(args)) { if (hasStartupTasks(args)) {
const { webContents } = this._window; const { webContents } = this._window;

View File

@ -1,53 +0,0 @@
import { credentials, makeClientConstructor } from '@grpc/grpc-js';
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
export interface CreateClientOptions {
/**
* The port to the Arduino CLI daemon.
*/
readonly port: number;
/**
* Defaults to `'localhost'`.
*/
readonly host?: string;
/**
* gRCP channel options. Defaults to `createDefaultChannelOptions` with `'0.0.0'` `appVersion`
*/
readonly channelOptions?: Record<string, unknown>;
}
export function createDefaultChannelOptions(
appVersion = '0.0.0'
): Record<string, unknown> {
return {
'grpc.max_send_message_length': 512 * 1024 * 1024,
'grpc.max_receive_message_length': 512 * 1024 * 1024,
'grpc.primary_user_agent': `arduino-ide/${appVersion}`,
};
}
export function createArduinoCoreServiceClient(
options: CreateClientOptions
): ArduinoCoreServiceClient {
const {
port,
host = 'localhost',
channelOptions = createDefaultChannelOptions(),
} = options;
const address = `${host}:${port}`;
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
const ArduinoCoreServiceClient = makeClientConstructor(
// @ts-expect-error: ignore
commandsGrpcPb['cc.arduino.cli.commands.v1.ArduinoCoreService'],
'ArduinoCoreServiceService'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any;
const client = new ArduinoCoreServiceClient(
address,
credentials.createInsecure(),
channelOptions
) as ArduinoCoreServiceClient;
return client;
}

View File

@ -1,7 +1,7 @@
import { join } from 'node:path'; import { join } from 'node:path';
import { inject, injectable, named } from '@theia/core/shared/inversify'; import { inject, injectable, named } from '@theia/core/shared/inversify';
import { spawn, ChildProcess } from 'node:child_process'; import { spawn, ChildProcess } from 'node:child_process';
import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileUri } from '@theia/core/lib/node/file-uri';
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { Deferred, retry } from '@theia/core/lib/common/promise-util'; import { Deferred, retry } from '@theia/core/lib/common/promise-util';
import { import {
@ -39,11 +39,11 @@ export class ArduinoDaemonImpl
private readonly processUtils: ProcessUtils; private readonly processUtils: ProcessUtils;
private readonly toDispose = new DisposableCollection(); private readonly toDispose = new DisposableCollection();
private readonly onDaemonStartedEmitter = new Emitter<number>(); private readonly onDaemonStartedEmitter = new Emitter<string>();
private readonly onDaemonStoppedEmitter = new Emitter<void>(); private readonly onDaemonStoppedEmitter = new Emitter<void>();
private _running = false; private _running = false;
private _port = new Deferred<number>(); private _port = new Deferred<string>();
// Backend application lifecycle. // Backend application lifecycle.
@ -53,18 +53,18 @@ export class ArduinoDaemonImpl
// Daemon API // Daemon API
async getPort(): Promise<number> { async getPort(): Promise<string> {
return this._port.promise; return this._port.promise;
} }
async tryGetPort(): Promise<number | undefined> { async tryGetPort(): Promise<string | undefined> {
if (this._running) { if (this._running) {
return this._port.promise; return this._port.promise;
} }
return undefined; return undefined;
} }
async start(): Promise<number> { async start(): Promise<string> {
try { try {
this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any. this.toDispose.dispose(); // This will `kill` the previously started daemon process, if any.
const cliPath = this.getExecPath(); const cliPath = this.getExecPath();
@ -101,13 +101,13 @@ export class ArduinoDaemonImpl
this.toDispose.dispose(); this.toDispose.dispose();
} }
async restart(): Promise<number> { async restart(): Promise<string> {
return this.start(); return this.start();
} }
// Backend only daemon API // Backend only daemon API
get onDaemonStarted(): Event<number> { get onDaemonStarted(): Event<string> {
return this.onDaemonStartedEmitter.event; return this.onDaemonStartedEmitter.event;
} }
@ -150,11 +150,11 @@ export class ArduinoDaemonImpl
protected async spawnDaemonProcess(): Promise<{ protected async spawnDaemonProcess(): Promise<{
daemon: ChildProcess; daemon: ChildProcess;
port: number; port: string;
}> { }> {
const args = await this.getSpawnArgs(); const args = await this.getSpawnArgs();
const cliPath = this.getExecPath(); const cliPath = this.getExecPath();
const ready = new Deferred<{ daemon: ChildProcess; port: number }>(); const ready = new Deferred<{ daemon: ChildProcess; port: string }>();
const options = { const options = {
env: { ...deepClone(process.env), NO_COLOR: String(true) }, env: { ...deepClone(process.env), NO_COLOR: String(true) },
}; };
@ -195,13 +195,7 @@ export class ArduinoDaemonImpl
if (port.length && address.length) { if (port.length && address.length) {
grpcServerIsReady = true; grpcServerIsReady = true;
const portNumber = Number.parseInt(port, 10); ready.resolve({ daemon, port });
if (Number.isNaN(portNumber)) {
ready.reject(
new Error(`Received a NaN port from the CLI: ${port}`)
);
}
ready.resolve({ daemon, port: portNumber });
} }
} }
}); });
@ -231,7 +225,7 @@ export class ArduinoDaemonImpl
return ready.promise; return ready.promise;
} }
private fireDaemonStarted(port: number): void { private fireDaemonStarted(port: string): void {
this._running = true; this._running = true;
this._port.resolve(port); this._port.resolve(port);
this.onDaemonStartedEmitter.fire(port); this.onDaemonStartedEmitter.fire(port);
@ -244,7 +238,7 @@ export class ArduinoDaemonImpl
} }
this._running = false; this._running = false;
this._port.reject(); // Reject all pending. this._port.reject(); // Reject all pending.
this._port = new Deferred<number>(); this._port = new Deferred<string>();
this.onDaemonStoppedEmitter.fire(); this.onDaemonStoppedEmitter.fire();
this.notificationService.notifyDaemonDidStop(); this.notificationService.notifyDaemonDidStop();
} }

View File

@ -105,22 +105,23 @@ import { ClangFormatter } from './clang-formatter';
import { FormatterPath } from '../common/protocol/formatter'; import { FormatterPath } from '../common/protocol/formatter';
import { HostedPluginLocalizationService } from './theia/plugin-ext/hosted-plugin-localization-service'; import { HostedPluginLocalizationService } from './theia/plugin-ext/hosted-plugin-localization-service';
import { HostedPluginLocalizationService as TheiaHostedPluginLocalizationService } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-localization-service'; import { HostedPluginLocalizationService as TheiaHostedPluginLocalizationService } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-localization-service';
import { SurveyNotificationServiceImpl } from './survey-service-impl';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { IsTempSketch } from './is-temp-sketch'; import { IsTempSketch } from './is-temp-sketch';
import { WebsocketEndpoint } from './theia/core/websocket-endpoint'; import { rebindNsfwFileSystemWatcher } from './theia/filesystem/nsfw-bindings';
import { MessagingContribution } from './theia/core/messaging-contribution';
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader'; import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
import { import { PluginDeployer } from '@theia/plugin-ext/lib/common/plugin-protocol';
PluginDeployer,
PluginScanner,
} from '@theia/plugin-ext/lib/common/plugin-protocol';
import { import {
LocalDirectoryPluginDeployerResolverWithFallback, LocalDirectoryPluginDeployerResolverWithFallback,
PluginDeployer_GH_12064, PluginDeployer_GH_12064,
} from './theia/plugin-ext/plugin-deployer'; } from './theia/plugin-ext/plugin-deployer';
import { SettingsReader } from './settings-reader'; import { SettingsReader } from './settings-reader';
import { VsCodePluginScanner } from './theia/plugin-ext-vscode/scanner-vscode';
import { rebindParcelFileSystemWatcher } from './theia/filesystem/parcel-bindings';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope(); bind(BackendApplication).toSelf().inSingletonScope();
@ -300,7 +301,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
) )
) )
.inSingletonScope(); .inSingletonScope();
rebindParcelFileSystemWatcher(rebind); rebindNsfwFileSystemWatcher(rebind);
// Output service per connection. // Output service per connection.
bind(ConnectionContainerModule).toConstantValue( bind(ConnectionContainerModule).toConstantValue(
@ -378,8 +379,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
HostedPluginLocalizationService HostedPluginLocalizationService
); );
// Survey notification bindings
// It's currently unused. https://github.com/arduino/arduino-ide/pull/1150
bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope();
bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
({ container }) =>
new JsonRpcConnectionHandler(SurveyNotificationServicePath, () =>
container.get<SurveyNotificationService>(SurveyNotificationService)
)
)
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope(); bind(IsTempSketch).toSelf().inSingletonScope();
rebind(MessagingService.Identifier).to(WebsocketEndpoint).inSingletonScope(); rebind(MessagingService.Identifier)
.to(MessagingContribution)
.inSingletonScope();
// Removed undesired contributions from VS Code extensions // Removed undesired contributions from VS Code extensions
// Such as the RTOS view from the `cortex-debug` extension // Such as the RTOS view from the `cortex-debug` extension
@ -394,11 +410,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope(); rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
bind(SettingsReader).toSelf().inSingletonScope(); bind(SettingsReader).toSelf().inSingletonScope();
// To read the enablement property of the viewsWelcome
// https://github.com/eclipse-theia/theia/issues/14309
bind(VsCodePluginScanner).toSelf().inSingletonScope();
rebind(PluginScanner).toService(VsCodePluginScanner);
}); });
function bindChildLogger(bind: interfaces.Bind, name: string): void { function bindChildLogger(bind: interfaces.Bind, name: string): void {

View File

@ -267,12 +267,24 @@ export class BoardDiscovery
const { port, boards } = detectedPort; const { port, boards } = detectedPort;
const key = Port.keyOf(port); const key = Port.keyOf(port);
if (eventType === EventType.Add) { if (eventType === EventType.Add) {
// Note that, the serial discovery might detect port details (such as addressLabel) in chunks. const alreadyDetectedPort = newState[key];
// For example, first, the Teensy 4.1 port is detected with the `[no_device] Triple Serial` address label, if (alreadyDetectedPort) {
// Then, when more refinements are available, the same port is detected with `/dev/cu.usbmodem127902301 Triple Serial` address label. console.warn(
// In such cases, an `add` event is received from the CLI, and the the detected port is overridden in the state. `Detected a new port that has been already discovered. The old value will be overridden. Old value: ${JSON.stringify(
alreadyDetectedPort
)}, new value: ${JSON.stringify(detectedPort)}`
);
}
newState[key] = { port, boards }; newState[key] = { port, boards };
} else if (eventType === EventType.Remove) { } else if (eventType === EventType.Remove) {
const alreadyDetectedPort = newState[key];
if (!alreadyDetectedPort) {
console.warn(
`Detected a port removal but it has not been discovered. This is most likely a bug! Detected port was: ${JSON.stringify(
detectedPort
)}`
);
}
delete newState[key]; delete newState[key];
} }
} }

View File

@ -1,9 +1,9 @@
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { notEmpty } from '@theia/core/lib/common/objects'; import { notEmpty } from '@theia/core/lib/common/objects';
import { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { import {
Board,
BoardDetails, BoardDetails,
BoardSearch, BoardSearch,
BoardUserField, BoardUserField,
@ -32,9 +32,11 @@ import {
BoardListAllResponse, BoardListAllResponse,
BoardSearchRequest, BoardSearchRequest,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb'; } from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import { PlatformSummary } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb'; import { Platform } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { import {
PlatformInstallRequest, PlatformInstallRequest,
PlatformListRequest,
PlatformListResponse,
PlatformSearchRequest, PlatformSearchRequest,
PlatformSearchResponse, PlatformSearchResponse,
PlatformUninstallRequest, PlatformUninstallRequest,
@ -73,11 +75,7 @@ export class BoardsServiceImpl
async getBoardDetails(options: { async getBoardDetails(options: {
fqbn: string; fqbn: string;
forceRefresh?: boolean;
}): Promise<BoardDetails | undefined> { }): Promise<BoardDetails | undefined> {
if (options.forceRefresh) {
await this.refresh();
}
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
const { fqbn } = options; const { fqbn } = options;
@ -180,7 +178,7 @@ export class BoardsServiceImpl
const req = new IsDebugSupportedRequest() const req = new IsDebugSupportedRequest()
.setInstance(instance) .setInstance(instance)
.setFqbn(fqbn) .setFqbn(fqbn)
.setProgrammer(programmer ?? ''); .setProgrammer(programmer);
try { try {
const debugFqbn = await new Promise<string>((resolve, reject) => const debugFqbn = await new Promise<string>((resolve, reject) =>
client.isDebugSupported(req, (err, resp) => { client.isDebugSupported(req, (err, resp) => {
@ -249,22 +247,24 @@ export class BoardsServiceImpl
async getInstalledPlatforms(): Promise<BoardsPackage[]> { async getInstalledPlatforms(): Promise<BoardsPackage[]> {
const { instance, client } = await this.coreClient; const { instance, client } = await this.coreClient;
const resp = await new Promise<PlatformSearchResponse>( return new Promise<BoardsPackage[]>((resolve, reject) => {
(resolve, reject) => { client.platformList(
client.platformSearch( new PlatformListRequest().setInstance(instance),
new PlatformSearchRequest() (err, response) => {
.setInstance(instance) if (err) {
.setManuallyInstalled(true), // include core manually installed to the sketchbook reject(err);
(err, resp) => (err ? reject(err) : resolve(resp)) return;
); }
} resolve(
); response
const searchOutput = resp.getSearchOutputList(); .getInstalledPlatformsList()
return searchOutput .map((platform, _, installedPlatforms) =>
.map((message) => message.toObject(false)) toBoardsPackage(platform, installedPlatforms)
.filter((summary) => summary.installedVersion) // only installed ones )
.map(createBoardsPackage) );
.filter(notEmpty); }
);
});
} }
private async handleListBoards( private async handleListBoards(
@ -287,28 +287,12 @@ export class BoardsServiceImpl
for (const board of resp.getBoardsList()) { for (const board of resp.getBoardsList()) {
const platform = board.getPlatform(); const platform = board.getPlatform();
if (platform) { if (platform) {
const metadata = platform.getMetadata(); const platformId = platform.getId();
if (!metadata) {
console.warn(
`Platform metadata is missing for platform: ${JSON.stringify(
platform.toObject(false)
)}. Skipping`
);
continue;
}
const platformId = metadata.getId();
const release = platform.getRelease();
if (!release) {
console.warn(
`Platform release is missing for platform: ${platformId}. Skipping`
);
continue;
}
const fqbn = board.getFqbn() || undefined; // prefer undefined over empty string const fqbn = board.getFqbn() || undefined; // prefer undefined over empty string
const parsedPlatformId = createPlatformIdentifier(platformId); const parsedPlatformId = createPlatformIdentifier(platformId);
if (!parsedPlatformId) { if (!parsedPlatformId) {
console.warn( console.warn(
`Could not create platform identifier from platform ID input: ${platformId}. Skipping` `Could not create platform identifier from platform ID input: ${platform.getId()}. Skipping`
); );
continue; continue;
} }
@ -335,8 +319,8 @@ export class BoardsServiceImpl
name: board.getName(), name: board.getName(),
fqbn: board.getFqbn(), fqbn: board.getFqbn(),
packageId: parsedPlatformId, packageId: parsedPlatformId,
packageName: release.getName(), packageName: platform.getName(),
manuallyInstalled: metadata.getManuallyInstalled(), manuallyInstalled: platform.getManuallyInstalled(),
}); });
} }
} }
@ -391,25 +375,89 @@ export class BoardsServiceImpl
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
// `core search` returns with all platform versions when the command is executed via gRPC or with `--format json` const installedPlatformsReq = new PlatformListRequest();
// The `--all` flag is applicable only when filtering for the human-readable (`--format text`) output of the CLI installedPlatformsReq.setInstance(instance);
const resp = await new Promise<PlatformSearchResponse>( const installedPlatformsResp = await new Promise<PlatformListResponse>(
(resolve, reject) => { (resolve, reject) => {
client.platformSearch( client.platformList(installedPlatformsReq, (err, resp) => {
new PlatformSearchRequest() !!err ? reject(err) : resolve(resp);
.setInstance(instance) });
.setSearchArgs(options.query ?? ''),
(err, resp) => (err ? reject(err) : resolve(resp))
);
} }
); );
const typeFilter = this.typePredicate(options); const installedPlatforms =
const searchOutput = resp.getSearchOutputList(); installedPlatformsResp.getInstalledPlatformsList();
const boardsPackages = searchOutput
.map((message) => message.toObject(false)) const req = new PlatformSearchRequest();
.map(createBoardsPackage) req.setSearchArgs(options.query || '');
.filter(notEmpty) req.setAllVersions(true);
.filter(typeFilter); req.setInstance(instance);
const resp = await new Promise<PlatformSearchResponse>(
(resolve, reject) => {
client.platformSearch(req, (err, resp) => {
!!err ? reject(err) : resolve(resp);
});
}
);
const packages = new Map<string, BoardsPackage>();
// We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order.
// Otherwise we lose the FQBN information.
const groupedById: Map<string, Platform[]> = new Map();
for (const platform of resp.getSearchOutputList()) {
const id = platform.getId();
const idGroup = groupedById.get(id);
if (idGroup) {
idGroup.push(platform);
} else {
groupedById.set(id, [platform]);
}
}
const installedAwareVersionComparator = (
left: Platform,
right: Platform
) => {
// XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string.
const leftInstalled = !!installedPlatforms.find(
(ip) =>
ip.getId() === left.getId() && ip.getInstalled() === left.getLatest()
);
const rightInstalled = !!installedPlatforms.find(
(ip) =>
ip.getId() === right.getId() &&
ip.getInstalled() === right.getLatest()
);
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
const invertedVersionComparator =
Installable.Version.COMPARATOR(left.getLatest(), right.getLatest()) *
-1;
// Higher version comes first.
return invertedVersionComparator;
};
for (const value of groupedById.values()) {
value.sort(installedAwareVersionComparator);
}
for (const value of groupedById.values()) {
for (const platform of value) {
const id = platform.getId();
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions.sort(Installable.Version.COMPARATOR).reverse();
} else {
packages.set(id, toBoardsPackage(platform, installedPlatforms));
}
}
}
const filter = this.typePredicate(options);
const boardsPackages = [...packages.values()].filter(filter);
return sortComponents(boardsPackages, boardsPackageSortGroup); return sortComponents(boardsPackages, boardsPackageSortGroup);
} }
@ -576,52 +624,36 @@ function boardsPackageSortGroup(boardsPackage: BoardsPackage): SortGroup {
return types.join('-') as SortGroup; return types.join('-') as SortGroup;
} }
function createBoardsPackage( function toBoardsPackage(
summary: PlatformSummary.AsObject platform: Platform,
): BoardsPackage | undefined { installedPlatforms: Platform[]
if (!isPlatformSummaryWithMetadata(summary)) { ): BoardsPackage {
return undefined; let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(
(ip) => ip.getId() === platform.getId()
);
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
} }
const versionReleaseMap = new Map(summary.releasesMap); return {
const actualRelease = id: platform.getId(),
versionReleaseMap.get(summary.installedVersion) ?? name: platform.getName(),
versionReleaseMap.get(summary.latestVersion); author: platform.getMaintainer(),
if (!actualRelease) { availableVersions: [platform.getLatest()],
return undefined; description: platform
} .getBoardsList()
const { name, typesList, boardsList, deprecated, compatible } = actualRelease; .map((b) => b.getName())
if (!compatible) { .join(', '),
return undefined; // never show incompatible platforms types: platform.getTypeList(),
} deprecated: platform.getDeprecated(),
const { id, website, maintainer } = summary.metadata;
const availableVersions = Array.from(versionReleaseMap.keys())
.sort(Installable.Version.COMPARATOR)
.reverse();
const boardsPackage: Mutable<BoardsPackage> = {
id,
name,
summary: nls.localize( summary: nls.localize(
'arduino/component/boardsIncluded', 'arduino/component/boardsIncluded',
'Boards included in this package:' 'Boards included in this package:'
), ),
description: boardsList.map(({ name }) => name).join(', '), installedVersion,
boards: boardsList, boards: platform
types: typesList, .getBoardsList()
moreInfoLink: website, .map((b) => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
author: maintainer, moreInfoLink: platform.getWebsite(),
deprecated,
availableVersions,
}; };
if (summary.installedVersion) {
boardsPackage.installedVersion = summary.installedVersion;
}
return boardsPackage;
}
type PlatformSummaryWithMetadata = PlatformSummary.AsObject &
Required<Pick<PlatformSummary.AsObject, 'metadata'>>;
function isPlatformSummaryWithMetadata(
summary: PlatformSummary.AsObject
): summary is PlatformSummaryWithMetadata {
return Boolean(summary.metadata);
} }

View File

@ -1,6 +1,6 @@
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileUri } from '@theia/core/lib/node/file-uri';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { constants, promises as fs } from 'node:fs'; import { constants, promises as fs } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';

View File

@ -1,6 +1,6 @@
import { notEmpty } from '@theia/core/lib/common/objects'; import { notEmpty } from '@theia/core/lib/common/objects';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileUri } from '@theia/core/lib/node/file-uri';
import { import {
Range, Range,
Position, Position,

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