diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a72b4845d..556c6ecf2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ on: jobs: build: + if: github.repository == 'arduino/arduino-ide' strategy: matrix: config: @@ -50,21 +51,26 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }} IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }} + IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }} run: | # See: https://www.electron.build/code-signing - if [ "${{ runner.OS }}" = "macOS" ]; then - export CSC_LINK="${{ runner.temp }}/signing_certificate.p12" - # APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from: - # https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate - echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK" - - export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}" - - elif [ "${{ runner.OS }}" = "Windows" ]; then - export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx" - echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK" - - export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}" + if [ $IS_FORK = true ]; then + echo "Skipping the app signing: building from a fork." + else + if [ "${{ runner.OS }}" = "macOS" ]; then + export CSC_LINK="${{ runner.temp }}/signing_certificate.p12" + # APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from: + # https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate + echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK" + + export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}" + + elif [ "${{ runner.OS }}" = "Windows" ]; then + export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx" + echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK" + + export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}" + fi fi yarn --cwd ./electron/packager/ @@ -120,7 +126,7 @@ jobs: publish: needs: changelog - if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + if: github.repository == 'arduino/arduino-ide' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) runs-on: ubuntu-latest steps: - name: Download [GitHub Actions] @@ -141,7 +147,7 @@ jobs: release: needs: changelog - if: startsWith(github.ref, 'refs/tags/') + if: github.repository == 'arduino/arduino-ide' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Download [GitHub Actions] diff --git a/.github/workflows/check-certificates.yml b/.github/workflows/check-certificates.yml index af4a4c535..a4e52cdf4 100644 --- a/.github/workflows/check-certificates.yml +++ b/.github/workflows/check-certificates.yml @@ -15,6 +15,11 @@ env: jobs: check-certificates: + # Only run when the workflow will have access to the certificate secrets. + if: > + (github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') || + (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide') + runs-on: ubuntu-latest strategy: diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 000000000..5eed13e24 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,136 @@ +# Development + +This page includes technical documentation for developers who want to build the IDE locally and contribute to the project. + +## Architecture overview + +The IDE consists of three major parts: + - the _Electron main_ process, + - the _backend_, and + - the _frontend_. + +The _Electron main_ process is responsible for: + - creating the application, + - managing the application lifecycle via listeners, and + - creating and managing the web pages for the app. + +In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly on main process. + +By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.[[1]] + +In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the IDE, only the _backend_ allows OS interaction. + +The _backend_ process is responsible for: + - providing access to the filesystem, + - communicating with the [Arduino CLI](https://github.com/arduino/arduino-cli) via gRPC, + - running your terminal, + - exposing additional RESTful APIs, + - performing the Git commands in the local repositories, + - hosting and running any VS Code extensions, or + - executing VS Code tasks[[2]]. + +The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`. + +The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_. + +[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process +[2]: https://code.visualstudio.com/Docs/editor/tasks + + +## Build from source + +If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the +project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions. + +### Build +```sh +yarn +``` + +### Rebuild the native dependencies +```sh +yarn rebuild:electron +``` + +### Start +```sh +yarn start +``` + +### CI + +This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions). + + - _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`. + - _Nightly_ builds run every day at 03:00 GMT from the `main` branch. + - _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build: + - Create a local tag: + ```sh + git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release." + ``` + - Push it to the remote: + ```sh + git push origin 1.2.3 + ``` + +## Notes for macOS contributors +Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally. +For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts. +Due to this limitation, Mac users have two options for testing contributions from forks: + +### The Safe approach (recommended) + +Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test. + +### The Risky approach + +*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.* +1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!). +1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push. +1. Re-enable Gatekeeper after tests are done, following the guide linked above. + +### Creating a release + +You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`. + + - Make sure the `main` state represents what you want to release and you're on `main`. + - Prepare a release-candidate build on a branch: +```bash +git branch 0.2.0-rc \ +&& git checkout 0.2.0-rc +``` + - Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one: +```bash +yarn update:version 0.2.0 +``` + - This should generate multiple outgoing changes with the version update. + - Commit your changes and push to the remote: +```bash +git add . \ +&& git commit -s -m "Updated versions to 0.2.0" \ +&& git push +``` + - Create the GH PR the workflow starts automatically. + - Once you're happy with the RC, merge the changes to the `main`. + - Create a tag and push it: +```bash +git tag -a 0.2.0 -m "0.2.0" \ +&& git push origin 0.2.0 +``` + - The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases). + - If you do not want to release the `EXE` and `MSI` installers, wipe them manually. + - If you do not like the generated changelog, modify it and update the GH release. + +## FAQ + +* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?* + + Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior. + +* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?* + + [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at: + - Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`, + - macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and + - Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`. + diff --git a/README.md b/README.md index bc7c27f79..532c5eaa8 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,24 @@ -# Arduino IDE + -[![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22) - -### Download +# Arduino IDE 2.x (beta) -You can download the latest version of the Arduino IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-ide/releases) or following the links in the following table. +[![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) -#### Latest version +This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino. -Platform | 32 bit | 64 bit | ---------- | ------------------------ | ------------------------------------------------------------------------------ | -Linux | | [Linux 64 bit] | -Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] | -Windows | | [Windows 64 bit installer]
[Windows 64 bit MSI]
[Windows 64 bit ZIP] | -macOS | | [macOS 64 bit] | +The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade. -[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287 -[Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Linux_64bit.zip -[Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.exe -[Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.msi -[Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.zip -[macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_macOS_64bit.dmg +> ⚠️ This is **beta** software. Help us test it! -#### Previous versions +![](static/screenshot.png) -These are available from the [GitHub releases page](https://github.com/arduino/arduino-ide/releases). +## Download -#### Nightly builds +You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software). +### Nightly builds These builds are generated every day at 03:00 GMT from the `main` branch and -should be considered unstable. In order to get the latest nightly build -available for the supported platform, use the following links: +should be considered unstable: Platform | 32 bit | 64 bit | --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | @@ -39,7 +27,7 @@ Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] Windows | | [Nightly Windows 64 bit installer]
[Nightly Windows 64 bit MSI]
[Nightly Windows 64 bit ZIP] | macOS | | [Nightly macOS 64 bit] | -[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287 +[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/107 [Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip [Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe [Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi @@ -49,117 +37,41 @@ macOS | | [Nightly macOS 64 bit] > These links return an HTTP `302: Found` response, redirecting to latest generated builds by replacing `latest` with the latest available build date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is - replaced with `20190806` ) - -### Build from source - -If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the -project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions. - -### Build -```sh -yarn -``` - -### Rebuild the native dependencies -```sh -yarn rebuild:electron -``` - -### Start -```sh -yarn start -``` - -### CI - -This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22). - - - _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`. - - _Nightly_ builds run every day at 03:00 GMT from the `main` branch. - - _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build: - - Create a local tag: - ```sh - git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release." - ``` - - Push it to the remote: - ```sh - git push origin 1.2.3 - ``` - -### Creating a GH release -This section guides you through how to create a new release. Let's assume the current version is `0.1.3` and you want to release `0.2.0`. - - - Make sure the `main` state represents what you want to release and you're on `main`. - - Prepare a release-candidate build on a branch: -```bash -git branch 0.2.0-rc \ -&& git checkout 0.2.0-rc -``` - - Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one: -```bash -yarn update:version 0.2.0 -``` - - This should generate multiple outgoing changes with the version update. - - Commit your changes and push to the remote: -```bash -git add . \ -&& git commit -s -m "Updated versions to 0.2.0" \ -&& git push -``` - - Create the GH PR the workflow starts automatically. - - Once you're happy with the RC, merge the changes to the `main`. - - Create a tag and push it: -```bash -git tag -a 0.2.0 -m "0.2.0" \ -&& git push origin 0.2.0 -``` - - The release build starts automatically and uploads the artifacts with the changelog to the Pro IDE [release page](https://github.com/arduino/arduino-ide/releases). - - If you do not want to release the `EXE` and `MSI` installers, wipe them manually. - - If you do not like the generated changelog, modify it and update the GH release. - - -### FAQ - - - Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE? - - A: Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior. - - - Q: I have understood that not all versions of the CLI is compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE? - - A: [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at: - - Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`, - - macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and - - Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`. - -### Architecture overview - -The Pro IDE consists of three major parts: - - the _Electron main_ process, - - the _backend_, and - - the _frontend_. - -The _Electron main_ process is responsible for: - - creating the application, - - managing the application lifecycle via listeners, and - - creating and managing the web pages for the app. - -In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly on main process. - -By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.[[1]] - -In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the Pro IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the Pro IDE, only the _backend_ allows OS interaction. - -The _backend_ process is responsible for: - - providing access to the filesystem, - - communicating with the Arduino CLI via gRPC, - - running your terminal, - - exposing additional RESTful APIs, - - performing the Git commands in the local repositories, - - hosting and running any VS Code extensions, or - - executing VS Code tasks[[2]]. - -The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`. - -The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_. - -[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process -[2]: https://code.visualstudio.com/Docs/editor/tasks + replaced with `20190806`) + +## Support + +If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-us/categories/360002212660-Software-and-Downloads) and browse the [forum](https://forum.arduino.cc/index.php?board=150.0). + +## Bugs & Issues + +If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply: + +* Before posting, please check if the same problem has been already reported by someone else to avoid duplicates. +* Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board. + +### Security + +If you think you found a vulnerability or other security-related bug in this project, please read our +[security policy](https://github.com/arduino/arduino-ide/security/policy) and report the bug to our Security Team 🛡️ +Thank you! + +e-mail contact: security@arduino.cc + +## Contributions and development + +Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users. + +This repository contains the main code, but two more repositories are included during the build process: + +* [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger +* [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code + +See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code. +## Donations + +This open source code was written by the Arduino team and is maintained on a daily basis with the help of the community. We invest a considerable amount of time in development, testing and optimization. Please consider [donating](https://www.arduino.cc/en/donate/) or [sponsoring](https://github.com/sponsors/arduino) to support our work, as well as [buying original Arduino boards](https://store.arduino.cc/) which is the best way to make sure our effort can continue in the long term. + +## License + +The code contained in this repository and the executable distributions are licensed under the terms of the GNU AGPLv3. The executable distributions contain third-party code licensed under other compatible licenses such as GPLv2, MIT and BSD-3. If you have questions about licensing please contact us at [license@arduino.cc](mailto:license@arduino.cc). diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 87805ef63..6c594cfec 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -1,8 +1,8 @@ { "name": "arduino-ide-extension", - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "description": "An extension for Theia building the Arduino IDE", - "license": "MIT", + "license": "AGPL-3.0-or-later", "scripts": { "prepare": "yarn download-cli && yarn download-ls && yarn clean && yarn download-examples && yarn build", "clean": "rimraf lib", @@ -115,16 +115,13 @@ "frontend": "lib/browser/theia/core/browser-menu-module", "frontendElectron": "lib/electron-browser/theia/core/electron-menu-module" }, - { - "frontend": "lib/browser/boards/quick-open/boards-quick-open-module" - }, { "electronMain": "lib/electron-main/arduino-electron-main-module" } ], "arduino": { "cli": { - "version": "0.16.1" + "version": "0.17.0" } } } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 092834fca..fa102118d 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -222,6 +222,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut webContents.setZoomLevel(event.newValue || 0); } }); + app.shell.leftPanelHandler.removeMenu('settings-menu'); } protected languageServerFqbn?: string; @@ -413,11 +414,20 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut id: 'arduino.toolbar.hoverBackground', defaults: { dark: 'button.hoverBackground', - light: 'button.hoverBackground', - hc: 'activityBar.inactiveForeground' + light: 'button.foreground', + hc: 'textLink.foreground' }, description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.' }, + { + id: 'arduino.toolbar.toggleBackground', + defaults: { + dark: 'editor.selectionBackground', + light: 'editor.selectionBackground', + hc: 'textPreformat.foreground' + }, + description: 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)' + }, { id: 'arduino.output.foreground', defaults: { diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 96efce4e3..8152d79dc 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -35,7 +35,9 @@ import { CommonFrontendContribution as TheiaCommonFrontendContribution, KeybindingRegistry as TheiaKeybindingRegistry, TabBarRendererFactory, - ContextMenuRenderer + ContextMenuRenderer, + createTreeContainer, + TreeWidget } from '@theia/core/lib/browser'; import { MenuContribution } from '@theia/core/lib/common/menu'; import { ApplicationShell } from './theia/core/application-shell'; @@ -143,6 +145,14 @@ import { ArchiveSketch } from './contributions/archive-sketch'; import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution'; import { OutputToolbarContribution } from './theia/output/output-toolbar-contribution'; import { AddZipLibrary } from './contributions/add-zip-library'; +import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; +import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution'; +import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager'; +import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; +import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget'; +import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget'; +import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget'; +import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -257,6 +267,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(WorkspaceService).toSelf().inSingletonScope(); rebind(TheiaWorkspaceService).toService(WorkspaceService); + bind(WorkspaceVariableContribution).toSelf().inSingletonScope(); + rebind(TheiaWorkspaceVariableContribution).toService(WorkspaceVariableContribution); // Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`. bind(EditorMode).toSelf().inSingletonScope(); @@ -294,6 +306,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoTextModelService).toSelf().inSingletonScope(); rebind(TheiaMonacoTextModelService).toService(MonacoTextModelService); + bind(SearchInWorkspaceWidget).toSelf(); + rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget); + rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(({ container }) => { + const childContainer = createTreeContainer(container); + childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf() + childContainer.rebind(TreeWidget).toService(SearchInWorkspaceResultTreeWidget); + return childContainer.get(SearchInWorkspaceResultTreeWidget); + }); + // Show a disconnected status bar, when the daemon is not available bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope(); rebind(TheiaApplicationConnectionStatusContribution).toService(ApplicationConnectionStatusContribution); @@ -390,6 +411,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // To remove the `Run` menu item from the application menu. bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope(); rebind(TheiaDebugFrontendApplicationContribution).toService(DebugFrontendApplicationContribution); + // To be able to use a `launch.json` from outside of the workspace. + bind(DebugConfigurationManager).toSelf().inSingletonScope(); + rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager); // Preferences bindArduinoPreferences(bind); diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 0b095f544..7070526c3 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -15,16 +15,10 @@ import { } from '../../common/protocol'; import { BoardsConfig } from './boards-config'; import { naturalCompare } from '../../common/utils'; -import { compareAnything } from '../theia/monaco/comparers'; import { NotificationCenter } from '../notification-center'; import { CommandService } from '@theia/core'; import { ArduinoCommands } from '../arduino-commands'; -interface BoardMatch { - readonly board: BoardWithPackage; - readonly matches: monaco.filters.IMatch[] | undefined; -} - @injectable() export class BoardsServiceProvider implements FrontendApplicationContribution { @@ -93,7 +87,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { if (!AttachedBoardsChangeEvent.isEmpty(event)) { this.logger.info('Attached boards and available ports changed:'); this.logger.info(AttachedBoardsChangeEvent.toString(event)); - this.logger.info(`------------------------------------------`); + this.logger.info('------------------------------------------'); } this._attachedBoards = event.newState.boards; this._availablePorts = event.newState.ports; @@ -205,38 +199,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { } } - async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise> { - const boards = await this.boardsService.allBoards({}); - const coresFilter = !!cores && cores.length - ? ((toFilter: BoardWithPackage) => cores.some(core => core === toFilter.packageName || core === toFilter.packageId)) - : () => true; - if (!query) { - return boards.filter(coresFilter).sort(Board.compare); - } - const toMatch = ((toFilter: BoardWithPackage) => (({ board: toFilter, matches: monaco.filters.matchesFuzzy(query, toFilter.name, true) }))); - const compareEntries = (left: BoardMatch, right: BoardMatch, lookFor: string) => { - const leftMatches = left.matches || []; - const rightMatches = right.matches || []; - if (leftMatches.length && !rightMatches.length) { - return -1; - } - if (!leftMatches.length && rightMatches.length) { - return 1; - } - if (leftMatches.length === 0 && rightMatches.length === 0) { - return 0; - } - const leftLabel = left.board.name.replace(/\r?\n/g, ' '); - const rightLabel = right.board.name.replace(/\r?\n/g, ' '); - return compareAnything(leftLabel, rightLabel, lookFor); - } - const normalizedQuery = query.toLowerCase(); - return boards - .filter(coresFilter) - .map(toMatch) - .filter(({ matches }) => !!matches) - .sort((left, right) => compareEntries(left, right, normalizedQuery)) - .map(({ board }) => board); + async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise { + const boards = await this.boardsService.searchBoards({ query }); + return boards; } get boardsConfig(): BoardsConfig.Config { diff --git a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts index fdbb290d3..47403845e 100644 --- a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts @@ -12,7 +12,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont widgetName: BoardsListWidget.WIDGET_LABEL, defaultWidgetOptions: { area: 'left', - rank: 600 + rank: 2 }, toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`, toggleKeybinding: 'CtrlCmd+Shift+B' diff --git a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts deleted file mode 100644 index 1d1f6e07e..000000000 --- a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ContainerModule } from 'inversify'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { CommandContribution } from '@theia/core/lib/common/command'; -import { QuickOpenContribution } from '@theia/core/lib/browser/quick-open'; -import { KeybindingContribution } from '@theia/core/lib/browser/keybinding'; -import { BoardsQuickOpenService } from './boards-quick-open-service'; - -export default new ContainerModule(bind => { - bind(BoardsQuickOpenService).toSelf().inSingletonScope(); - bind(CommandContribution).toService(BoardsQuickOpenService); - bind(KeybindingContribution).toService(BoardsQuickOpenService); - bind(QuickOpenContribution).toService(BoardsQuickOpenService); - bind(ILogger).toDynamicValue(({ container }) => container.get(ILogger).child('boards-quick-open')) - .inSingletonScope() - .whenTargetNamed('boards-quick-open'); -}); diff --git a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts deleted file mode 100644 index 11182fed6..000000000 --- a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts +++ /dev/null @@ -1,309 +0,0 @@ -import * as fuzzy from 'fuzzy'; -import { inject, injectable, postConstruct, named } from 'inversify'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command'; -import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; -import { QuickOpenItem, QuickOpenModel, QuickOpenMode, QuickOpenGroupItem } from '@theia/core/lib/common/quick-open-model'; -import { - QuickOpenService, - QuickOpenHandler, - QuickOpenOptions, - QuickOpenItemOptions, - QuickOpenContribution, - QuickOpenActionProvider, - QuickOpenHandlerRegistry, - QuickOpenGroupItemOptions -} from '@theia/core/lib/browser/quick-open'; -import { naturalCompare } from '../../../common/utils'; -import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol'; -import { BoardsDataStore } from '../boards-data-store'; -import { BoardsServiceProvider, AvailableBoard } from '../boards-service-provider'; -import { NotificationCenter } from '../../notification-center'; - -@injectable() -export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenModel, QuickOpenHandler, CommandContribution, KeybindingContribution, Command { - - readonly id = 'arduino-boards-quick-open'; - readonly prefix = '|'; - readonly description = 'Configure Available Boards'; - readonly label: 'Configure Available Boards'; - - @inject(ILogger) - @named('boards-quick-open') - protected readonly logger: ILogger; - - @inject(QuickOpenService) - protected readonly quickOpenService: QuickOpenService; - - @inject(BoardsService) - protected readonly boardsService: BoardsService; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - - @inject(BoardsDataStore) - protected readonly boardsDataStore: BoardsDataStore; - - @inject(NotificationCenter) - protected notificationCenter: NotificationCenter; - - protected isOpen: boolean = false; - protected currentQuery: string = ''; - // Attached boards plus the user's config. - protected availableBoards: AvailableBoard[] = []; - // Only for the `selected` one from the `availableBoards`. Note: the `port` of the `selected` is optional. - protected data: BoardsDataStore.Data = BoardsDataStore.Data.EMPTY; - protected allBoards: Board.Detailed[] = [] - protected selectedBoard?: (AvailableBoard & { port: Port }); - - // `init` name is used by the `QuickOpenHandler`. - @postConstruct() - protected postConstruct(): void { - this.notificationCenter.onIndexUpdated(() => this.update(this.availableBoards)); - this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards)); - this.update(this.boardsServiceClient.availableBoards); - } - - registerCommands(registry: CommandRegistry): void { - registry.registerCommand(this, { execute: () => this.open() }); - } - - registerKeybindings(registry: KeybindingRegistry): void { - registry.registerKeybinding({ command: this.id, keybinding: 'ctrlCmd+k ctrlCmd+b' }); - } - - registerQuickOpenHandlers(registry: QuickOpenHandlerRegistry): void { - registry.registerHandler(this); - } - - getModel(): QuickOpenModel { - return this; - } - - getOptions(): QuickOpenOptions { - let placeholder = ''; - if (!this.selectedBoard) { - placeholder += 'No board selected.'; - } - placeholder += 'Type to filter boards'; - if (this.data.configOptions.length) { - placeholder += ' or use the ↓↑ keys to adjust the board settings...'; - } else { - placeholder += '...'; - } - return { - placeholder, - fuzzyMatchLabel: true, - onClose: () => this.isOpen = false - }; - } - - open(): void { - this.isOpen = true; - this.quickOpenService.open(this, this.getOptions()); - } - - onType( - lookFor: string, - acceptor: (items: QuickOpenItem[], actionProvider?: QuickOpenActionProvider) => void): void { - - this.currentQuery = lookFor; - const fuzzyFilter = this.fuzzyFilter(lookFor); - const availableBoards = this.availableBoards.filter(AvailableBoard.hasPort).filter(({ name }) => fuzzyFilter(name)); - const toAccept: QuickOpenItem[] = []; - - // Show the selected attached in a different group. - if (this.selectedBoard && fuzzyFilter(this.selectedBoard.name)) { - toAccept.push(this.toQuickItem(this.selectedBoard, { groupLabel: 'Selected Board' })); - } - - // Filter the selected from the attached ones. - toAccept.push(...availableBoards.filter(board => board !== this.selectedBoard).map((board, i) => { - let group: QuickOpenGroupItemOptions | undefined = undefined; - if (i === 0) { - // If no `selectedBoard`, then this item is the top one, no borders required. - group = { groupLabel: 'Attached Boards', showBorder: !!this.selectedBoard }; - } - return this.toQuickItem(board, group); - })); - - // Show the config only if the `input` is empty. - if (!lookFor.trim().length) { - toAccept.push(...this.data.configOptions.map((config, i) => { - let group: QuickOpenGroupItemOptions | undefined = undefined; - if (i === 0) { - group = { groupLabel: 'Board Settings', showBorder: true }; - } - return this.toQuickItem(config, group); - })); - } else { - toAccept.push(...this.allBoards.filter(({ name }) => fuzzyFilter(name)).map((board, i) => { - let group: QuickOpenGroupItemOptions | undefined = undefined; - if (i === 0) { - group = { groupLabel: 'Boards', showBorder: true }; - } - return this.toQuickItem(board, group); - })); - } - - acceptor(toAccept); - } - - private fuzzyFilter(lookFor: string): (inputString: string) => boolean { - const shouldFilter = !!lookFor.trim().length; - return (inputString: string) => shouldFilter ? fuzzy.test(lookFor.toLocaleLowerCase(), inputString.toLocaleLowerCase()) : true; - } - - protected async update(availableBoards: AvailableBoard[]): Promise { - // `selectedBoard` is not an attached board, we need to show the board settings for it (TODO: clarify!) - const selectedBoard = availableBoards.filter(AvailableBoard.hasPort).find(({ selected }) => selected); - const [data, boards] = await Promise.all([ - selectedBoard && selectedBoard.fqbn ? this.boardsDataStore.getData(selectedBoard.fqbn) : Promise.resolve(BoardsDataStore.Data.EMPTY), - this.boardsService.allBoards({}) - ]); - this.allBoards = Board.decorateBoards(selectedBoard, boards) - .filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board))); - this.availableBoards = availableBoards; - this.data = data; - this.selectedBoard = selectedBoard; - - if (this.isOpen) { - // Hack, to update the state without closing and reopening the quick open widget. - (this.quickOpenService as any).onType(this.currentQuery); - } - } - - protected toQuickItem(item: BoardsQuickOpenService.Item, group?: QuickOpenGroupItemOptions): QuickOpenItem { - let options: QuickOpenItemOptions; - if (AvailableBoard.is(item)) { - const description = `on ${Port.toString(item.port)}` - options = { - label: `${item.name}`, - description, - descriptionHighlights: [ - { - start: 0, - end: description.length - } - ], - run: this.toRun(() => this.boardsServiceClient.boardsConfig = ({ selectedBoard: item, selectedPort: item.port })) - }; - } else if (ConfigOption.is(item)) { - const selected = item.values.find(({ selected }) => selected); - const value = selected ? selected.label : 'Not set'; - const label = `${item.label}: ${value}`; - options = { - label, - // Intended to match the value part of a board setting. - // NOTE: this does not work, as `fuzzyMatchLabel: true` is set. Manual highlighting is ignored, apparently. - labelHighlights: [ - { - start: label.length - value.length, - end: label.length - } - ], - run: (mode) => { - if (mode === QuickOpenMode.OPEN) { - this.setConfig(item); - return false; - } - return true; - } - }; - if (!selected) { - options.description = 'Not set'; - }; - } else { - options = { - label: `${item.name}`, - description: `${item.missing ? '' : `[installed with '${item.packageName}']`}`, - run: (mode) => { - if (mode === QuickOpenMode.OPEN) { - this.selectBoard(item); - return false; - } - return true; - } - }; - } - if (group) { - return new QuickOpenGroupItem({ ...options, ...group }); - } else { - return new QuickOpenItem(options); - } - } - - protected toRun(run: (() => void)): ((mode: QuickOpenMode) => boolean) { - return (mode) => { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - run(); - return true; - }; - } - - protected async selectBoard(board: Board): Promise { - const allPorts = this.availableBoards.filter(AvailableBoard.hasPort).map(({ port }) => port).sort(Port.compare); - const toItem = (port: Port) => new QuickOpenItem({ - label: Port.toString(port, { useLabel: true }), - run: this.toRun(() => { - this.boardsServiceClient.boardsConfig = { - selectedBoard: board, - selectedPort: port - }; - }) - }); - const options = { - placeholder: `Select a port for '${board.name}'. Press 'Enter' to confirm or 'Escape' to cancel.`, - fuzzyMatchLabel: true - } - this.quickOpenService.open({ - onType: (lookFor, acceptor) => { - const fuzzyFilter = this.fuzzyFilter(lookFor); - acceptor(allPorts.filter(({ address }) => fuzzyFilter(address)).map(toItem)); - } - }, options); - } - - protected async setConfig(config: ConfigOption): Promise { - const toItem = (value: ConfigValue) => new QuickOpenItem({ - label: value.label, - iconClass: value.selected ? 'fa fa-check' : '', - run: this.toRun(() => { - if (!this.selectedBoard) { - this.logger.warn(`Could not alter the boards settings. No board selected. ${JSON.stringify(config)}`); - return; - } - if (!this.selectedBoard.fqbn) { - this.logger.warn(`Could not alter the boards settings. The selected board does not have a FQBN. ${JSON.stringify(this.selectedBoard)}`); - return; - } - const { fqbn } = this.selectedBoard; - this.boardsDataStore.selectConfigOption({ - fqbn, - option: config.option, - selectedValue: value.value - }); - }) - }); - const options = { - placeholder: `Configure '${config.label}'. Press 'Enter' to confirm or 'Escape' to cancel.`, - fuzzyMatchLabel: true - } - this.quickOpenService.open({ - onType: (lookFor, acceptor) => { - const fuzzyFilter = this.fuzzyFilter(lookFor); - acceptor(config.values - .filter(({ label }) => fuzzyFilter(label)) - .sort((left, right) => naturalCompare(left.label, right.label)) - .map(toItem)); - } - }, options); - } - -} - -export namespace BoardsQuickOpenService { - export type Item = AvailableBoard & { port: Port } | Board.Detailed | ConfigOption; -} diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts index 508a02d44..0a12e979d 100644 --- a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -6,6 +6,7 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import URI from '@theia/core/lib/common/uri'; import { InstallationProgressDialog } from '../widgets/progress-dialog'; import { LibraryService } from '../../common/protocol'; +import { ConfirmDialog } from '@theia/core/lib/browser'; @injectable() export class AddZipLibrary extends SketchContribution { @@ -49,21 +50,60 @@ export class AddZipLibrary extends SketchContribution { }); if (!canceled && filePaths.length) { const zipUri = await this.fileSystemExt.getUri(filePaths[0]); - const dialog = new InstallationProgressDialog('Installing library', 'zip'); try { - this.outputChannelManager.getChannel('Arduino').clear(); - dialog.open(); - await this.libraryService.installZip({ zipUri }); - } catch (e) { - this.messageService.error(e.toString()); - } finally { - dialog.close(); + await this.doInstall(zipUri); + } catch (error) { + if (error instanceof AlreadyInstalledError) { + const result = await new ConfirmDialog({ + msg: error.message, + title: 'Do you want to overwrite the existing library?', + ok: 'Yes', + cancel: 'No' + }).open(); + if (result) { + await this.doInstall(zipUri, true); + } + } + } + } + } + + private async doInstall(zipUri: string, overwrite?: boolean): Promise { + const dialog = new InstallationProgressDialog('Installing library', 'zip'); + try { + this.outputChannelManager.getChannel('Arduino').clear(); + dialog.open(); + await this.libraryService.installZip({ zipUri, overwrite }); + } catch (error) { + if (error instanceof Error) { + const match = error.message.match(/library (.*?) already installed/); + if (match && match.length >= 2) { + const name = match[1].trim(); + if (name) { + throw new AlreadyInstalledError(`A library folder named ${name} already exists. Do you want to overwrite it?`, name); + } else { + throw new AlreadyInstalledError('A library already exists. Do you want to overwrite it?'); + } + } } + this.messageService.error(error.toString()); + throw error; + } finally { + dialog.close(); } } } +class AlreadyInstalledError extends Error { + + constructor(message: string, readonly libraryName?: string) { + super(message); + Object.setPrototypeOf(this, AlreadyInstalledError.prototype); + } + +} + export namespace AddZipLibrary { export namespace Commands { export const ADD_ZIP_LIBRARY: Command = { diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index 3373e20cf..3e84a76d0 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -210,7 +210,7 @@ PID: ${PID}`; } protected async installedBoards(): Promise { - const allBoards = await this.boardsService.allBoards({}); + const allBoards = await this.boardsService.searchBoards({}); return allBoards.filter(InstalledBoardWithPackage.is); } diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index e6855d315..193922c43 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -106,9 +106,11 @@ export class Debug extends SketchContribution { if (!sketch) { return; } - const [cliPath, sketchPath] = await Promise.all([ + const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(sketch); + const [cliPath, sketchPath, configPath] = await Promise.all([ this.fileService.fsPath(new URI(executables.cliUri)), - this.fileService.fsPath(new URI(sketch.uri)) + this.fileService.fsPath(new URI(sketch.uri)), + this.fileService.fsPath(new URI(ideTempFolderUri)), ]) const config = { cliPath, @@ -116,7 +118,8 @@ export class Debug extends SketchContribution { fqbn, name }, - sketchPath + sketchPath, + configPath }; return this.commandService.executeCommand('arduino.debug.start', config); } diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index 4b1d5f1a5..480095097 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -1,15 +1,15 @@ import * as PQueue from 'p-queue'; import { inject, injectable, postConstruct } from 'inversify'; -import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu'; +import { MenuPath, CompositeMenuNode, SubMenuOptions } from '@theia/core/lib/common/menu'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { OpenSketch } from './open-sketch'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { MainMenuManager } from '../../common/main-menu-manager'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; -import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service'; +import { ExamplesService } from '../../common/protocol/examples-service'; import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution'; import { NotificationCenter } from '../notification-center'; -import { Board } from '../../common/protocol'; +import { Board, Sketch, SketchContainer } from '../../common/protocol'; @injectable() export abstract class Examples extends SketchContribution { @@ -59,18 +59,35 @@ export abstract class Examples extends SketchContribution { } registerRecursively( - exampleContainerOrPlaceholder: ExampleContainer | string, + sketchContainerOrPlaceholder: SketchContainer | (Sketch | SketchContainer)[] | string, menuPath: MenuPath, - pushToDispose: DisposableCollection = new DisposableCollection()): void { + pushToDispose: DisposableCollection = new DisposableCollection(), + subMenuOptions?: SubMenuOptions | undefined): void { - if (typeof exampleContainerOrPlaceholder === 'string') { - const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder); + if (typeof sketchContainerOrPlaceholder === 'string') { + const placeholder = new PlaceholderMenuNode(menuPath, sketchContainerOrPlaceholder); this.menuRegistry.registerMenuNode(menuPath, placeholder); pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id))); } else { - const { label, sketches, children } = exampleContainerOrPlaceholder; - const submenuPath = [...menuPath, label]; - this.menuRegistry.registerSubmenu(submenuPath, label); + const sketches: Sketch[] = []; + const children: SketchContainer[] = []; + let submenuPath = menuPath; + + if (SketchContainer.is(sketchContainerOrPlaceholder)) { + const { label } = sketchContainerOrPlaceholder; + submenuPath = [...menuPath, label]; + this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions); + sketches.push(...sketchContainerOrPlaceholder.sketches); + children.push(...sketchContainerOrPlaceholder.children); + } else { + for (const sketchOrContainer of sketchContainerOrPlaceholder) { + if (SketchContainer.is(sketchOrContainer)) { + children.push(sketchOrContainer); + } else { + sketches.push(sketchOrContainer); + } + } + } children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose)); for (const sketch of sketches) { const { uri } = sketch; @@ -83,7 +100,7 @@ export abstract class Examples extends SketchContribution { } }; pushToDispose.push(this.commandRegistry.registerCommand(command, handler)); - this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name }); + this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name, order: sketch.name.toLocaleLowerCase() }); pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))); } } @@ -98,22 +115,20 @@ export class BuiltInExamples extends Examples { this.register(); // no `await` } - protected async register() { - let exampleContainers: ExampleContainer[] | undefined; + protected async register(): Promise { + let sketchContainers: SketchContainer[] | undefined; try { - exampleContainers = await this.examplesService.builtIns(); + sketchContainers = await this.examplesService.builtIns(); } catch (e) { console.error('Could not initialize built-in examples.', e); this.messageService.error('Could not initialize built-in examples.'); return; } this.toDispose.dispose(); - for (const container of ['Built-in examples', ...exampleContainers]) { + for (const container of ['Built-in examples', ...sketchContainers]) { this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose); } this.menuManager.update(); - // TODO: remove - console.log(typeof this.menuRegistry); } } @@ -136,7 +151,7 @@ export class LibraryExamples extends Examples { this.register(board); } - protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) { + protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard): Promise { return this.queue.add(async () => { this.toDispose.dispose(); if (!board || !board.fqbn) { diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-sketch.ts index a04f882f3..5d6f74123 100644 --- a/arduino-ide-extension/src/browser/contributions/open-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-sketch.ts @@ -8,6 +8,8 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; import { ExamplesService } from '../../common/protocol/examples-service'; import { BuiltInExamples } from './examples'; +import { Sketchbook } from './sketchbook'; +import { SketchContainer } from '../../common/protocol'; @injectable() export class OpenSketch extends SketchContribution { @@ -24,7 +26,10 @@ export class OpenSketch extends SketchContribution { @inject(ExamplesService) protected readonly examplesService: ExamplesService; - protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection(); + @inject(Sketchbook) + protected readonly sketchbook: Sketchbook; + + protected readonly toDispose = new DisposableCollection(); registerCommands(registry: CommandRegistry): void { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, { @@ -33,11 +38,11 @@ export class OpenSketch extends SketchContribution { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, { isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', execute: async (_: Widget, target: EventTarget) => { - const sketches = await this.sketchService.getSketches(); - if (!sketches.length) { + const container = await this.sketchService.getSketches({ exclude: ['**/hardware/**'] }); + if (SketchContainer.isEmpty(container)) { this.openSketch(); } else { - this.toDisposeBeforeCreateNewContextMenu.dispose(); + this.toDispose.dispose(); if (!(target instanceof HTMLElement)) { return; } @@ -50,21 +55,12 @@ export class OpenSketch extends SketchContribution { commandId: OpenSketch.Commands.OPEN_SKETCH.id, label: 'Open...' }); - this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH))); - for (const sketch of sketches) { - const command = { id: `arduino-open-sketch--${sketch.uri}` }; - const handler = { execute: () => this.openSketch(sketch) }; - this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler)); - this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, { - commandId: command.id, - label: sketch.name - }); - this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))); - } + this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH))); + this.sketchbook.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, this.toDispose); try { const containers = await this.examplesService.builtIns(); for (const container of containers) { - this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu); + this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDispose); } } catch (e) { console.error('Error when collecting built-in examples.', e); diff --git a/arduino-ide-extension/src/browser/contributions/sketchbook.ts b/arduino-ide-extension/src/browser/contributions/sketchbook.ts index 55efb77d7..9264bed59 100644 --- a/arduino-ide-extension/src/browser/contributions/sketchbook.ts +++ b/arduino-ide-extension/src/browser/contributions/sketchbook.ts @@ -1,13 +1,13 @@ import { inject, injectable } from 'inversify'; -import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution'; +import { CommandRegistry, MenuModelRegistry } from './contribution'; import { ArduinoMenus } from '../menu/arduino-menus'; import { MainMenuManager } from '../../common/main-menu-manager'; import { NotificationCenter } from '../notification-center'; -import { OpenSketch } from './open-sketch'; +import { Examples } from './examples'; +import { SketchContainer } from '../../common/protocol'; @injectable() -export class Sketchbook extends SketchContribution { +export class Sketchbook extends Examples { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @@ -21,17 +21,16 @@ export class Sketchbook extends SketchContribution { @inject(NotificationCenter) protected readonly notificationCenter: NotificationCenter; - protected toDisposePerSketch = new Map(); - onStart(): void { - this.sketchService.getSketches().then(sketches => { - this.register(sketches); + this.sketchService.getSketches({}).then(container => { + this.register(container); this.mainMenuManager.update(); }); - this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => { - this.unregister(removed); - this.register(created); - this.mainMenuManager.update(); + this.sketchServiceClient.onSketchbookDidChange(() => { + this.sketchService.getSketches({}).then(container => { + this.register(container); + this.mainMenuManager.update(); + }); }); } @@ -39,31 +38,9 @@ export class Sketchbook extends SketchContribution { registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' }); } - protected register(sketches: Sketch[]): void { - for (const sketch of sketches) { - const { uri } = sketch; - const toDispose = this.toDisposePerSketch.get(uri); - if (toDispose) { - toDispose.dispose(); - } - const command = { id: `arduino-sketchbook-open--${uri}` }; - const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) }; - this.commandRegistry.registerCommand(command, handler); - this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name }); - this.toDisposePerSketch.set(sketch.uri, new DisposableCollection( - Disposable.create(() => this.commandRegistry.unregisterCommand(command)), - Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)) - )); - } - } - - protected unregister(sketches: Sketch[]): void { - for (const { uri } of sketches) { - const toDispose = this.toDisposePerSketch.get(uri); - if (toDispose) { - toDispose.dispose(); - } - } + protected register(container: SketchContainer): void { + this.toDispose.dispose(); + this.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, this.toDispose); } } diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 4f0109aa8..c06dece19 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -1,4 +1,5 @@ import { inject, injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; @@ -22,15 +23,24 @@ export class UploadSketch extends SketchContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClientImpl: BoardsServiceProvider; + protected readonly onDidChangeEmitter = new Emitter>(); + readonly onDidChange = this.onDidChangeEmitter.event; + + protected uploadInProgress = false; + registerCommands(registry: CommandRegistry): void { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { - execute: () => this.uploadSketch() + execute: () => this.uploadSketch(), + isEnabled: () => !this.uploadInProgress, }); registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, { - execute: () => this.uploadSketch(true) + execute: () => this.uploadSketch(true), + isEnabled: () => !this.uploadInProgress, }); registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, { isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + isEnabled: () => !this.uploadInProgress, + isToggled: () => this.uploadInProgress, execute: () => registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id) }); } @@ -64,11 +74,22 @@ export class UploadSketch extends SketchContribution { id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id, command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id, tooltip: 'Upload', - priority: 1 + priority: 1, + onDidChange: this.onDidChange }); } async uploadSketch(usingProgrammer: boolean = false): Promise { + + // even with buttons disabled, better to double check if an upload is already in progress + if (this.uploadInProgress) { + return; + } + + // toggle the toolbar button and menu item state. + // uploadInProgress will be set to false whether the upload fails or not + this.uploadInProgress = true; + this.onDidChangeEmitter.fire(); const sketch = await this.sketchServiceClient.currentSketch(); if (!sketch) { return; @@ -131,6 +152,9 @@ export class UploadSketch extends SketchContribution { } catch (e) { this.messageService.error(e.toString()); } finally { + this.uploadInProgress = false; + this.onDidChangeEmitter.fire(); + if (monitorConfig) { const { board, port } = monitorConfig; try { diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index fe9b3e074..225d1281c 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -1,4 +1,5 @@ import { inject, injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; @@ -18,15 +19,24 @@ export class VerifySketch extends SketchContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClientImpl: BoardsServiceProvider; + protected readonly onDidChangeEmitter = new Emitter>(); + readonly onDidChange = this.onDidChangeEmitter.event; + + protected verifyInProgress = false; + registerCommands(registry: CommandRegistry): void { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { - execute: () => this.verifySketch() + execute: () => this.verifySketch(), + isEnabled: () => !this.verifyInProgress, }); registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { - execute: () => this.verifySketch(true) + execute: () => this.verifySketch(true), + isEnabled: () => !this.verifyInProgress, }); registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + isEnabled: () => !this.verifyInProgress, + isToggled: () => this.verifyInProgress, execute: () => registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id) }); } @@ -60,12 +70,24 @@ export class VerifySketch extends SketchContribution { id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id, command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id, tooltip: 'Verify', - priority: 0 + priority: 0, + onDidChange: this.onDidChange }); } async verifySketch(exportBinaries?: boolean): Promise { + + // even with buttons disabled, better to double check if a verify is already in progress + if (this.verifyInProgress) { + return; + } + + // toggle the toolbar button and menu item state. + // verifyInProgress will be set to false whether the compilation fails or not + this.verifyInProgress = true; + this.onDidChangeEmitter.fire(); const sketch = await this.sketchServiceClient.currentSketch(); + if (!sketch) { return; } @@ -90,6 +112,9 @@ export class VerifySketch extends SketchContribution { this.messageService.info('Done compiling.', { timeout: 1000 }); } catch (e) { this.messageService.error(e.toString()); + } finally { + this.verifyInProgress = false; + this.onDidChangeEmitter.fire(); } } diff --git a/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts index eb87dd194..5dbace9a8 100644 --- a/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts @@ -14,7 +14,7 @@ export class LibraryListWidgetFrontendContribution extends AbstractViewContribut widgetName: LibraryListWidget.WIDGET_LABEL, defaultWidgetOptions: { area: 'left', - rank: 700 + rank: 3 }, toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`, toggleKeybinding: 'CtrlCmd+Shift+I' diff --git a/arduino-ide-extension/src/browser/output-service-impl.ts b/arduino-ide-extension/src/browser/output-service-impl.ts index 098db8b42..60c5a94bd 100644 --- a/arduino-ide-extension/src/browser/output-service-impl.ts +++ b/arduino-ide-extension/src/browser/output-service-impl.ts @@ -14,7 +14,7 @@ export class OutputServiceImpl implements OutputService { append(message: OutputMessage): void { const { chunk } = message; - const channel = this.outputChannelManager.getChannel(`Arduino`); + const channel = this.outputChannelManager.getChannel('Arduino'); channel.show({ preserveFocus: true }); channel.append(chunk); } diff --git a/arduino-ide-extension/src/browser/settings.tsx b/arduino-ide-extension/src/browser/settings.tsx index 650868406..6db428ddb 100644 --- a/arduino-ide-extension/src/browser/settings.tsx +++ b/arduino-ide-extension/src/browser/settings.tsx @@ -24,6 +24,7 @@ export interface Settings extends Index { editorFontSize: number; // `editor.fontSize` themeId: string; // `workbench.colorTheme` autoSave: 'on' | 'off'; // `editor.autoSave` + quickSuggestions: Record<'other'|'comments'|'strings', boolean>; // `editor.quickSuggestions` autoScaleInterface: boolean; // `arduino.window.autoScale` interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751 @@ -84,6 +85,7 @@ export class SettingsService { editorFontSize, themeId, autoSave, + quickSuggestions, autoScaleInterface, interfaceScale, // checkForUpdates, @@ -97,6 +99,11 @@ export class SettingsService { this.preferenceService.get('editor.fontSize', 12), this.preferenceService.get('workbench.colorTheme', 'arduino-theme'), this.preferenceService.get<'on' | 'off'>('editor.autoSave', 'on'), + this.preferenceService.get('editor.quickSuggestion', { + 'other': false, + 'comments': false, + 'strings': false + }), this.preferenceService.get('arduino.window.autoScale', true), this.preferenceService.get('arduino.window.zoomLevel', 0), // this.preferenceService.get('arduino.ide.autoUpdate', true), @@ -113,6 +120,7 @@ export class SettingsService { editorFontSize, themeId, autoSave, + quickSuggestions, autoScaleInterface, interfaceScale, // checkForUpdates, @@ -155,10 +163,10 @@ export class SettingsService { return `Invalid sketchbook location: ${sketchbookPath}`; } if (editorFontSize <= 0) { - return `Invalid editor font size. It must be a positive integer.`; + return 'Invalid editor font size. It must be a positive integer.'; } if (!ThemeService.get().getThemes().find(({ id }) => id === themeId)) { - return `Invalid theme.`; + return 'Invalid theme.'; } return true; } catch (err) { @@ -175,6 +183,7 @@ export class SettingsService { editorFontSize, themeId, autoSave, + quickSuggestions, autoScaleInterface, interfaceScale, // checkForUpdates, @@ -199,6 +208,7 @@ export class SettingsService { this.preferenceService.set('editor.fontSize', editorFontSize, PreferenceScope.User), this.preferenceService.set('workbench.colorTheme', themeId, PreferenceScope.User), this.preferenceService.set('editor.autoSave', autoSave, PreferenceScope.User), + this.preferenceService.set('editor.quickSuggestions', quickSuggestions, PreferenceScope.User), this.preferenceService.set('arduino.window.autoScale', autoScaleInterface, PreferenceScope.User), this.preferenceService.set('arduino.window.zoomLevel', interfaceScale, PreferenceScope.User), // this.preferenceService.set('arduino.ide.autoUpdate', checkForUpdates, PreferenceScope.User), @@ -360,6 +370,13 @@ export class SettingsComponent extends React.Component Auto save +