Compare commits

...

26 Commits

Author SHA1 Message Date
Francesco Stasi
7d5381bbde Updated to 2.0.0-beta.11 2021-08-25 10:43:10 +02:00
Francesco Stasi
302fb7b6af [ATL-1533] Firmware&Certificate Uploader (#469)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-08-25 10:36:51 +02:00
Alberto Iannaccone
6233e1fa98 [ATL-493] Support platforms installed in directories.user 2021-08-23 10:47:36 +02:00
per1234
2cb9889fe4 Add source URL comment to "Check Certificates" workflow
This will make it easier for the maintainers to sync fixes and improvements in either direction between the upstream
"template" workflow and its installation in this repository.
2021-08-18 03:00:24 -07:00
per1234
bed6e0b741 Use major version ref of Slack notification action
At the time the workflow was written the authors of the `rtCamp/action-slack-notify` GitHub Actions action did not
provide a major version ref. This meant that it was necessary to pin the action to a specific version.

Since then, a few new releases have been made, meaning an outdated version of the action was in use as a consequence of
the pinning.

The action now offers a `v2` major ref. Use of this ref will cause the workflow to benefit from ongoing development to
the action up until such time as a new major release is made, at which time we would need to evaluate whether any changes
to the workflow are required by the breaking change that triggered the major release before updating the major ref
(e.g., `uses: rtCamp/action-slack-notify@v3`).
2021-08-18 03:00:24 -07:00
per1234
302f0109dd Use standardized repository secret name for Slack webhook
The "Check Certificates" workflow is configured to send a notification via Slack if a problem is found with a certificate.
TThis is currently posted to the `team_tooling` channel, but that is not necessarily always going to be the case, and for
every deployment of the workflow. So a less specific secret name is more universally applicable to serve all applications
of this "template" workflow.
2021-08-18 03:00:24 -07:00
per1234
735d3733e2 Make trivial formatting changes to "Check Certificates" workflow
No functional change, and neither is necessarily superior, but this is the formatting style either defined in the
"template", or by the repository's Prettier formatting configuration preferences, so it must be brought into compliance
here as well.
2021-08-18 03:00:24 -07:00
per1234
4b36852f57 Use the matrix identifier to name the "Check Certificates" workflow jobs
When no name is provided for a matrix job, the workflow job is named according to the contents of
`jobs[].<job_id>.strategy.matrix[]`. That can result in some fairly cryptic job names when the matrix contains a complex
data structure as is the case here. We already have a string to uniquely identify each certificate to humans, which is
exactly what the `jobs[].<job_id>.name` property does for jobs, so it will be an improvement to name the jobs according
to that identifier.
2021-08-18 03:00:24 -07:00
per1234
b84b6c921d Make trivial adjustments to comments in "Check Certificates" workflow
No functional difference, and neither is necessarily superior, but this is how it is in the "template", and so it must be
here as well.
2021-08-18 03:00:24 -07:00
per1234
289f07f187 Run "Check Certificates" workflow on modification
This will facilitate testing and review of modifications to the workflow.

Because the workflow requires access to repository secrets, and so will fail whenever triggered by an event from a fork,
a conditional is added to make it only run when the modifications are made within the `arduino/arduino-ide`
repository.
2021-08-18 03:00:24 -07:00
per1234
b9c777a5c3 Add API trigger to "Check Certificates" workflow
The `repository_dispatch` event allows triggering workflows via the GitHub API. This might be useful for triggering an
immediate check in multiple relevant repositories after an external change, or some automated process. Although we don't
have any specific need for this event at the moment, the event has no impact on the workflow, so there is no reason
against having it. It is the sort of thing that can end up being useful if it is already in consistently in place, but
not worth setting up on demand, since the effort to set it up is greater than the effort to trigger all the workflows
manually.
2021-08-18 03:00:24 -07:00
per1234
92af4bef26 Use standardized name for certificate check workflow
This is the naming convention established in the standardized "template" workflow.
2021-08-18 03:00:24 -07:00
Jim Marinis
167f059163 Update BUILDING.md
Corrected typographical error where "on" was used rather than "one".
2021-08-06 05:16:05 -07:00
Francesco Stasi
93515fc906 Updated to 2.0.0-beta.10 2021-08-05 10:21:51 +02:00
Francesco Stasi
20c2e1c67e [ATL-1539] Integrate FWUploader into IDE2 (#466) 2021-07-28 16:42:38 +02:00
Alberto Iannaccone
65152731f9 [ATL-1454] Refactor pull/push to edit files in place (#464)
* improve push/pull process

* improved diff tree performance generation

* skip some files to be synced

Co-authored-by: Francesco Stasi <f.stasi@me.com>
2021-07-28 14:00:54 +02:00
Francesco Stasi
57b9eb95bb preserve node expanded state on refresh 2021-07-23 17:03:44 +02:00
Francesco Stasi
64dc124a53 fixed remote sketches sorting 2021-07-23 12:23:21 +02:00
Alberto Iannaccone
38d372e2d5 force some files to be read-only (#453) 2021-07-23 10:01:42 +02:00
Alberto Iannaccone
5897f379a4 fix url to open sketch in cloud editor (#452) 2021-07-23 10:01:21 +02:00
Francesco Stasi
d790266cc8 Improve remote sketchbook explorer (#459)
* Refactor remote sketchbook explorer
* sketches sorting
2021-07-22 14:34:10 +02:00
Francesco Stasi
4da5d573e4 [atl-1433][atl-1433] improve local sketchbook explorer (#446) 2021-07-21 15:48:15 +02:00
Francesco Stasi
4e6f9ae75d ATL-1451: reveal sketch directory in file explorer (#450) 2021-07-13 15:09:23 +02:00
Francesco Stasi
e10f0f1683 Make tab width 2 spaces (#445) 2021-07-09 10:14:42 +02:00
Francesco Stasi
40a73af82b Updated to 2.0.0-beta.9 2021-07-08 16:41:04 +02:00
Francesco Stasi
461ca06445 Include arduino_secrets when needed (#438)
* include arduino_secrets when needed

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-07-08 16:39:16 +02:00
243 changed files with 40596 additions and 20226 deletions

View File

@@ -1,35 +1,41 @@
name: Check for issues with signing certificates # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-certificates.md
name: Check Certificates
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on: on:
push:
paths:
- '.github/workflows/check-certificates.ya?ml'
pull_request:
paths:
- '.github/workflows/check-certificates.ya?ml'
schedule: schedule:
# run every 10 hours # Run every 10 hours.
- cron: "0 */10 * * *" - cron: '0 */10 * * *'
# workflow_dispatch event allows the workflow to be triggered manually.
# This could be used to run an immediate check after updating certificate secrets.
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
workflow_dispatch: workflow_dispatch:
repository_dispatch:
env: env:
# Begin notifications when there are less than this many days remaining before expiration # Begin notifications when there are less than this many days remaining before expiration.
EXPIRATION_WARNING_PERIOD: 30 EXPIRATION_WARNING_PERIOD: 30
jobs: jobs:
check-certificates: check-certificates:
name: ${{ matrix.certificate.identifier }}
# Only run when the workflow will have access to the certificate secrets. # Only run when the workflow will have access to the certificate secrets.
if: > if: >
(github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') || (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') (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide')
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
certificate: certificate:
- identifier: macOS signing certificate # Text used to identify the certificate in notifications # Additional certificate definitions can be added to this list.
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # The name of the secret that contains the certificate - identifier: macOS signing certificate # Text used to identify certificate in notifications.
password-secret: KEYCHAIN_PASSWORD # The name of the secret that contains the certificate password 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.
- identifier: Windows signing certificate - identifier: Windows signing certificate
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
@@ -37,7 +43,7 @@ jobs:
steps: steps:
- name: Set certificate path environment variable - name: Set certificate path environment variable
run: | run: |
# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable # See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV" echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
- name: Decode certificate - name: Decode certificate
@@ -59,18 +65,17 @@ jobs:
exit 1 exit 1
) )
# See: https://github.com/rtCamp/action-slack-notify
- name: Slack notification of certificate verification failure - name: Slack notification of certificate verification failure
if: failure() if: failure()
uses: rtCamp/action-slack-notify@v2.1.0
env: env:
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_MESSAGE: | SLACK_MESSAGE: |
:warning::warning::warning::warning: :warning::warning::warning::warning:
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!! WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
:warning::warning::warning::warning: :warning::warning::warning::warning:
SLACK_COLOR: danger SLACK_COLOR: danger
MSG_MINIMAL: true MSG_MINIMAL: true
uses: rtCamp/action-slack-notify@v2
- name: Get days remaining before certificate expiration date - name: Get days remaining before certificate expiration date
env: env:
@@ -99,7 +104,7 @@ jobs:
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))"
# Display the expiration information in the log # Display the expiration information in the log.
echo "Certificate expiration date: $EXPIRATION_DATE" echo "Certificate expiration date: $EXPIRATION_DATE"
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION" echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
@@ -114,14 +119,14 @@ jobs:
fi fi
- name: Slack notification of pending certificate expiration - name: Slack notification of pending certificate expiration
# Don't send spurious expiration notification if verification fails # Don't send spurious expiration notification if verification fails.
if: failure() && steps.check-expiration.outcome == 'failure' if: failure() && steps.check-expiration.outcome == 'failure'
uses: rtCamp/action-slack-notify@v2.1.0
env: env:
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_MESSAGE: | SLACK_MESSAGE: |
:warning::warning::warning::warning: :warning::warning::warning::warning:
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!! WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
:warning::warning::warning::warning: :warning::warning::warning::warning:
SLACK_COLOR: danger SLACK_COLOR: danger
MSG_MINIMAL: true MSG_MINIMAL: true
uses: rtCamp/action-slack-notify@v2

View File

@@ -1,13 +1,6 @@
{ {
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"overrides": [ "printWidth": 80
{
"files": "*.{json,yml}",
"options": {
"tabWidth": 2
}
}
]
} }

View File

@@ -14,7 +14,7 @@ The _Electron main_ process is responsible for:
- managing the application lifecycle via listeners, and - managing the application lifecycle via listeners, and
- creating and managing the web pages for the app. - 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. In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly one main process.
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup> By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>

View File

@@ -58,3 +58,13 @@ The Config Service knows about your system, like for example the default sketch
#### Rebuild gRPC protocol interfaces #### Rebuild gRPC protocol interfaces
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command - Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol` `yarn --cwd arduino-ide-extension generate-protocol`
### Customize Icons
ArduinoIde uses a customized version of FontAwesome.
In order to update/replace icons follow the following steps:
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
- load it
- edit the icons as needed
- !! download the **new** `arduino-icons.json` file and put it in this repo
- Click on "Generate Font" in Icomoon, then download
- place the updated fonts in the `src/style/fonts` directory

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,13 @@
{ {
"name": "arduino-ide-extension", "name": "arduino-ide-extension",
"version": "2.0.0-beta.8", "version": "2.0.0-beta.11",
"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": {
"prepare": "yarn download-cli && yarn download-ls && yarn clean && yarn download-examples && yarn build", "prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn clean && yarn download-examples && yarn build",
"clean": "rimraf lib", "clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js", "download-cli": "node ./scripts/download-cli.js",
"download-fwuploader": "node ./scripts/download-fwuploader.js",
"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",
@@ -28,11 +29,12 @@
"@theia/monaco": "next", "@theia/monaco": "next",
"@theia/navigator": "next", "@theia/navigator": "next",
"@theia/outline-view": "next", "@theia/outline-view": "next",
"@theia/preferences": "next",
"@theia/output": "next", "@theia/output": "next",
"@theia/preferences": "next",
"@theia/search-in-workspace": "next", "@theia/search-in-workspace": "next",
"@theia/terminal": "next", "@theia/terminal": "next",
"@theia/workspace": "next", "@theia/workspace": "next",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2", "@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0", "@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3", "@types/btoa": "^1.2.3",
@@ -137,6 +139,9 @@
"arduino": { "arduino": {
"cli": { "cli": {
"version": "0.18.3" "version": "0.18.3"
},
"fwuploader": {
"version": "2.0.0"
} }
} }
} }

View File

@@ -0,0 +1,166 @@
// @ts-check
(async () => {
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
const semver = require('semver');
const downloader = require('./downloader');
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) {
return undefined;
}
const { arduino } = pkg;
if (!arduino) {
return undefined;
}
const { fwuploader } = arduino;
if (!fwuploader) {
return undefined;
}
const { version } = fwuploader;
return version;
})();
if (!version) {
shell.echo(
`Could not retrieve Firmware Uploader version info from the 'package.json'.`
);
shell.exit(1);
}
const { platform, arch } = process;
const buildFolder = path.join(__dirname, '..', 'build');
const fwuploderName = `arduino-fwuploader${
platform === 'win32' ? '.exe' : ''
}`;
const destinationPath = path.join(buildFolder, fwuploderName);
if (typeof version === 'string') {
const suffix = (() => {
switch (platform) {
case 'darwin':
return 'macOS_64bit.tar.gz';
case 'win32':
return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm':
return 'Linux_ARMv7.tar.gz';
case 'arm64':
return 'Linux_ARM64.tar.gz';
case 'x64':
return 'Linux_64bit.tar.gz';
default:
return undefined;
}
}
default:
return undefined;
}
})();
if (!suffix) {
shell.echo(
`The Firmware Uploader is not available for ${platform} ${arch}.`
);
shell.exit(1);
}
if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-fwuploader/arduino-fwuploader_${version}_${suffix}`;
shell.echo(
`📦 Identified released version of the Firmware Uploader. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(
url,
destinationPath,
'arduino-fwuploader'
);
} else {
shell.echo(`🔥 Could not interpret 'version': ${version}`);
shell.exit(1);
}
} else {
// We assume an object with `owner`, `repo`, commitish?` properties.
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(
`Building Firmware Uploader from ${url}. Commitish: ${
commitish ? commitish : 'HEAD'
}`
);
if (fs.existsSync(destinationPath)) {
shell.echo(
`Skipping the Firmware Uploader build because it already exists: ${destinationPath}`
);
return;
}
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning Firmware Uploader source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Cloned Firmware Uploader repo.');
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (
shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0
) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the Firmware Uploader...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Firmware Uploader build done.');
if (!fs.existsSync(path.join(tempRepoPath, fwuploderName))) {
shell.echo(
`Could not find the Firmware Uploader at ${path.join(
tempRepoPath,
fwuploderName
)}.`
);
shell.exit(1);
}
const builtFwUploaderPath = path.join(tempRepoPath, fwuploderName);
shell.echo(
`>>> Copying Firmware Uploader from ${builtFwUploaderPath} to ${destinationPath}...`
);
if (shell.cp(builtFwUploaderPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the Firmware Uploader.`);
shell.echo('<<< Verifying Firmware Uploader...');
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo('>>> Verified Firmware Uploader.');
}
})();

View File

@@ -4,18 +4,18 @@ import { Command } from '@theia/core/lib/common/command';
* @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions. * @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions.
*/ */
export namespace ArduinoCommands { export namespace ArduinoCommands {
export const TOGGLE_COMPILE_FOR_DEBUG: Command = { export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
id: 'arduino-toggle-compile-for-debug', id: 'arduino-toggle-compile-for-debug',
}; };
/** /**
* Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...) * Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...)
*/ */
export const OPEN_SKETCH_FILES: Command = { export const OPEN_SKETCH_FILES: Command = {
id: 'arduino-open-sketch-files', id: 'arduino-open-sketch-files',
}; };
export const OPEN_BOARDS_DIALOG: Command = { export const OPEN_BOARDS_DIALOG: Command = {
id: 'arduino-open-boards-dialog', id: 'arduino-open-boards-dialog',
}; };
} }

View File

@@ -1,18 +1,38 @@
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger, DisposableCollection } from '@theia/core';
import { import {
ContextMenuRenderer, MAIN_MENU_BAR,
FrontendApplication, FrontendApplicationContribution, MenuContribution,
OpenerService, StatusBar, StatusBarAlignment MenuModelRegistry,
SelectionService,
ILogger,
DisposableCollection,
} from '@theia/core';
import {
ContextMenuRenderer,
FrontendApplication,
FrontendApplicationContribution,
OpenerService,
StatusBar,
StatusBarAlignment,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
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 { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import {
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { EditorMainMenu, EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser'; import {
EditorMainMenu,
EditorManager,
EditorOpenerOptions,
} from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution'; import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
@@ -26,7 +46,14 @@ import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react'; import * as React from 'react';
import { remote } from 'electron'; import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager'; import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, CoreService, Port, SketchesService, ExecutableService, Sketch } from '../common/protocol'; import {
BoardsService,
CoreService,
Port,
SketchesService,
ExecutableService,
Sketch,
} from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon'; import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext'; import { FileSystemExt } from '../common/protocol/filesystem-ext';
@@ -53,434 +80,514 @@ import { FrontendApplicationStateService } from '@theia/core/lib/browser/fronten
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution'; import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
@injectable() @injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution, export class ArduinoFrontendContribution
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution { implements
FrontendApplicationContribution,
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution
{
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger) @inject(MessageService)
protected logger: ILogger; protected readonly messageService: MessageService;
@inject(MessageService) @inject(BoardsService)
protected readonly messageService: MessageService; protected readonly boardsService: BoardsService;
@inject(BoardsService) @inject(CoreService)
protected readonly boardsService: BoardsService; protected readonly coreService: CoreService;
@inject(CoreService) @inject(BoardsServiceProvider)
protected readonly coreService: CoreService; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(BoardsServiceProvider) @inject(SelectionService)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly selectionService: SelectionService;
@inject(SelectionService) @inject(EditorManager)
protected readonly selectionService: SelectionService; protected readonly editorManager: EditorManager;
@inject(EditorManager) @inject(ContextMenuRenderer)
protected readonly editorManager: EditorManager; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(ContextMenuRenderer) @inject(FileDialogService)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly fileDialogService: FileDialogService;
@inject(FileDialogService) @inject(FileService)
protected readonly fileDialogService: FileDialogService; protected readonly fileService: FileService;
@inject(FileService) @inject(SketchesService)
protected readonly fileService: FileService; protected readonly sketchService: SketchesService;
@inject(SketchesService) @inject(BoardsConfigDialog)
protected readonly sketchService: SketchesService; protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(BoardsConfigDialog) @inject(MenuModelRegistry)
protected readonly boardsConfigDialog: BoardsConfigDialog; protected readonly menuRegistry: MenuModelRegistry;
@inject(MenuModelRegistry) @inject(CommandRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(CommandRegistry) @inject(StatusBar)
protected readonly commandRegistry: CommandRegistry; protected readonly statusBar: StatusBar;
@inject(StatusBar) @inject(WorkspaceService)
protected readonly statusBar: StatusBar; protected readonly workspaceService: WorkspaceService;
@inject(WorkspaceService) @inject(MonitorConnection)
protected readonly workspaceService: WorkspaceService; protected readonly monitorConnection: MonitorConnection;
@inject(MonitorConnection) @inject(FileNavigatorContribution)
protected readonly monitorConnection: MonitorConnection; protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(FileNavigatorContribution) @inject(OutputContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution; protected readonly outputContribution: OutputContribution;
@inject(OutputContribution) @inject(OutlineViewContribution)
protected readonly outputContribution: OutputContribution; protected readonly outlineContribution: OutlineViewContribution;
@inject(OutlineViewContribution) @inject(ProblemContribution)
protected readonly outlineContribution: OutlineViewContribution; protected readonly problemContribution: ProblemContribution;
@inject(ProblemContribution) @inject(ScmContribution)
protected readonly problemContribution: ProblemContribution; protected readonly scmContribution: ScmContribution;
@inject(ScmContribution) @inject(SearchInWorkspaceFrontendContribution)
protected readonly scmContribution: ScmContribution; protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SearchInWorkspaceFrontendContribution) @inject(SketchbookWidgetContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution; protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
@inject(SketchbookWidgetContribution) @inject(EditorMode)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution; protected readonly editorMode: EditorMode;
@inject(EditorMode) @inject(ArduinoDaemon)
protected readonly editorMode: EditorMode; protected readonly daemon: ArduinoDaemon;
@inject(ArduinoDaemon) @inject(OpenerService)
protected readonly daemon: ArduinoDaemon; protected readonly openerService: OpenerService;
@inject(OpenerService) @inject(ConfigService)
protected readonly openerService: OpenerService; protected readonly configService: ConfigService;
@inject(ConfigService) @inject(BoardsDataStore)
protected readonly configService: ConfigService; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsDataStore) @inject(MainMenuManager)
protected readonly boardsDataStore: BoardsDataStore; protected readonly mainMenuManager: MainMenuManager;
@inject(MainMenuManager) @inject(FileSystemExt)
protected readonly mainMenuManager: MainMenuManager; protected readonly fileSystemExt: FileSystemExt;
@inject(FileSystemExt) @inject(HostedPluginSupport)
protected readonly fileSystemExt: FileSystemExt; protected hostedPluginSupport: HostedPluginSupport;
@inject(HostedPluginSupport) @inject(ExecutableService)
protected hostedPluginSupport: HostedPluginSupport; protected executableService: ExecutableService;
@inject(ExecutableService) @inject(ResponseService)
protected executableService: ExecutableService; protected readonly responseService: ResponseService;
@inject(ResponseService) @inject(ArduinoPreferences)
protected readonly responseService: ResponseService; protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoPreferences) @inject(SketchesServiceClientImpl)
protected readonly arduinoPreferences: ArduinoPreferences; protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(SketchesServiceClientImpl) @inject(FrontendApplicationStateService)
protected readonly sketchServiceClient: SketchesServiceClientImpl; protected readonly appStateService: FrontendApplicationStateService;
@inject(FrontendApplicationStateService) protected invalidConfigPopup:
protected readonly appStateService: FrontendApplicationStateService; | Promise<void | 'No' | 'Yes' | undefined>
| undefined;
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined; protected toDisposeOnStop = new DisposableCollection();
protected toDisposeOnStop = new DisposableCollection();
@postConstruct()
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.');
}
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
className: 'arduino-selected-board'
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
className: 'arduino-selected-port'
});
}
}
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && (!await this.sketchService.isTemp(sketch))) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(this.fileService.onDidFilesChange(async event => {
for (const { type, resource } of event.changes) {
if (type === FileChangeType.ADDED && resource.parent.toString() === sketch.uri) {
const reloadedSketch = await this.sketchService.loadSketch(sketch.uri)
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.ensureOpened(resource.toString(), true, { mode: 'open' });
}
}
}
}));
}
});
@postConstruct()
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn(
'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'
);
} }
const updateStatusBar = ({
onStart(app: FrontendApplication): void { selectedBoard,
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode. selectedPort,
for (const viewContribution of [ }: BoardsConfig.Config) => {
this.fileNavigatorContributions, this.statusBar.setElement('arduino-selected-board', {
this.outputContribution, alignment: StatusBarAlignment.RIGHT,
this.outlineContribution, text: selectedBoard
this.problemContribution, ? `$(microchip) ${selectedBoard.name}`
this.scmContribution, : '$(close) no board selected',
this.siwContribution, className: 'arduino-selected-board',
this.sketchbookWidgetContribution] as Array<FrontendApplicationContribution>) { });
if (viewContribution.initializeLayout) { if (selectedBoard) {
viewContribution.initializeLayout(app); this.statusBar.setElement('arduino-selected-port', {
} alignment: StatusBarAlignment.RIGHT,
} text: selectedPort
const start = async ({ selectedBoard }: BoardsConfig.Config) => { ? `on ${Port.toString(selectedPort)}`
if (selectedBoard) { : '[not connected]',
const { name, fqbn } = selectedBoard; className: 'arduino-selected-port',
if (fqbn) { });
this.startLanguageServer(fqbn, name); }
};
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && !(await this.sketchService.isTemp(sketch))) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
if (
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch = await this.sketchService.loadSketch(
sketch.uri
);
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.ensureOpened(resource.toString(), true, {
mode: 'open',
});
} }
}
} }
}; })
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
app.shell.leftPanelHandler.removeMenu('settings-menu');
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
protected languageServerFqbn?: string;
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(`Could not start language server for ${fqbn}. The core is not installed for the board.`);
if (this.languageServerFqbn) {
try {
await this.commandRegistry.executeCommand('arduino.languageserver.stop');
console.info(`Stopped language server process for ${this.languageServerFqbn}.`);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(`Failed to start language server process for ${this.languageServerFqbn}`, e);
throw e;
}
}
return;
}
if (fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileService.fsPath(new URI(currentSketch.uri));
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(new URI(await this.configService.getCliConfigFileUri()))
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${20_000} ms.`)), 20_000)),
this.commandRegistry.executeCommand<string>('arduino.languageserver.start', {
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
board: {
fqbn,
name: name ? `"${name}"` : undefined
}
})
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor'
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
}
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
}
});
}
registerMenus(registry: MenuModelRegistry) {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '4'
});
}
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService.info(message, 'Later', 'Yes').then(async answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false });
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
protected async ensureOpened(uri: string, forceOpen = false, options?: EditorOpenerOptions | undefined): Promise<any> {
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
}
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background'
},
description: 'The primary branding color, such as dialog titles, library, and board manager list labels.'
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background'
},
description: 'Secondary branding color for list selections, dropdowns, and widget borders.'
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hc: 'editorWidget.background'
},
description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.'
},
{
id: 'arduino.toolbar.background',
defaults: {
dark: 'button.background',
light: 'button.background',
hc: 'activityBar.inactiveForeground'
},
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
},
{
id: 'arduino.toolbar.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.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: {
dark: 'editor.foreground',
light: 'editor.foreground',
hc: 'editor.foreground'
},
description: 'Color of the text in the Output view.'
},
{
id: 'arduino.output.background',
defaults: {
dark: 'editor.background',
light: 'editor.background',
hc: 'editor.background'
},
description: 'Background color of the Output view.'
}
); );
} }
});
}
onStart(app: FrontendApplication): void {
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution,
this.sketchbookWidgetContribution,
] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
}
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
this.startLanguageServer(fqbn, name);
}
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.language.log' &&
event.newValue !== event.oldValue
) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.window.zoomLevel' &&
typeof event.newValue === 'number' &&
event.newValue !== event.oldValue
) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
app.shell.leftPanelHandler.removeMenu('settings-menu');
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
protected languageServerFqbn?: string;
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(
fqbn: string,
name: string | undefined
): Promise<void> {
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(
`Could not start language server for ${fqbn}. The core is not installed for the board.`
);
if (this.languageServerFqbn) {
try {
await this.commandRegistry.executeCommand(
'arduino.languageserver.stop'
);
console.info(
`Stopped language server process for ${this.languageServerFqbn}.`
);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(
`Failed to start language server process for ${this.languageServerFqbn}`,
e
);
throw e;
}
}
return;
}
if (fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(
new URI(await this.configService.getCliConfigFileUri())
),
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandRegistry.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
board: {
fqbn,
name: name ? `"${name}"` : undefined,
},
}
),
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => (
<BoardsToolBarItem
key="boardsToolbarItem"
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl}
/>
),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7,
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor',
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug,
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
},
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
},
});
}
registerMenus(registry: MenuModelRegistry) {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
};
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '4',
});
}
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService
.info(message, 'Later', 'Yes')
.then(async (answer) => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
protected async ensureOpened(
uri: string,
forceOpen = false,
options?: EditorOpenerOptions | undefined
): Promise<any> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
}
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
},
description:
'The primary branding color, such as dialog titles, library, and board manager list labels.',
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
},
description:
'Secondary branding color for list selections, dropdowns, and widget borders.',
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hc: 'editorWidget.background',
},
description:
'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.',
},
{
id: 'arduino.toolbar.background',
defaults: {
dark: 'button.background',
light: 'button.background',
hc: 'activityBar.inactiveForeground',
},
description:
'Background color of the toolbar items. Such as Upload, Verify, etc.',
},
{
id: 'arduino.toolbar.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.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: {
dark: 'editor.foreground',
light: 'editor.foreground',
hc: 'editor.foreground',
},
description: 'Color of the text in the Output view.',
},
{
id: 'arduino.output.background',
defaults: {
dark: 'editor.background',
light: 'editor.background',
hc: 'editor.background',
},
description: 'Background color of the Output view.',
}
);
}
} }

View File

@@ -1,156 +1,161 @@
import { interfaces } from 'inversify'; import { interfaces } from 'inversify';
import { import {
createPreferenceProxy, createPreferenceProxy,
PreferenceProxy, PreferenceProxy,
PreferenceService, PreferenceService,
PreferenceContribution, PreferenceContribution,
PreferenceSchema, PreferenceSchema,
} from '@theia/core/lib/browser/preferences'; } from '@theia/core/lib/browser/preferences';
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol'; import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
export const ArduinoConfigSchema: PreferenceSchema = { export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object', type: 'object',
properties: { properties: {
'arduino.language.log': { 'arduino.language.log': {
type: 'boolean', type: 'boolean',
description: description:
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.", "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
default: false, default: false,
},
'arduino.compile.verbose': {
type: 'boolean',
description: 'True for verbose compile output. False by default',
default: false,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description:
"Tells gcc which warning level to use. It's 'None' by default",
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: 'True for verbose upload output. False by default.',
default: false,
},
'arduino.upload.verify': {
type: 'boolean',
default: false,
},
'arduino.window.autoScale': {
type: 'boolean',
description:
'True if the user interface automatically scales with the font size.',
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description:
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
default: 0,
},
'arduino.ide.autoUpdate': {
type: 'boolean',
description:
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true,
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description:
'True to show all sketch files inside the sketch. It is false by default.',
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description:
'True if the sketch sync functions are enabled. Defaults to true.',
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description:
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
type: 'string',
description:
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: 'The OAuth2 client ID.',
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: 'The OAuth2 domain.',
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: 'The 0Auth2 audience.',
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: 'The URI used to register a new user.',
default: 'https://auth.arduino.cc/login#/register',
},
}, },
'arduino.compile.verbose': {
type: 'boolean',
description: 'True for verbose compile output. False by default',
default: false,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description:
"Tells gcc which warning level to use. It's 'None' by default",
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: 'True for verbose upload output. False by default.',
default: false,
},
'arduino.upload.verify': {
type: 'boolean',
default: false,
},
'arduino.window.autoScale': {
type: 'boolean',
description:
'True if the user interface automatically scales with the font size.',
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description:
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
default: 0,
},
'arduino.ide.autoUpdate': {
type: 'boolean',
description:
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true,
},
'arduino.board.certificates': {
type: 'string',
description: 'List of certificates that can be uploaded to boards',
default: '',
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description:
'True to show all sketch files inside the sketch. It is false by default.',
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description:
'True if the sketch sync functions are enabled. Defaults to true.',
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description:
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
type: 'string',
description:
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: 'The OAuth2 client ID.',
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: 'The OAuth2 domain.',
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: 'The 0Auth2 audience.',
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: 'The URI used to register a new user.',
default: 'https://auth.arduino.cc/login#/register',
},
},
}; };
export interface ArduinoConfiguration { export interface ArduinoConfiguration {
'arduino.language.log': boolean; 'arduino.language.log': boolean;
'arduino.compile.verbose': boolean; 'arduino.compile.verbose': boolean;
'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.window.autoScale': boolean; 'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number; 'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean; 'arduino.ide.autoUpdate': boolean;
'arduino.sketchbook.showAllFiles': boolean; 'arduino.board.certificates': string;
'arduino.cloud.enabled': boolean; 'arduino.sketchbook.showAllFiles': boolean;
'arduino.cloud.pull.warn': boolean; 'arduino.cloud.enabled': boolean;
'arduino.cloud.push.warn': boolean; 'arduino.cloud.pull.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean; 'arduino.cloud.push.warn': boolean;
'arduino.cloud.sketchSyncEnpoint': string; 'arduino.cloud.pushpublic.warn': boolean;
'arduino.auth.clientID': string; 'arduino.cloud.sketchSyncEnpoint': string;
'arduino.auth.domain': string; 'arduino.auth.clientID': string;
'arduino.auth.audience': string; 'arduino.auth.domain': string;
'arduino.auth.registerUri': string; 'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
} }
export const ArduinoPreferences = Symbol('ArduinoPreferences'); export const ArduinoPreferences = Symbol('ArduinoPreferences');
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>; export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
export function createArduinoPreferences( export function createArduinoPreferences(
preferences: PreferenceService preferences: PreferenceService
): ArduinoPreferences { ): ArduinoPreferences {
return createPreferenceProxy(preferences, ArduinoConfigSchema); return createPreferenceProxy(preferences, ArduinoConfigSchema);
} }
export function bindArduinoPreferences(bind: interfaces.Bind): void { export function bindArduinoPreferences(bind: interfaces.Bind): void {
bind(ArduinoPreferences).toDynamicValue((ctx) => { bind(ArduinoPreferences).toDynamicValue((ctx) => {
const preferences = const preferences = ctx.container.get<PreferenceService>(PreferenceService);
ctx.container.get<PreferenceService>(PreferenceService); return createArduinoPreferences(preferences);
return createArduinoPreferences(preferences); });
}); bind(PreferenceContribution).toConstantValue({
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema,
schema: ArduinoConfigSchema, });
});
} }

View File

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

View File

@@ -5,13 +5,13 @@ 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'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
CommandRegistry, CommandRegistry,
CommandContribution, CommandContribution,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
import { import {
AuthenticationService, AuthenticationService,
AuthenticationServiceClient, AuthenticationServiceClient,
AuthenticationSession, AuthenticationSession,
} from '../../common/protocol/authentication-service'; } from '../../common/protocol/authentication-service';
import { CloudUserCommands } from './cloud-user-commands'; import { CloudUserCommands } from './cloud-user-commands';
import { serverPort } from '../../node/auth/authentication-server'; import { serverPort } from '../../node/auth/authentication-server';
@@ -20,74 +20,74 @@ import { ArduinoPreferences } from '../arduino-preferences';
@injectable() @injectable()
export class AuthenticationClientService export class AuthenticationClientService
implements implements
FrontendApplicationContribution, FrontendApplicationContribution,
CommandContribution, CommandContribution,
AuthenticationServiceClient AuthenticationServiceClient
{ {
@inject(AuthenticationService) @inject(AuthenticationService)
protected readonly service: JsonRpcProxy<AuthenticationService>; protected readonly service: JsonRpcProxy<AuthenticationService>;
@inject(WindowService) @inject(WindowService)
protected readonly windowService: WindowService; protected readonly windowService: WindowService;
@inject(ArduinoPreferences) @inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences; protected readonly arduinoPreferences: ArduinoPreferences;
protected authOptions: AuthOptions; protected authOptions: AuthOptions;
protected _session: AuthenticationSession | undefined; protected _session: AuthenticationSession | undefined;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
protected readonly onSessionDidChangeEmitter = new Emitter< protected readonly onSessionDidChangeEmitter = new Emitter<
AuthenticationSession | undefined AuthenticationSession | undefined
>(); >();
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event; readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
onStart(): void { onStart(): void {
this.toDispose.push(this.onSessionDidChangeEmitter); this.toDispose.push(this.onSessionDidChangeEmitter);
this.service.setClient(this); this.service.setClient(this);
this.service this.service
.session() .session()
.then((session) => this.notifySessionDidChange(session)); .then((session) => this.notifySessionDidChange(session));
this.setOptions();
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.preferenceName.startsWith('arduino.auth.')) {
this.setOptions(); this.setOptions();
this.arduinoPreferences.onPreferenceChanged((event) => { }
if (event.preferenceName.startsWith('arduino.auth.')) { });
this.setOptions(); }
}
});
}
setOptions(): void { setOptions(): void {
this.service.setOptions({ this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`, redirectUri: `http://localhost:${serverPort}/callback`,
responseType: 'code', responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'], clientID: this.arduinoPreferences['arduino.auth.clientID'],
domain: this.arduinoPreferences['arduino.auth.domain'], domain: this.arduinoPreferences['arduino.auth.domain'],
audience: this.arduinoPreferences['arduino.auth.audience'], audience: this.arduinoPreferences['arduino.auth.audience'],
registerUri: this.arduinoPreferences['arduino.auth.registerUri'], registerUri: this.arduinoPreferences['arduino.auth.registerUri'],
scopes: ['openid', 'profile', 'email', 'offline_access'], scopes: ['openid', 'profile', 'email', 'offline_access'],
}); });
} }
protected updateSession(session?: AuthenticationSession | undefined) { protected updateSession(session?: AuthenticationSession | undefined) {
this._session = session; this._session = session;
this.onSessionDidChangeEmitter.fire(this._session); this.onSessionDidChangeEmitter.fire(this._session);
} }
get session(): AuthenticationSession | undefined { get session(): AuthenticationSession | undefined {
return this._session; return this._session;
} }
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudUserCommands.LOGIN, { registry.registerCommand(CloudUserCommands.LOGIN, {
execute: () => this.service.login(), execute: () => this.service.login(),
}); });
registry.registerCommand(CloudUserCommands.LOGOUT, { registry.registerCommand(CloudUserCommands.LOGOUT, {
execute: () => this.service.logout(), execute: () => this.service.logout(),
}); });
} }
notifySessionDidChange(session: AuthenticationSession | undefined): void { notifySessionDidChange(session: AuthenticationSession | undefined): void {
this.updateSession(session); this.updateSession(session);
} }
} }

View File

@@ -1,18 +1,18 @@
import { Command } from '@theia/core/lib/common/command'; import { Command } from '@theia/core/lib/common/command';
export namespace CloudUserCommands { export namespace CloudUserCommands {
export const LOGIN: Command = { export const LOGIN: Command = {
id: 'arduino-cloud--login', id: 'arduino-cloud--login',
label: 'Sign in', label: 'Sign in',
}; };
export const LOGOUT: Command = { export const LOGOUT: Command = {
id: 'arduino-cloud--logout', id: 'arduino-cloud--logout',
label: 'Sign Out', label: 'Sign Out',
}; };
export const OPEN_PROFILE_CONTEXT_MENU: Command = { export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu', id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu', label: 'Contextual menu',
}; };
} }

View File

@@ -2,9 +2,9 @@ import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
BoardsService, BoardsService,
BoardsPackage, BoardsPackage,
Board, Board,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
@@ -18,111 +18,103 @@ import { ResponseServiceImpl } from '../response-service-impl';
*/ */
@injectable() @injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution { export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceImpl) @inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl; protected readonly responseService: ResponseServiceImpl;
@inject(BoardsListWidgetFrontendContribution) @inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
// Workaround for https://github.com/eclipse-theia/theia/issues/9349 // Workaround for https://github.com/eclipse-theia/theia/issues/9349
protected notifications: Board[] = []; protected notifications: Board[] = [];
onStart(): void { onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged( this.boardsServiceClient.onBoardsConfigChanged(
this.ensureCoreExists.bind(this) this.ensureCoreExists.bind(this)
);
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (
selectedBoard &&
!this.notifications.find((board) => Board.sameAs(board, selectedBoard))
) {
this.notifications.push(selectedBoard);
this.boardsService.search({}).then((packages) => {
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some((board) => board.name === selectedBoard.name)
); );
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void { // check if one of the packages for the board is already installed. if so, no hint
const { selectedBoard } = config;
if ( if (
selectedBoard && packagesForBoard.some(({ installedVersion }) => !!installedVersion)
!this.notifications.find((board) =>
Board.sameAs(board, selectedBoard)
)
) { ) {
this.notifications.push(selectedBoard); return;
this.boardsService.search({}).then((packages) => { }
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some(
(board) => board.name === selectedBoard.name
)
);
// check if one of the packages for the board is already installed. if so, no hint // filter the installable (not installed) packages,
if ( // CLI returns the packages already sorted with the deprecated ones at the end of the list
packagesForBoard.some( // in order to ensure the new ones are preferred
({ installedVersion }) => !!installedVersion const candidates = packagesForBoard.filter(
) ({ installable, installedVersion }) =>
) { installable && !installedVersion
return; );
}
// filter the installable (not installed) packages, const candidate = candidates[0];
// CLI returns the packages already sorted with the deprecated ones at the end of the list if (candidate) {
// in order to ensure the new ones are preferred const version = candidate.availableVersions[0]
const candidates = packagesForBoard.filter( ? `[v ${candidate.availableVersions[0]}]`
({ installable, installedVersion }) => : '';
installable && !installedVersion // tslint:disable-next-line:max-line-length
); this.messageService
.info(
const candidate = candidates[0]; `The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`,
if (candidate) { 'Install Manually',
const version = candidate.availableVersions[0] 'Yes'
? `[v ${candidate.availableVersions[0]}]` )
: ''; .then(async (answer) => {
// tslint:disable-next-line:max-line-length const index = this.notifications.findIndex((board) =>
this.messageService Board.sameAs(board, selectedBoard)
.info( );
`The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, if (index !== -1) {
'Install Manually', this.notifications.splice(index, 1);
'Yes' }
) if (answer === 'Yes') {
.then(async (answer) => { await Installable.installWithProgress({
const index = this.notifications.findIndex( installable: this.boardsService,
(board) => Board.sameAs(board, selectedBoard) item: candidate,
); messageService: this.messageService,
if (index !== -1) { responseService: this.responseService,
this.notifications.splice(index, 1); version: candidate.availableVersions[0],
} });
if (answer === 'Yes') { return;
await Installable.installWithProgress({ }
installable: this.boardsService, if (answer) {
item: candidate, this.boardsManagerFrontendContribution
messageService: this.messageService, .openView({ reveal: true })
responseService: this.responseService, .then((widget) =>
version: candidate.availableVersions[0], widget.refresh(candidate.name.toLocaleLowerCase())
}); );
return; }
}
if (answer) {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh(
candidate.name.toLocaleLowerCase()
)
);
}
});
}
}); });
} }
});
} }
}
} }

View File

@@ -9,64 +9,62 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class BoardsConfigDialogWidget extends ReactWidget { export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>(); protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
protected readonly onBoardConfigChangedEmitter = protected readonly onBoardConfigChangedEmitter =
new Emitter<BoardsConfig.Config>(); new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event; readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined; protected focusNode: HTMLElement | undefined;
constructor() { constructor() {
super(); super();
this.id = 'select-board-dialog'; this.id = 'select-board-dialog';
this.toDispose.pushAll([ this.toDispose.pushAll([
this.onBoardConfigChangedEmitter, this.onBoardConfigChangedEmitter,
this.onFilterTextDidChangeEmitter, this.onFilterTextDidChangeEmitter,
]); ]);
} }
search(query: string): void { search(query: string): void {
this.onFilterTextDidChangeEmitter.fire(query); this.onFilterTextDidChangeEmitter.fire(query);
} }
protected fireConfigChanged = (config: BoardsConfig.Config) => { protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config); this.onBoardConfigChangedEmitter.fire(config);
}; };
protected setFocusNode = (element: HTMLElement | undefined) => { protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element; this.focusNode = element;
}; };
protected render(): React.ReactNode { protected render(): React.ReactNode {
return ( return (
<div className="selectBoardContainer"> <div className="selectBoardContainer">
<BoardsConfig <BoardsConfig
boardsServiceProvider={this.boardsServiceClient} boardsServiceProvider={this.boardsServiceClient}
notificationCenter={this.notificationCenter} notificationCenter={this.notificationCenter}
onConfigChange={this.fireConfigChanged} onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={ onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
this.onFilterTextDidChangeEmitter.event />
} </div>
/> );
</div> }
);
} protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
protected onActivateRequest(msg: Message): void { if (this.focusNode instanceof HTMLInputElement) {
super.onActivateRequest(msg); this.focusNode.select();
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
} }
(this.focusNode || this.node).focus();
}
} }

View File

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

View File

@@ -4,408 +4,396 @@ import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { import {
Board, Board,
Port, Port,
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
BoardWithPackage, BoardWithPackage,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
export namespace BoardsConfig { export namespace BoardsConfig {
export interface Config { export interface Config {
selectedBoard?: Board; selectedBoard?: Board;
selectedPort?: Port; selectedPort?: Port;
} }
export interface Props { export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider; readonly boardsServiceProvider: BoardsServiceProvider;
readonly notificationCenter: NotificationCenter; readonly notificationCenter: NotificationCenter;
readonly onConfigChange: (config: Config) => void; readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>; readonly onFilteredTextDidChangeEvent: Event<string>;
} }
export interface State extends Config { export interface State extends Config {
searchResults: Array<BoardWithPackage>; searchResults: Array<BoardWithPackage>;
knownPorts: Port[]; knownPorts: Port[];
showAllPorts: boolean; showAllPorts: boolean;
query: string; query: string;
} }
} }
export abstract class Item<T> extends React.Component<{ export abstract class Item<T> extends React.Component<{
item: T; item: T;
label: string; label: string;
selected: boolean; selected: boolean;
onClick: (item: T) => void; onClick: (item: T) => void;
missing?: boolean; missing?: boolean;
details?: string; details?: string;
}> { }> {
render(): React.ReactNode { render(): React.ReactNode {
const { selected, label, missing, details } = this.props; const { selected, label, missing, details } = this.props;
const classNames = ['item']; const classNames = ['item'];
if (selected) { if (selected) {
classNames.push('selected'); classNames.push('selected');
}
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
} }
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
}
protected onClick = () => { protected onClick = () => {
this.props.onClick(this.props.item); this.props.onClick(this.props.item);
}; };
} }
export class BoardsConfig extends React.Component< export class BoardsConfig extends React.Component<
BoardsConfig.Props, BoardsConfig.Props,
BoardsConfig.State BoardsConfig.State
> { > {
protected toDispose = new DisposableCollection(); protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) { constructor(props: BoardsConfig.Props) {
super(props); super(props);
const { boardsConfig } = props.boardsServiceProvider; const { boardsConfig } = props.boardsServiceProvider;
this.state = { this.state = {
searchResults: [], searchResults: [],
knownPorts: [], knownPorts: [],
showAllPorts: false, showAllPorts: false,
query: '', query: '',
...boardsConfig, ...boardsConfig,
}; };
} }
componentDidMount() { componentDidMount() {
this.updateBoards(); this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
this.toDispose.pushAll([
this.props.notificationCenter.onAttachedBoardsChanged((event) =>
this.updatePorts( this.updatePorts(
this.props.boardsServiceProvider.availableBoards event.newState.ports,
.map(({ port }) => port) AttachedBoardsChangeEvent.diff(event).detached.ports
.filter(notEmpty) )
); ),
this.toDispose.pushAll([ this.props.boardsServiceProvider.onBoardsConfigChanged(
this.props.notificationCenter.onAttachedBoardsChanged((event) => ({ selectedBoard, selectedPort }) => {
this.updatePorts( this.setState({ selectedBoard, selectedPort }, () =>
event.newState.ports, this.fireConfigChanged()
AttachedBoardsChangeEvent.diff(event).detached.ports );
)
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () =>
this.fireConfigChanged()
);
}
),
this.props.notificationCenter.onPlatformInstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformUninstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdated(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStarted(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStopped(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
this.setState({ query }, () =>
this.updateBoards(this.state.query)
)
),
]);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
) => {
const query =
typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then((searchResults) =>
this.setState({ searchResults })
);
};
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some((port) => Port.equals(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
this.fireConfigChanged()
);
});
};
protected queryBoards = (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
};
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty);
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
};
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
};
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
};
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
return (
<div className="body">
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</div>
);
}
protected renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">
{footerRenderer ? footerRenderer() : ''}
</div>
</div>
</div>
);
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn
? `${name}-${packageName}-${fqbn}`
: `${name}-${packageName}`;
for (const board of Board.decorateBoards(
selectedBoard,
searchResults
)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
} }
),
this.props.notificationCenter.onPlatformInstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformUninstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdated(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStarted(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStopped(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
this.setState({ query }, () => this.updateBoards(this.state.query))
),
]);
}
return ( componentWillUnmount(): void {
<React.Fragment> this.toDispose.dispose();
<div className="search"> }
<input
type="search" protected fireConfigChanged() {
value={query} const { selectedBoard, selectedPort } = this.state;
className="theia-input" this.props.onConfigChange({ selectedBoard, selectedPort });
placeholder="SEARCH BOARD" }
onChange={this.updateBoards}
ref={this.focusNodeSet} protected updateBoards = (
/> eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
<i className="fa fa-search"></i> ) => {
</div> const query =
<div className="boards list"> typeof eventOrQuery === 'string'
{Array.from(distinctBoards.values()).map((board) => ( ? eventOrQuery
<Item<BoardWithPackage> : eventOrQuery.target.value.toLowerCase();
key={`${board.name}-${board.packageName}`} this.setState({ query });
item={board} this.queryBoards({ query }).then((searchResults) =>
label={board.name} this.setState({ searchResults })
details={board.details} );
selected={board.selected} };
onClick={this.selectBoard}
missing={board.missing} protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
/> this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
))} let { selectedPort } = this.state;
</div> // If the currently selected port is not available anymore, unset the selected port.
</React.Fragment> if (removedPorts.some((port) => Port.equals(port, selectedPort))) {
); selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
this.fireConfigChanged()
);
});
};
protected queryBoards = (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
};
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty);
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
};
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
};
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
};
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
return (
<div className="body">
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</div>
);
}
protected renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
</div>
</div>
);
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
} }
protected renderPorts(): React.ReactNode { return (
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort; <React.Fragment>
const ports = this.state.knownPorts.filter(filter); <div className="search">
return !ports.length ? ( <input
<div className="loading noselect">No ports discovered</div> type="search"
) : ( value={query}
<div className="ports list"> className="theia-input"
{ports.map((port) => ( placeholder="SEARCH BOARD"
<Item<Port> onChange={this.updateBoards}
key={Port.toString(port)} ref={this.focusNodeSet}
item={port} />
label={Port.toString(port)} <i className="fa fa-search"></i>
selected={Port.equals(this.state.selectedPort, port)} </div>
onClick={this.selectPort} <div className="boards list">
/> {Array.from(distinctBoards.values()).map((board) => (
))} <Item<BoardWithPackage>
</div> key={`${board.name}-${board.packageName}`}
); item={board}
} label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
))}
</div>
</React.Fragment>
);
}
protected renderPortsFooter(): React.ReactNode { protected renderPorts(): React.ReactNode {
return ( const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
<div className="noselect"> const ports = this.state.knownPorts.filter(filter);
<label title="Shows all available ports when enabled"> return !ports.length ? (
<input <div className="loading noselect">No ports discovered</div>
type="checkbox" ) : (
defaultChecked={this.state.showAllPorts} <div className="ports list">
onChange={this.toggleFilterPorts} {ports.map((port) => (
/> <Item<Port>
<span>Show all ports</span> key={Port.toString(port)}
</label> item={port}
</div> label={Port.toString(port)}
); selected={Port.equals(this.state.selectedPort, port)}
} onClick={this.selectPort}
/>
))}
</div>
);
}
protected renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label title="Shows all available ports when enabled">
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
</label>
</div>
);
}
} }
export namespace BoardsConfig { export namespace BoardsConfig {
export namespace Config { export namespace Config {
export function sameAs(config: Config, other: Config | Board): boolean { export function sameAs(config: Config, other: Config | Board): boolean {
const { selectedBoard, selectedPort } = config; const { selectedBoard, selectedPort } = config;
if (Board.is(other)) { if (Board.is(other)) {
return ( return (
!!selectedBoard && !!selectedBoard &&
Board.equals(other, selectedBoard) && Board.equals(other, selectedBoard) &&
Port.sameAs(selectedPort, other.port) Port.sameAs(selectedPort, other.port)
); );
} }
return sameAs(config, other); return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return (
left.selectedBoard === right.selectedBoard &&
left.selectedPort === right.selectedPort
);
}
export function toString(
config: Config,
options: { default: string } = { default: '' }
): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set(
'boards-config',
encodeURIComponent(jsonConfig)
);
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
} }
export function equals(left: Config, right: Config): boolean {
return (
left.selectedBoard === right.selectedBoard &&
left.selectedPort === right.selectedPort
);
}
export function toString(
config: Config,
options: { default: string } = { default: '' }
): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
}
} }

View File

@@ -3,8 +3,8 @@ import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command'; 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 { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
import { Board, ConfigOption, Programmer } from '../../common/protocol'; import { Board, ConfigOption, Programmer } from '../../common/protocol';
@@ -15,178 +15,145 @@ import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
@injectable() @injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution { export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection(); protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> { async onStart(): Promise<void> {
this.updateMenuActions( this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.boardsServiceClient.boardsConfig.selectedBoard this.boardsDataStore.onChanged(() =>
); this.updateMenuActions(
this.boardsDataStore.onChanged(() => this.boardsServiceClient.boardsConfig.selectedBoard
this.updateMenuActions( )
this.boardsServiceClient.boardsConfig.selectedBoard );
) this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
); this.updateMenuActions(selectedBoard)
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => );
this.updateMenuActions(selectedBoard) }
);
}
protected async updateMenuActions( protected async updateMenuActions(
selectedBoard: Board | undefined selectedBoard: Board | undefined
): Promise<void> { ): Promise<void> {
return this.queue.add(async () => { return this.queue.add(async () => {
this.toDisposeOnBoardChange.dispose(); this.toDisposeOnBoardChange.dispose();
this.mainMenuManager.update(); this.mainMenuManager.update();
if (selectedBoard) { if (selectedBoard) {
const { fqbn } = selectedBoard; const { fqbn } = selectedBoard;
if (fqbn) { if (fqbn) {
const { configOptions, programmers, selectedProgrammer } = const { configOptions, programmers, selectedProgrammer } =
await this.boardsDataStore.getData(fqbn); await this.boardsDataStore.getData(fqbn);
if (configOptions.length) { if (configOptions.length) {
const boardsConfigMenuPath = [ const boardsConfigMenuPath = [
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, ...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
'z01_boardsConfig', 'z01_boardsConfig',
]; // `z_` is for ordering. ]; // `z_` is for ordering.
for (const { for (const { label, option, values } of configOptions.sort(
label, ConfigOption.LABEL_COMPARATOR
option, )) {
values, const menuPath = [...boardsConfigMenuPath, `${option}`];
} of configOptions.sort( const commands = new Map<
ConfigOption.LABEL_COMPARATOR string,
)) { Disposable & { label: string }
const menuPath = [ >();
...boardsConfigMenuPath, for (const value of values) {
`${option}`, const id = `${fqbn}-${option}--${value.value}`;
]; const command = { id };
const commands = new Map< const selectedValue = value.value;
string, const handler = {
Disposable & { label: string } execute: () =>
>(); this.boardsDataStore.selectConfigOption({
for (const value of values) { fqbn,
const id = `${fqbn}-${option}--${value.value}`; option,
const command = { id }; selectedValue,
const selectedValue = value.value; }),
const handler = { isToggled: () => value.selected,
execute: () => };
this.boardsDataStore.selectConfigOption( commands.set(
{ fqbn, option, selectedValue } id,
), Object.assign(
isToggled: () => value.selected, this.commandRegistry.registerCommand(command, handler),
}; { label: value.label }
commands.set( )
id, );
Object.assign( }
this.commandRegistry.registerCommand( this.menuRegistry.registerSubmenu(menuPath, label);
command, this.toDisposeOnBoardChange.pushAll([
handler ...commands.values(),
), Disposable.create(() =>
{ label: value.label } unregisterSubmenu(menuPath, this.menuRegistry)
) ),
); ...Array.from(commands.keys()).map((commandId, i) => {
} const { label } = commands.get(commandId)!;
this.menuRegistry.registerSubmenu(menuPath, label); this.menuRegistry.registerMenuAction(menuPath, {
this.toDisposeOnBoardChange.pushAll([ commandId,
...commands.values(), order: `${i}`,
Disposable.create(() => label,
unregisterSubmenu( });
menuPath, return Disposable.create(() =>
this.menuRegistry this.menuRegistry.unregisterMenuAction(commandId)
) );
), }),
...Array.from(commands.keys()).map( ]);
(commandId, i) => {
const { label } =
commands.get(commandId)!;
this.menuRegistry.registerMenuAction(
menuPath,
{ commandId, order: `${i}`, label }
);
return Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
commandId
)
);
}
),
]);
}
}
if (programmers.length) {
const programmersMenuPath = [
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
'z02_programmers',
];
const label = selectedProgrammer
? `Programmer: "${selectedProgrammer.name}"`
: 'Programmer';
this.menuRegistry.registerSubmenu(
programmersMenuPath,
label
);
this.toDisposeOnBoardChange.push(
Disposable.create(() =>
unregisterSubmenu(
programmersMenuPath,
this.menuRegistry
)
)
);
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
const handler = {
execute: () =>
this.boardsDataStore.selectProgrammer({
fqbn,
selectedProgrammer: programmer,
}),
isToggled: () =>
Programmer.equals(
programmer,
selectedProgrammer
),
};
this.menuRegistry.registerMenuAction(
programmersMenuPath,
{ commandId: command.id, label: name }
);
this.commandRegistry.registerCommand(
command,
handler
);
this.toDisposeOnBoardChange.pushAll([
Disposable.create(() =>
this.commandRegistry.unregisterCommand(
command
)
),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
command.id
)
),
]);
}
}
this.mainMenuManager.update();
}
} }
}); }
} if (programmers.length) {
const programmersMenuPath = [
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
'z02_programmers',
];
const label = selectedProgrammer
? `Programmer: "${selectedProgrammer.name}"`
: 'Programmer';
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(
Disposable.create(() =>
unregisterSubmenu(programmersMenuPath, this.menuRegistry)
)
);
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
const handler = {
execute: () =>
this.boardsDataStore.selectProgrammer({
fqbn,
selectedProgrammer: programmer,
}),
isToggled: () =>
Programmer.equals(programmer, selectedProgrammer),
};
this.menuRegistry.registerMenuAction(programmersMenuPath, {
commandId: command.id,
label: name,
});
this.commandRegistry.registerCommand(command, handler);
this.toDisposeOnBoardChange.pushAll([
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command.id)
),
]);
}
}
this.mainMenuManager.update();
}
}
});
}
} }

View File

@@ -4,277 +4,269 @@ import { deepClone } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event'; import { Event, Emitter } from '@theia/core/lib/common/event';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
LocalStorageService, LocalStorageService,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { notEmpty } from '../../common/utils'; import { notEmpty } from '../../common/utils';
import { import {
BoardsService, BoardsService,
ConfigOption, ConfigOption,
Installable, Installable,
BoardDetails, BoardDetails,
Programmer, Programmer,
} from '../../common/protocol'; } from '../../common/protocol';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class BoardsDataStore implements FrontendApplicationContribution { export class BoardsDataStore implements FrontendApplicationContribution {
@inject(ILogger) @inject(ILogger)
@named('store') @named('store')
protected readonly logger: ILogger; protected readonly logger: ILogger;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(LocalStorageService) @inject(LocalStorageService)
protected readonly storageService: LocalStorageService; protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<void>(); protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void { onStart(): void {
this.notificationCenter.onPlatformInstalled(async ({ item }) => { this.notificationCenter.onPlatformInstalled(async ({ item }) => {
const { installedVersion: version } = item; const { installedVersion: version } = item;
if (!version) { if (!version) {
return; return;
} }
let shouldFireChanged = false; let shouldFireChanged = false;
for (const fqbn of item.boards for (const fqbn of item.boards
.map(({ fqbn }) => fqbn) .map(({ fqbn }) => fqbn)
.filter(notEmpty) .filter(notEmpty)
.filter((fqbn) => !!fqbn)) { .filter((fqbn) => !!fqbn)) {
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<
ConfigOption[] | undefined
>(key);
if (!data || !data.length) {
const details = await this.getBoardDetailsSafe(fqbn);
if (details) {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
shouldFireChanged = true;
}
}
}
}
if (shouldFireChanged) {
this.fireChanged();
}
});
}
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
}
const { configOptions } = await this.getData(
fqbn,
boardsPackageVersion
);
return ConfigOption.decorate(fqbn, configOptions);
}
async getData(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<BoardsDataStore.Data> {
if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
}
const version = await boardsPackageVersion;
if (!version) {
return BoardsDataStore.Data.EMPTY;
}
const key = this.getStorageKey(fqbn, version); const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData< let data = await this.storageService.getData<
BoardsDataStore.Data | undefined ConfigOption[] | undefined
>(key, undefined); >(key);
if (BoardsDataStore.Data.is(data)) { if (!data || !data.length) {
return data; const details = await this.getBoardDetailsSafe(fqbn);
} if (details) {
data = details.configOptions;
const boardDetails = await this.getBoardDetailsSafe(fqbn); if (data.length) {
if (!boardDetails) { await this.storageService.setData(key, data);
return BoardsDataStore.Data.EMPTY; shouldFireChanged = true;
}
data = {
configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer(
{
fqbn,
selectedProgrammer,
}: { fqbn: string; selectedProgrammer: Programmer },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { programmers } = data;
if (
!programmers.find((p) => Programmer.equals(selectedProgrammer, p))
) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({
fqbn,
data: { ...data, selectedProgrammer },
version,
});
this.fireChanged();
return true;
}
async selectConfigOption(
{
fqbn,
option,
selectedValue,
}: { fqbn: string; option: string; selectedValue: string },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
} }
}
} }
if (!updated) { }
return false; if (shouldFireChanged) {
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data, version });
this.fireChanged(); this.fireChanged();
return true; }
});
}
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
} }
protected async setData({ const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
fqbn, return ConfigOption.decorate(fqbn, configOptions);
data, }
version,
}: { async getData(
fqbn: string; fqbn: string | undefined,
data: BoardsDataStore.Data; boardsPackageVersion: MaybePromise<
version: Installable.Version; Installable.Version | undefined
}): Promise<void> { > = this.getBoardsPackageVersion(fqbn)
const key = this.getStorageKey(fqbn, version); ): Promise<BoardsDataStore.Data> {
return this.storageService.setData(key, data); if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
} }
protected getStorageKey( const version = await boardsPackageVersion;
fqbn: string, if (!version) {
version: Installable.Version return BoardsDataStore.Data.EMPTY;
): string { }
return `.arduinoIDE-configOptions-${version}-${fqbn}`; const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<
BoardsDataStore.Data | undefined
>(key, undefined);
if (BoardsDataStore.Data.is(data)) {
return data;
} }
protected async getBoardDetailsSafe( const boardDetails = await this.getBoardDetailsSafe(fqbn);
fqbn: string if (!boardDetails) {
): Promise<BoardDetails | undefined> { return BoardsDataStore.Data.EMPTY;
try {
const details = this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (
err instanceof Error &&
err.message.includes('loading board data') &&
err.message.includes('is not installed')
) {
this.logger.warn(
`The boards package is not installed for board with FQBN: ${fqbn}`
);
} else {
this.logger.error(
`An unexpected error occurred while retrieving the board details for ${fqbn}.`,
err
);
}
return undefined;
}
} }
protected fireChanged(): void { data = {
this.onChangedEmitter.fire(); configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer(
{
fqbn,
selectedProgrammer,
}: { fqbn: string; selectedProgrammer: Programmer },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { programmers } = data;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
} }
protected async getBoardsPackageVersion( const version = await boardsPackageVersion;
fqbn: string | undefined if (!version) {
): Promise<Installable.Version | undefined> { return false;
if (!fqbn) { }
return undefined;
} await this.setData({
const boardsPackage = await this.boardsService.getContainerBoardPackage( fqbn,
{ fqbn } data: { ...data, selectedProgrammer },
version,
});
this.fireChanged();
return true;
}
async selectConfigOption(
{
fqbn,
option,
selectedValue,
}: { fqbn: string; option: string; selectedValue: string },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
}
}
if (!updated) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data, version });
this.fireChanged();
return true;
}
protected async setData({
fqbn,
data,
version,
}: {
fqbn: string;
data: BoardsDataStore.Data;
version: Installable.Version;
}): Promise<void> {
const key = this.getStorageKey(fqbn, version);
return this.storageService.setData(key, data);
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
}
protected async getBoardDetailsSafe(
fqbn: string
): Promise<BoardDetails | undefined> {
try {
const details = this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (
err instanceof Error &&
err.message.includes('loading board data') &&
err.message.includes('is not installed')
) {
this.logger.warn(
`The boards package is not installed for board with FQBN: ${fqbn}`
); );
if (!boardsPackage) { } else {
return undefined; this.logger.error(
} `An unexpected error occurred while retrieving the board details for ${fqbn}.`,
return boardsPackage.installedVersion; err
);
}
return undefined;
} }
}
protected fireChanged(): void {
this.onChangedEmitter.fire();
}
protected async getBoardsPackageVersion(
fqbn: string | undefined
): Promise<Installable.Version | undefined> {
if (!fqbn) {
return undefined;
}
const boardsPackage = await this.boardsService.getContainerBoardPackage({
fqbn,
});
if (!boardsPackage) {
return undefined;
}
return boardsPackage.installedVersion;
}
} }
export namespace BoardsDataStore { export namespace BoardsDataStore {
export interface Data { export interface Data {
readonly configOptions: ConfigOption[]; readonly configOptions: ConfigOption[];
readonly programmers: Programmer[]; readonly programmers: Programmer[];
readonly selectedProgrammer?: Programmer; readonly selectedProgrammer?: Programmer;
} }
export namespace Data { export namespace Data {
export const EMPTY: Data = { export const EMPTY: Data = {
configOptions: [], configOptions: [],
programmers: [], programmers: [],
}; };
export function is(arg: any): arg is Data { export function is(arg: any): arg is Data {
return ( return (
!!arg && !!arg &&
'configOptions' in arg && 'configOptions' in arg &&
Array.isArray(arg['configOptions']) && Array.isArray(arg['configOptions']) &&
'programmers' in arg && 'programmers' in arg &&
Array.isArray(arg['programmers']) Array.isArray(arg['programmers'])
); );
}
} }
}
} }

View File

@@ -1,73 +1,73 @@
import { inject, injectable, postConstruct } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import { import {
BoardsPackage, BoardsPackage,
BoardsService, BoardsService,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget'; import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable() @injectable()
export class BoardsListWidget extends ListWidget<BoardsPackage> { export class BoardsListWidget extends ListWidget<BoardsPackage> {
static WIDGET_ID = 'boards-list-widget'; static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager'; static WIDGET_LABEL = 'Boards Manager';
constructor( constructor(
@inject(BoardsService) protected service: BoardsService, @inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer) @inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<BoardsPackage> protected itemRenderer: ListItemRenderer<BoardsPackage>
) { ) {
super({ super({
id: BoardsListWidget.WIDGET_ID, id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL, label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip', iconClass: 'fa fa-microchip',
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: BoardsPackage) => item.name, itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated, itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer, itemRenderer,
}); });
} }
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
super.init(); super.init();
this.toDispose.pushAll([ this.toDispose.pushAll([
this.notificationCenter.onPlatformInstalled(() => this.notificationCenter.onPlatformInstalled(() =>
this.refresh(undefined) this.refresh(undefined)
), ),
this.notificationCenter.onPlatformUninstalled(() => this.notificationCenter.onPlatformUninstalled(() =>
this.refresh(undefined) this.refresh(undefined)
), ),
]); ]);
} }
protected async install({ protected async install({
item, item,
progressId, progressId,
version, version,
}: { }: {
item: BoardsPackage; item: BoardsPackage;
progressId: string; progressId: string;
version: string; version: string;
}): Promise<void> { }): Promise<void> {
await super.install({ item, progressId, version }); await super.install({ item, progressId, version });
this.messageService.info( this.messageService.info(
`Successfully installed platform ${item.name}:${version}`, `Successfully installed platform ${item.name}:${version}`,
{ timeout: 3000 } { timeout: 3000 }
); );
} }
protected async uninstall({ protected async uninstall({
item, item,
progressId, progressId,
}: { }: {
item: BoardsPackage; item: BoardsPackage;
progressId: string; progressId: string;
}): Promise<void> { }): Promise<void> {
await super.uninstall({ item, progressId }); await super.uninstall({ item, progressId });
this.messageService.info( this.messageService.info(
`Successfully uninstalled platform ${item.name}:${item.installedVersion}`, `Successfully uninstalled platform ${item.name}:${item.installedVersion}`,
{ timeout: 3000 } { timeout: 3000 }
); );
} }
} }

View File

@@ -6,232 +6,217 @@ import { Port } from '../../common/protocol';
import { BoardsConfig } from './boards-config'; import { BoardsConfig } from './boards-config';
import { ArduinoCommands } from '../arduino-commands'; import { ArduinoCommands } from '../arduino-commands';
import { import {
BoardsServiceProvider, BoardsServiceProvider,
AvailableBoard, AvailableBoard,
} from './boards-service-provider'; } from './boards-service-provider';
export interface BoardsDropDownListCoords { export interface BoardsDropDownListCoords {
readonly top: number; readonly top: number;
readonly left: number; readonly left: number;
readonly width: number; readonly width: number;
readonly paddingTop: number; readonly paddingTop: number;
} }
export namespace BoardsDropDown { export namespace BoardsDropDown {
export interface Props { export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden'; readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Array< readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
AvailableBoard & { onClick: () => void; port: Port } readonly openBoardsConfig: () => void;
>; }
readonly openBoardsConfig: () => void;
}
} }
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> { export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownElement: HTMLElement; protected dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) { constructor(props: BoardsDropDown.Props) {
super(props); super(props);
let list = document.getElementById('boards-dropdown-container'); let list = document.getElementById('boards-dropdown-container');
if (!list) { if (!list) {
list = document.createElement('div'); list = document.createElement('div');
list.id = 'boards-dropdown-container'; list.id = 'boards-dropdown-container';
document.body.appendChild(list); document.body.appendChild(list);
this.dropdownElement = list; this.dropdownElement = list;
}
} }
}
render(): React.ReactNode { render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement); return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
} }
protected renderNode(): React.ReactNode { protected renderNode(): React.ReactNode {
const { coords, items } = this.props; const { coords, items } = this.props;
if (coords === 'hidden') { if (coords === 'hidden') {
return ''; return '';
}
return (
<div
className="arduino-boards-dropdown-list"
style={{
position: 'absolute',
...coords,
}}
>
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig(),
})}
{items
.map(({ name, port, selected, onClick }) => ({
label: `${name} at ${Port.toString(port)}`,
selected,
onClick,
}))
.map(this.renderItem)}
</div>
);
} }
return (
<div
className="arduino-boards-dropdown-list"
style={{
position: 'absolute',
...coords,
}}
>
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig(),
})}
{items
.map(({ name, port, selected, onClick }) => ({
label: `${name} at ${Port.toString(port)}`,
selected,
onClick,
}))
.map(this.renderItem)}
</div>
);
}
protected renderItem({ protected renderItem({
label, label,
selected, selected,
onClick, onClick,
}: { }: {
label: string; label: string;
selected?: boolean; selected?: boolean;
onClick: () => void; onClick: () => void;
}): React.ReactNode { }): React.ReactNode {
return ( return (
<div <div
key={label} key={label}
className={`arduino-boards-dropdown-item ${ className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`}
selected ? 'selected' : '' onClick={onClick}
}`} >
onClick={onClick} <div>{label}</div>
> {selected ? <span className="fa fa-check" /> : ''}
<div>{label}</div> </div>
{selected ? <span className="fa fa-check" /> : ''} );
</div> }
);
}
} }
export class BoardsToolBarItem extends React.Component< export class BoardsToolBarItem extends React.Component<
BoardsToolBarItem.Props, BoardsToolBarItem.Props,
BoardsToolBarItem.State BoardsToolBarItem.State
> { > {
static TOOLBAR_ID: 'boards-toolbar'; static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = protected readonly toDispose: DisposableCollection =
new DisposableCollection(); new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) { constructor(props: BoardsToolBarItem.Props) {
super(props); super(props);
const { availableBoards } = props.boardsServiceClient; const { availableBoards } = props.boardsServiceClient;
this.state = { this.state = {
availableBoards, availableBoards,
coords: 'hidden', coords: 'hidden',
};
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
this.props.boardsServiceClient.onAvailableBoardsChanged(
(availableBoards) => this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height,
},
});
} else {
this.setState({ coords: 'hidden' });
}
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
}; };
render(): React.ReactNode { document.addEventListener('click', () => {
const { coords, availableBoards } = this.state; this.setState({ coords: 'hidden' });
const boardsConfig = this.props.boardsServiceClient.boardsConfig; });
const title = BoardsConfig.Config.toString(boardsConfig, { }
default: 'no board selected',
componentDidMount() {
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height,
},
}); });
const decorator = (() => { } else {
const selectedBoard = availableBoards.find(
({ selected }) => selected
);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached';
}
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed';
}
return '';
})();
return (
<React.Fragment>
<div className="arduino-boards-toolbar-item-container">
<div className="arduino-boards-toolbar-item" title={title}>
<div className="inner-container" onClick={this.show}>
<span className={decorator} />
<div className="label noWrapInfo">
<div className="noWrapInfo noselect">
{title}
</div>
</div>
<span className="fa fa-caret-down caret" />
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={availableBoards
.filter(AvailableBoard.hasPort)
.map((board) => ({
...board,
onClick: () => {
if (
board.state ===
AvailableBoard.State.incomplete
) {
this.props.boardsServiceClient.boardsConfig =
{
selectedPort: board.port,
};
this.openDialog();
} else {
this.props.boardsServiceClient.boardsConfig =
{
selectedBoard: board,
selectedPort: board.port,
};
}
},
}))}
openBoardsConfig={this.openDialog}
></BoardsDropDown>
</React.Fragment>
);
}
protected openDialog = () => {
this.props.commands.executeCommand(
ArduinoCommands.OPEN_BOARDS_DIALOG.id
);
this.setState({ coords: 'hidden' }); this.setState({ coords: 'hidden' });
}; }
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
const title = BoardsConfig.Config.toString(boardsConfig, {
default: 'no board selected',
});
const decorator = (() => {
const selectedBoard = availableBoards.find(({ selected }) => selected);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached';
}
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed';
}
return '';
})();
return (
<React.Fragment>
<div className="arduino-boards-toolbar-item-container">
<div className="arduino-boards-toolbar-item" title={title}>
<div className="inner-container" onClick={this.show}>
<span className={decorator} />
<div className="label noWrapInfo">
<div className="noWrapInfo noselect">{title}</div>
</div>
<span className="fa fa-caret-down caret" />
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={availableBoards
.filter(AvailableBoard.hasPort)
.map((board) => ({
...board,
onClick: () => {
if (board.state === AvailableBoard.State.incomplete) {
this.props.boardsServiceClient.boardsConfig = {
selectedPort: board.port,
};
this.openDialog();
} else {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: board.port,
};
}
},
}))}
openBoardsConfig={this.openDialog}
></BoardsDropDown>
</React.Fragment>
);
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ coords: 'hidden' });
};
} }
export namespace BoardsToolBarItem { export namespace BoardsToolBarItem {
export interface Props { export interface Props {
readonly boardsServiceClient: BoardsServiceProvider; readonly boardsServiceClient: BoardsServiceProvider;
readonly commands: CommandRegistry; readonly commands: CommandRegistry;
} }
export interface State { export interface State {
availableBoards: AvailableBoard[]; availableBoards: AvailableBoard[];
coords: BoardsDropDownListCoords | 'hidden'; coords: BoardsDropDownListCoords | 'hidden';
} }
} }

View File

@@ -5,20 +5,20 @@ import { ListWidgetFrontendContribution } from '../widgets/component-list/list-w
@injectable() @injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> { export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
constructor() { constructor() {
super({ super({
widgetId: BoardsListWidget.WIDGET_ID, widgetId: BoardsListWidget.WIDGET_ID,
widgetName: BoardsListWidget.WIDGET_LABEL, widgetName: BoardsListWidget.WIDGET_LABEL,
defaultWidgetOptions: { defaultWidgetOptions: {
area: 'left', area: 'left',
rank: 2, rank: 2,
}, },
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`, toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'CtrlCmd+Shift+B', toggleKeybinding: 'CtrlCmd+Shift+B',
}); });
} }
async initializeLayout(): Promise<void> { async initializeLayout(): Promise<void> {
this.openView(); this.openView();
} }
} }

View File

@@ -5,119 +5,113 @@ import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ConfigService } from '../../common/protocol'; import { ConfigService } from '../../common/protocol';
@injectable() @injectable()
export class About extends Contribution { export class About extends Contribution {
@inject(ClipboardService) @inject(ClipboardService)
protected readonly clipboardService: ClipboardService; protected readonly clipboardService: ClipboardService;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, { registry.registerCommand(About.Commands.ABOUT_APP, {
execute: () => this.showAbout(), execute: () => this.showAbout(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id, commandId: About.Commands.ABOUT_APP.id,
label: `About ${this.applicationName}`, label: `About ${this.applicationName}`,
order: '0', order: '0',
}); });
} }
async showAbout(): Promise<void> { async showAbout(): Promise<void> {
const { const {
version, version,
commit, commit,
status: cliStatus, status: cliStatus,
} = await this.configService.getVersion(); } = await this.configService.getVersion();
const buildDate = this.buildDate; const buildDate = this.buildDate;
const detail = ( const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
showAll: boolean
) => `Version: ${remote.app.getVersion()}
Date: ${buildDate ? buildDate : 'dev build'}${ Date: ${buildDate ? buildDate : 'dev build'}${
buildDate && showAll ? ` (${this.ago(buildDate)})` : '' buildDate && showAll ? ` (${this.ago(buildDate)})` : ''
} }
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}] CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''} ${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
`; `;
const ok = 'OK'; const ok = 'OK';
const copy = 'Copy'; const copy = 'Copy';
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy]; const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
const { response } = await remote.dialog.showMessageBox( const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
message: `${this.applicationName}`, message: `${this.applicationName}`,
title: `${this.applicationName}`, title: `${this.applicationName}`,
type: 'info', type: 'info',
detail: detail(true), detail: detail(true),
buttons, buttons,
noLink: true, noLink: true,
defaultId: buttons.indexOf(ok), defaultId: buttons.indexOf(ok),
cancelId: buttons.indexOf(ok), cancelId: buttons.indexOf(ok),
} }
); );
if (buttons[response] === copy) { if (buttons[response] === copy) {
await this.clipboardService.writeText(detail(false).trim()); await this.clipboardService.writeText(detail(false).trim());
}
} }
}
protected get applicationName(): string { protected get applicationName(): string {
return FrontendApplicationConfigProvider.get().applicationName; return FrontendApplicationConfigProvider.get().applicationName;
} }
protected get buildDate(): string | undefined { protected get buildDate(): string | undefined {
return FrontendApplicationConfigProvider.get().buildDate; return FrontendApplicationConfigProvider.get().buildDate;
} }
protected ago(isoTime: string): string { protected ago(isoTime: string): string {
const now = moment(Date.now()); const now = moment(Date.now());
const other = moment(isoTime); const other = moment(isoTime);
let result = now.diff(other, 'minute'); let result = now.diff(other, 'minute');
if (result < 60) { if (result < 60) {
return result === 1 return result === 1 ? `${result} minute ago` : `${result} minute ago`;
? `${result} minute ago`
: `${result} minute ago`;
}
result = now.diff(other, 'hour');
if (result < 25) {
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
}
result = now.diff(other, 'day');
if (result < 8) {
return result === 1 ? `${result} day ago` : `${result} days ago`;
}
result = now.diff(other, 'week');
if (result < 5) {
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
}
result = now.diff(other, 'month');
if (result < 13) {
return result === 1
? `${result} month ago`
: `${result} months ago`;
}
result = now.diff(other, 'year');
return result === 1 ? `${result} year ago` : `${result} years ago`;
} }
result = now.diff(other, 'hour');
if (result < 25) {
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
}
result = now.diff(other, 'day');
if (result < 8) {
return result === 1 ? `${result} day ago` : `${result} days ago`;
}
result = now.diff(other, 'week');
if (result < 5) {
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
}
result = now.diff(other, 'month');
if (result < 13) {
return result === 1 ? `${result} month ago` : `${result} months ago`;
}
result = now.diff(other, 'year');
return result === 1 ? `${result} year ago` : `${result} years ago`;
}
} }
export namespace About { export namespace About {
export namespace Commands { export namespace Commands {
export const ABOUT_APP: Command = { export const ABOUT_APP: Command = {
id: 'arduino-about', id: 'arduino-about',
}; };
} }
} }

View File

@@ -2,74 +2,74 @@ import { inject, injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
URI, URI,
} from './contribution'; } from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser'; import { FileDialogService } from '@theia/filesystem/lib/browser';
@injectable() @injectable()
export class AddFile extends SketchContribution { export class AddFile extends SketchContribution {
@inject(FileDialogService) @inject(FileDialogService)
protected readonly fileDialogService: FileDialogService; protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, { registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile(), execute: () => this.addFile(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, { registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id, commandId: AddFile.Commands.ADD_FILE.id,
label: 'Add File...', label: 'Add File...',
order: '2', order: '2',
}); });
} }
protected async addFile(): Promise<void> { protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) { if (!sketch) {
return; return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`,
});
if (response === 0) {
// Cancel
return;
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', {
timeout: 2000,
});
} }
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`,
});
if (response === 0) {
// Cancel
return;
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', {
timeout: 2000,
});
}
} }
export namespace AddFile { export namespace AddFile {
export namespace Commands { export namespace Commands {
export const ADD_FILE: Command = { export const ADD_FILE: Command = {
id: 'arduino-add-file', id: 'arduino-add-file',
}; };
} }
} }

View File

@@ -7,133 +7,127 @@ import { ArduinoMenus } from '../menu/arduino-menus';
import { ResponseServiceImpl } from '../response-service-impl'; import { ResponseServiceImpl } from '../response-service-impl';
import { Installable, LibraryService } from '../../common/protocol'; import { Installable, LibraryService } from '../../common/protocol';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class AddZipLibrary extends SketchContribution { export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer) @inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer; protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceImpl) @inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl; protected readonly responseService: ResponseServiceImpl;
@inject(LibraryService) @inject(LibraryService)
protected readonly libraryService: LibraryService; protected readonly libraryService: LibraryService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, { registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
execute: () => this.addZipLibrary(), execute: () => this.addZipLibrary(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
const includeLibMenuPath = [ const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP, ...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include', '0_include',
]; ];
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt. // TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
registry.registerSubmenu(includeLibMenuPath, 'Include Library', { registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1', order: '1',
}); });
registry.registerMenuAction([...includeLibMenuPath, '1_install'], { registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id, commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
label: 'Add .ZIP Library...', label: 'Add .ZIP Library...',
order: '1', order: '1',
}); });
} }
async addZipLibrary(): Promise<void> { async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri(); const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri)); const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog({ const { canceled, filePaths } = await remote.dialog.showOpenDialog({
title: "Select a zip file containing the library you'd like to add", title: "Select a zip file containing the library you'd like to add",
defaultPath, defaultPath,
properties: ['openFile'], properties: ['openFile'],
filters: [ filters: [
{ {
name: 'Library', name: 'Library',
extensions: ['zip'], extensions: ['zip'],
}, },
], ],
}); });
if (!canceled && filePaths.length) { if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]); const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try { try {
await this.doInstall(zipUri); await this.doInstall(zipUri);
} catch (error) { } catch (error) {
if (error instanceof AlreadyInstalledError) { if (error instanceof AlreadyInstalledError) {
const result = await new ConfirmDialog({ const result = await new ConfirmDialog({
msg: error.message, msg: error.message,
title: 'Do you want to overwrite the existing library?', title: 'Do you want to overwrite the existing library?',
ok: 'Yes', ok: 'Yes',
cancel: 'No', cancel: 'No',
}).open(); }).open();
if (result) { if (result) {
await this.doInstall(zipUri, true); await this.doInstall(zipUri, true);
} }
}
}
} }
}
} }
}
private async doInstall( private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
zipUri: string, try {
overwrite?: boolean await Installable.doWithProgress({
): Promise<void> { messageService: this.messageService,
try { progressText: `Processing ${new URI(zipUri).path.base}`,
await Installable.doWithProgress({ responseService: this.responseService,
messageService: this.messageService, run: () => this.libraryService.installZip({ zipUri, overwrite }),
progressText: `Processing ${new URI(zipUri).path.base}`, });
responseService: this.responseService, this.messageService.info(
run: () => `Successfully installed library from ${
this.libraryService.installZip({ zipUri, overwrite }), new URI(zipUri).path.base
}); } archive`,
this.messageService.info( { timeout: 3000 }
`Successfully installed library from ${ );
new URI(zipUri).path.base } catch (error) {
} archive`, if (error instanceof Error) {
{ timeout: 3000 } 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
); );
} catch (error) { } else {
if (error instanceof Error) { throw new AlreadyInstalledError(
const match = error.message.match( 'A library already exists. Do you want to overwrite it?'
/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;
} }
}
this.messageService.error(error.toString());
throw error;
} }
}
} }
class AlreadyInstalledError extends Error { class AlreadyInstalledError extends Error {
constructor(message: string, readonly libraryName?: string) { constructor(message: string, readonly libraryName?: string) {
super(message); super(message);
Object.setPrototypeOf(this, AlreadyInstalledError.prototype); Object.setPrototypeOf(this, AlreadyInstalledError.prototype);
} }
} }
export namespace AddZipLibrary { export namespace AddZipLibrary {
export namespace Commands { export namespace Commands {
export const ADD_ZIP_LIBRARY: Command = { export const ADD_ZIP_LIBRARY: Command = {
id: 'arduino-add-zip-library', id: 'arduino-add-zip-library',
}; };
} }
} }

View File

@@ -4,65 +4,65 @@ import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class ArchiveSketch extends SketchContribution { export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, { registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch(), execute: () => this.archiveSketch(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id, commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: 'Archive Sketch', label: 'Archive Sketch',
order: '1', order: '1',
}); });
} }
protected async archiveSketch(): Promise<void> { protected async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([ const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(), this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(), this.configService.getConfiguration(),
]); ]);
if (!sketch) { if (!sketch) {
return; return;
}
const archiveBasename = `${sketch.name}-${dateFormat(
new Date(),
'yymmdd'
)}a.zip`;
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, {
timeout: 2000,
});
} }
const archiveBasename = `${sketch.name}-${dateFormat(
new Date(),
'yymmdd'
)}a.zip`;
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, {
timeout: 2000,
});
}
} }
export namespace ArchiveSketch { export namespace ArchiveSketch {
export namespace Commands { export namespace Commands {
export const ARCHIVE_SKETCH: Command = { export const ARCHIVE_SKETCH: Command = {
id: 'arduino-archive-sketch', id: 'arduino-archive-sketch',
}; };
} }
} }

View File

@@ -2,8 +2,8 @@ import { inject, injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { import {
DisposableCollection, DisposableCollection,
Disposable, Disposable,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils'; import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config'; import { BoardsConfig } from '../boards/boards-config';
@@ -12,327 +12,304 @@ import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
ArduinoMenus, ArduinoMenus,
PlaceholderMenuNode, PlaceholderMenuNode,
unregisterSubmenu, unregisterSubmenu,
} from '../menu/arduino-menus'; } from '../menu/arduino-menus';
import { import {
BoardsService, BoardsService,
InstalledBoardWithPackage, InstalledBoardWithPackage,
AvailablePorts, AvailablePorts,
Port, Port,
} from '../../common/protocol'; } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution'; import { SketchContribution, Command, CommandRegistry } from './contribution';
@injectable() @injectable()
export class BoardSelection extends SketchContribution { export class BoardSelection extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry; protected readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider; protected readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection(); protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, { registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => { execute: async () => {
const { selectedBoard, selectedPort } = const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig; this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) { if (!selectedBoard) {
this.messageService.info( this.messageService.info(
'Please select a board to obtain board info.' 'Please select a board to obtain board info.'
); );
return; return;
} }
if (!selectedBoard.fqbn) { if (!selectedBoard.fqbn) {
this.messageService.info( this.messageService.info(
`The platform for the selected '${selectedBoard.name}' board is not installed.` `The platform for the selected '${selectedBoard.name}' board is not installed.`
); );
return; return;
} }
if (!selectedPort) { if (!selectedPort) {
this.messageService.info( this.messageService.info(
'Please select a port to obtain board info.' 'Please select a port to obtain board info.'
); );
return; return;
} }
const boardDetails = await this.boardsService.getBoardDetails({ const boardDetails = await this.boardsService.getBoardDetails({
fqbn: selectedBoard.fqbn, fqbn: selectedBoard.fqbn,
}); });
if (boardDetails) { if (boardDetails) {
const { VID, PID } = boardDetails; const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name} const detail = `BN: ${selectedBoard.name}
VID: ${VID} VID: ${VID}
PID: ${PID}`; PID: ${PID}`;
await remote.dialog.showMessageBox( await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
remote.getCurrentWindow(), message: 'Board Info',
{ title: 'Board Info',
message: 'Board Info', type: 'info',
title: 'Board Info', detail,
type: 'info', buttons: ['OK'],
detail, });
buttons: ['OK'], }
} },
); });
} }
},
}); onStart(): void {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> {
const [installedBoards, availablePorts, config] = await Promise.all([
this.installedBoards(),
this.boardsService.getState(),
this.boardsServiceProvider.boardsConfig,
]);
this.rebuildMenus(installedBoards, availablePorts, config);
}
protected rebuildMenus(
installedBoards: InstalledBoardWithPackage[],
availablePorts: AvailablePorts,
config: BoardsConfig.Config
): void {
this.toDisposeBeforeMenuRebuild.dispose();
// Boards submenu
const boardsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'1_boards',
];
const boardsSubmenuLabel = config.selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
boardsSubmenuPath,
`Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`,
{ order: '100' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
)
);
// Ports submenu
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
`Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`,
{ order: '101' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: 'Get Board Info',
order: '103',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
getBoardInfo
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(getBoardInfo)
)
);
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...',
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
const packageLabel =
packageName + `${manuallyInstalled ? ' (in Sketchbook)' : ''}`;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel);
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () => {
if (
fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: {
name,
fqbn,
port: this.boardsServiceProvider.boardsConfig.selectedBoard
?.port, // TODO: verify!
},
selectedPort:
this.boardsServiceProvider.boardsConfig.selectedPort,
};
}
},
isToggled: () =>
fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
};
// Board menu
const menuAction = { commandId: id, label: name };
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
);
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
} }
onStart(): void { // Installed ports
this.updateMenus(); const registerPorts = (ports: AvailablePorts) => {
this.notificationCenter.onPlatformInstalled( const addresses = Object.keys(ports);
this.updateMenus.bind(this) if (!addresses.length) {
); return;
this.notificationCenter.onPlatformUninstalled( }
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> { // Register placeholder for protocol
const [installedBoards, availablePorts, config] = await Promise.all([ const [port] = ports[addresses[0]];
this.installedBoards(), const protocol = port.protocol;
this.boardsService.getState(), const menuPath = [...portsSubmenuPath, protocol];
this.boardsServiceProvider.boardsConfig, const placeholder = new PlaceholderMenuNode(
]); menuPath,
this.rebuildMenus(installedBoards, availablePorts, config); `${firstToUpperCase(port.protocol)} ports`
} );
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
)
);
protected rebuildMenus( for (const address of addresses) {
installedBoards: InstalledBoardWithPackage[], if (!!ports[address]) {
availablePorts: AvailablePorts, const [port, boards] = ports[address];
config: BoardsConfig.Config if (!boards.length) {
): void { boards.push({
this.toDisposeBeforeMenuRebuild.dispose(); name: '',
});
// Boards submenu }
const boardsSubmenuPath = [ for (const { name, fqbn } of boards) {
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, const id = `arduino-select-port--${address}${
'1_boards', fqbn ? `--${fqbn}` : ''
]; }`;
const boardsSubmenuLabel = config.selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
boardsSubmenuPath,
`Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`,
{ order: '100' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
)
);
// Ports submenu
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
`Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`,
{ order: '101' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: 'Get Board Info',
order: '103',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
getBoardInfo
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(getBoardInfo)
)
);
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...',
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name } = board;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(
platformMenuPath,
packageName
);
const id = `arduino-select-board--${fqbn}`;
const command = { id }; const command = { id };
const handler = { const handler = {
execute: () => { execute: () => {
if ( if (
fqbn !== !Port.equals(
this.boardsServiceProvider.boardsConfig.selectedBoard port,
?.fqbn this.boardsServiceProvider.boardsConfig.selectedPort
) { )
this.boardsServiceProvider.boardsConfig = { ) {
selectedBoard: { this.boardsServiceProvider.boardsConfig = {
name, selectedBoard:
fqbn, this.boardsServiceProvider.boardsConfig.selectedBoard,
port: this.boardsServiceProvider.boardsConfig selectedPort: port,
.selectedBoard?.port, // TODO: verify! };
}, }
selectedPort: },
this.boardsServiceProvider.boardsConfig isToggled: () =>
.selectedPort, Port.equals(
}; port,
} this.boardsServiceProvider.boardsConfig.selectedPort
}, ),
isToggled: () => };
fqbn === const label = `${address}${name ? ` (${name})` : ''}`;
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn, const menuAction = {
commandId: id,
label,
order: `1${label}`, // `1` comes after the placeholder which has order `0`
}; };
// Board menu
const menuAction = { commandId: id, label: name };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push( this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => Disposable.create(() =>
this.commandRegistry.unregisterCommand(command) this.commandRegistry.unregisterCommand(command)
) )
); );
this.menuModelRegistry.registerMenuAction( this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
platformMenuPath, }
menuAction
);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
} }
}
};
// Installed ports const { serial, network, unknown } =
const registerPorts = (ports: AvailablePorts) => { AvailablePorts.groupByProtocol(availablePorts);
const addresses = Object.keys(ports); registerPorts(serial);
if (!addresses.length) { registerPorts(network);
return; registerPorts(unknown);
}
// Register placeholder for protocol this.mainMenuManager.update();
const [port] = ports[addresses[0]]; }
const protocol = port.protocol;
const menuPath = [...portsSubmenuPath, protocol];
const placeholder = new PlaceholderMenuNode(
menuPath,
`${firstToUpperCase(port.protocol)} ports`
);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
)
);
for (const address of addresses) { protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
if (!!ports[address]) { const allBoards = await this.boardsService.searchBoards({});
const [port, boards] = ports[address]; return allBoards.filter(InstalledBoardWithPackage.is);
if (!boards.length) { }
boards.push({
name: '',
});
}
for (const { name, fqbn } of boards) {
const id = `arduino-select-port--${address}${
fqbn ? `--${fqbn}` : ''
}`;
const command = { id };
const handler = {
execute: () => {
if (
!Port.equals(
port,
this.boardsServiceProvider.boardsConfig
.selectedPort
)
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard:
this.boardsServiceProvider
.boardsConfig.selectedBoard,
selectedPort: port,
};
}
},
isToggled: () =>
Port.equals(
port,
this.boardsServiceProvider.boardsConfig
.selectedPort
),
};
const label = `${address}${name ? ` (${name})` : ''}`;
const menuAction = {
commandId: id,
label,
order: `1${label}`, // `1` comes after the placeholder which has order `0`
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(
menuPath,
menuAction
);
}
}
}
};
const { serial, network, unknown } =
AvailablePorts.groupByProtocol(availablePorts);
registerPorts(serial);
registerPorts(network);
registerPorts(unknown);
this.mainMenuManager.update();
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.searchBoards({});
return allBoards.filter(InstalledBoardWithPackage.is);
}
} }
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' };
} }
} }

View File

@@ -6,87 +6,85 @@ import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection'; import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class BurnBootloader extends SketchContribution { export class BurnBootloader extends SketchContribution {
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager; protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, { registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(), execute: () => this.burnBootloader(),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
label: 'Burn Bootloader',
order: 'z99',
});
}
async burnBootloader(): Promise<void> {
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
} }
try {
registerMenus(registry: MenuModelRegistry): void { const { boardsConfig } = this.boardsServiceClientImpl;
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, { const port = boardsConfig.selectedPort?.address;
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id, const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
label: 'Burn Bootloader', await Promise.all([
order: 'z99', this.boardsDataStore.appendConfigToFqbn(
}); boardsConfig.selectedBoard?.fqbn
} ),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
async burnBootloader(): Promise<void> { this.preferences.get('arduino.upload.verify'),
const monitorConfig = this.monitorConnection.monitorConfig; this.preferences.get('arduino.upload.verbose'),
if (monitorConfig) { ]);
await this.monitorConnection.disconnect(); this.outputChannelManager.getChannel('Arduino').clear();
} await this.coreService.burnBootloader({
try { fqbn,
const { boardsConfig } = this.boardsServiceClientImpl; programmer,
const port = boardsConfig.selectedPort?.address; port,
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = verify,
await Promise.all([ verbose,
this.boardsDataStore.appendConfigToFqbn( });
boardsConfig.selectedBoard?.fqbn this.messageService.info('Done burning bootloader.', {
), timeout: 3000,
this.boardsDataStore.getData( });
boardsConfig.selectedBoard?.fqbn } catch (e) {
), this.messageService.error(e.toString());
this.preferences.get('arduino.upload.verify'), } finally {
this.preferences.get('arduino.upload.verbose'), if (monitorConfig) {
]); await this.monitorConnection.connect(monitorConfig);
this.outputChannelManager.getChannel('Arduino').clear(); }
await this.coreService.burnBootloader({
fqbn,
programmer,
port,
verify,
verbose,
});
this.messageService.info('Done burning bootloader.', {
timeout: 3000,
});
} catch (e) {
this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
}
}
} }
}
} }
export namespace BurnBootloader { export namespace BurnBootloader {
export namespace Commands { export namespace Commands {
export const BURN_BOOTLOADER: Command = { export const BURN_BOOTLOADER: Command = {
id: 'arduino-burn-bootloader', id: 'arduino-burn-bootloader',
}; };
} }
} }

View File

@@ -8,12 +8,12 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { SaveAsSketch } from './save-as-sketch'; import { SaveAsSketch } from './save-as-sketch';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
URI, URI,
} from './contribution'; } from './contribution';
/** /**
@@ -21,118 +21,112 @@ import {
*/ */
@injectable() @injectable()
export class Close extends SketchContribution { export class Close extends SketchContribution {
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
protected shell: ApplicationShell; protected shell: ApplicationShell;
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
this.shell = app.shell; this.shell = app.shell;
} }
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Close.Commands.CLOSE, { registry.registerCommand(Close.Commands.CLOSE, {
execute: async () => { execute: async () => {
// Close current editor if closeable. // Close current editor if closeable.
const { currentEditor } = this.editorManager; const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) { if (currentEditor && currentEditor.title.closable) {
currentEditor.close(); currentEditor.close();
return; return;
}
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (
currentWidgetInMain &&
currentWidgetInMain.title.closable
) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: ["Don't Save", 'Cancel', 'Save'],
message:
'Do you want to save changes to this sketch before closing?',
detail: "If you don't save, your changes will be lost.",
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
window.close();
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: 'Close',
order: '5',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',
});
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
protected async wasTouched(uri: string): Promise<boolean> {
const editorWidget = await this.editorManager.getByUri(new URI(uri));
if (editorWidget) {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor
.getControl()
.getModel()
?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
return true;
}
}
} }
return false;
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: ["Don't Save", 'Cancel', 'Save'],
message:
'Do you want to save changes to this sketch before closing?',
detail: "If you don't save, your changes will be lost.",
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
window.close();
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: 'Close',
order: '5',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',
});
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
protected async wasTouched(uri: string): Promise<boolean> {
const editorWidget = await this.editorManager.getByUri(new URI(uri));
if (editorWidget) {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor.getControl().getModel()?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
return true;
}
}
} }
return false;
}
} }
export namespace Close { export namespace Close {
export namespace Commands { export namespace Commands {
export const CLOSE: Command = { export const CLOSE: Command = {
id: 'arduino-close', id: 'arduino-close',
}; };
} }
} }

View File

@@ -11,147 +11,144 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { import {
MenuModelRegistry, MenuModelRegistry,
MenuContribution, MenuContribution,
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
import { import {
KeybindingRegistry, KeybindingRegistry,
KeybindingContribution, KeybindingContribution,
} from '@theia/core/lib/browser/keybinding'; } from '@theia/core/lib/browser/keybinding';
import { import {
TabBarToolbarContribution, TabBarToolbarContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
FrontendApplication, FrontendApplication,
} from '@theia/core/lib/browser/frontend-application'; } from '@theia/core/lib/browser/frontend-application';
import { import {
Command, Command,
CommandRegistry, CommandRegistry,
CommandContribution, CommandContribution,
CommandService, CommandService,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode'; import { EditorMode } from '../editor-mode';
import { SettingsService } from '../settings'; import { SettingsService } from '../settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { import {
SketchesService, SketchesService,
ConfigService, ConfigService,
FileSystemExt, FileSystemExt,
Sketch, Sketch,
} from '../../common/protocol'; } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
export { export {
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
URI, URI,
Sketch, Sketch,
open, open,
}; };
@injectable() @injectable()
export abstract class Contribution export abstract class Contribution
implements implements
CommandContribution, CommandContribution,
MenuContribution, MenuContribution,
KeybindingContribution, KeybindingContribution,
TabBarToolbarContribution, TabBarToolbarContribution,
FrontendApplicationContribution FrontendApplicationContribution
{ {
@inject(ILogger) @inject(ILogger)
protected readonly logger: ILogger; protected readonly logger: ILogger;
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(CommandService) @inject(CommandService)
protected readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(WorkspaceService) @inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService; protected readonly workspaceService: WorkspaceService;
@inject(EditorMode) @inject(EditorMode)
protected readonly editorMode: EditorMode; protected readonly editorMode: EditorMode;
@inject(LabelProvider) @inject(LabelProvider)
protected readonly labelProvider: LabelProvider; protected readonly labelProvider: LabelProvider;
@inject(SettingsService) @inject(SettingsService)
protected readonly settingsService: SettingsService; protected readonly settingsService: SettingsService;
onStart(app: FrontendApplication): MaybePromise<void> {} onStart(app: FrontendApplication): MaybePromise<void> {}
registerCommands(registry: CommandRegistry): void {} registerCommands(registry: CommandRegistry): void {}
registerMenus(registry: MenuModelRegistry): void {} registerMenus(registry: MenuModelRegistry): void {}
registerKeybindings(registry: KeybindingRegistry): void {} registerKeybindings(registry: KeybindingRegistry): void {}
registerToolbarItems(registry: TabBarToolbarRegistry): void {} registerToolbarItems(registry: TabBarToolbarRegistry): void {}
} }
@injectable() @injectable()
export abstract class SketchContribution extends Contribution { export abstract class SketchContribution extends Contribution {
@inject(FileService) @inject(FileService)
protected readonly fileService: FileService; protected readonly fileService: FileService;
@inject(FileSystemExt) @inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt; protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchService: SketchesService; protected readonly sketchService: SketchesService;
@inject(OpenerService) @inject(OpenerService)
protected readonly openerService: OpenerService; protected readonly openerService: OpenerService;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl; protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ArduinoPreferences) @inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences; protected readonly preferences: ArduinoPreferences;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager; protected readonly outputChannelManager: OutputChannelManager;
protected async sourceOverride(): Promise<Record<string, string>> { protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {}; const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) { if (sketch) {
for (const editor of this.editorManager.all) { for (const editor of this.editorManager.all) {
const uri = editor.editor.uri; const uri = editor.editor.uri;
if ( if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
Saveable.isDirty(editor) && override[uri.toString()] = editor.editor.document.getText();
Sketch.isInSketch(uri, sketch)
) {
override[uri.toString()] = editor.editor.document.getText();
}
}
} }
return override; }
} }
return override;
}
} }
export namespace Contribution { export namespace Contribution {
export function configure<T>( export function configure<T>(
bind: interfaces.Bind, bind: interfaces.Bind,
serviceIdentifier: typeof Contribution serviceIdentifier: typeof Contribution
): void { ): void {
bind(serviceIdentifier).toSelf().inSingletonScope(); bind(serviceIdentifier).toSelf().inSingletonScope();
bind(CommandContribution).toService(serviceIdentifier); bind(CommandContribution).toService(serviceIdentifier);
bind(MenuContribution).toService(serviceIdentifier); bind(MenuContribution).toService(serviceIdentifier);
bind(KeybindingContribution).toService(serviceIdentifier); bind(KeybindingContribution).toService(serviceIdentifier);
bind(TabBarToolbarContribution).toService(serviceIdentifier); bind(TabBarToolbarContribution).toService(serviceIdentifier);
bind(FrontendApplicationContribution).toService(serviceIdentifier); bind(FrontendApplicationContribution).toService(serviceIdentifier);
} }
} }

View File

@@ -6,164 +6,159 @@ import { NotificationCenter } from '../notification-center';
import { Board, BoardsService, ExecutableService } from '../../common/protocol'; import { Board, BoardsService, ExecutableService } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
SketchContribution, SketchContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class Debug extends SketchContribution { export class Debug extends SketchContribution {
@inject(HostedPluginSupport) @inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport; protected hostedPluginSupport: HostedPluginSupport;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(ExecutableService) @inject(ExecutableService)
protected readonly executableService: ExecutableService; protected readonly executableService: ExecutableService;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardService: BoardsService; protected readonly boardService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider; protected readonly boardsServiceProvider: BoardsServiceProvider;
/** /**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled. * If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/ */
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism. protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
protected disabledMessageDidChangeEmitter = new Emitter< protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
string | undefined protected onDisabledMessageDidChange =
>(); this.disabledMessageDidChangeEmitter.event;
protected onDisabledMessageDidChange =
this.disabledMessageDidChangeEmitter.event;
protected get disabledMessage(): string | undefined { protected get disabledMessage(): string | undefined {
return this._disabledMessages; return this._disabledMessages;
} }
protected set disabledMessage(message: string | undefined) { protected set disabledMessage(message: string | undefined) {
this._disabledMessages = message; this._disabledMessages = message;
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages); this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
} }
protected readonly debugToolbarItem = { protected readonly debugToolbarItem = {
id: Debug.Commands.START_DEBUGGING.id, id: Debug.Commands.START_DEBUGGING.id,
command: Debug.Commands.START_DEBUGGING.id, command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${ tooltip: `${
this.disabledMessage this.disabledMessage
? `Debug - ${this.disabledMessage}` ? `Debug - ${this.disabledMessage}`
: 'Start Debugging' : 'Start Debugging'
}`, }`,
priority: 3, priority: 3,
onDidChange: this.onDisabledMessageDidChange as Event<void>, onDidChange: this.onDisabledMessageDidChange as Event<void>,
};
onStart(): void {
this.onDisabledMessageDidChange(
() =>
(this.debugToolbarItem.tooltip = `${
this.disabledMessage
? `Debug - ${this.disabledMessage}`
: 'Start Debugging'
}`)
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = 'No board selected';
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
} else {
this.disabledMessage = undefined;
}
}; };
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
}
onStart(): void { registerCommands(registry: CommandRegistry): void {
this.onDisabledMessageDidChange( registry.registerCommand(Debug.Commands.START_DEBUGGING, {
() => execute: () => this.startDebug(),
(this.debugToolbarItem.tooltip = `${ isVisible: (widget) =>
this.disabledMessage ArduinoToolbar.is(widget) && widget.side === 'left',
? `Debug - ${this.disabledMessage}` isEnabled: () => !this.disabledMessage,
: 'Start Debugging' });
}`) }
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = 'No board selected';
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
} else {
this.disabledMessage = undefined;
}
};
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
}
registerCommands(registry: CommandRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, { registry.registerItem(this.debugToolbarItem);
execute: () => this.startDebug(), }
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.disabledMessage,
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void { protected async startDebug(
registry.registerItem(this.debugToolbarItem); board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
return;
} }
const { name, fqbn } = board;
protected async startDebug( if (!fqbn) {
board: Board | undefined = this.boardsServiceProvider.boardsConfig return;
.selectedBoard
): Promise<void> {
if (!board) {
return;
}
const { name, fqbn } = board;
if (!fqbn) {
return;
}
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
return;
}
const 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(ideTempFolderUri)),
]);
const config = {
cliPath,
board: {
fqbn,
name,
},
sketchPath,
configPath,
};
return this.commandService.executeCommand(
'arduino.debug.start',
config
);
} }
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
return;
}
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(ideTempFolderUri)),
]);
const config = {
cliPath,
board: {
fqbn,
name,
},
sketchPath,
configPath,
};
return this.commandService.executeCommand('arduino.debug.start', config);
}
} }
export namespace Debug { export namespace Debug {
export namespace Commands { export namespace Commands {
export const START_DEBUGGING: Command = { export const START_DEBUGGING: Command = {
id: 'arduino-start-debug', id: 'arduino-start-debug',
label: 'Start Debugging', label: 'Start Debugging',
category: 'Arduino', category: 'Arduino',
}; };
} }
} }

View File

@@ -4,11 +4,11 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
@@ -16,312 +16,305 @@ import { ArduinoMenus } from '../menu/arduino-menus';
// 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) @inject(MonacoEditorService)
protected readonly codeEditorService: MonacoEditorService; protected readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService) @inject(ClipboardService)
protected readonly clipboardService: ClipboardService; protected readonly clipboardService: ClipboardService;
@inject(PreferenceService) @inject(PreferenceService)
protected readonly preferences: PreferenceService; protected readonly preferences: PreferenceService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, { registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
execute: () => this.run('editor.action.gotoLine'), execute: () => this.run('editor.action.gotoLine'),
}); });
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, { registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, {
execute: () => this.run('editor.action.commentLine'), execute: () => this.run('editor.action.commentLine'),
}); });
registry.registerCommand(EditContributions.Commands.INDENT_LINES, { registry.registerCommand(EditContributions.Commands.INDENT_LINES, {
execute: () => this.run('editor.action.indentLines'), execute: () => this.run('editor.action.indentLines'),
}); });
registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, { registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, {
execute: () => this.run('editor.action.outdentLines'), execute: () => this.run('editor.action.outdentLines'),
}); });
registry.registerCommand(EditContributions.Commands.FIND, { registry.registerCommand(EditContributions.Commands.FIND, {
execute: () => this.run('actions.find'), execute: () => this.run('actions.find'),
}); });
registry.registerCommand(EditContributions.Commands.FIND_NEXT, { registry.registerCommand(EditContributions.Commands.FIND_NEXT, {
execute: () => this.run('actions.findWithSelection'), execute: () => this.run('actions.findWithSelection'),
}); });
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, {
execute: () => this.run('editor.action.nextMatchFindAction'), execute: () => this.run('editor.action.nextMatchFindAction'),
}); });
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
this.run('editor.action.previousSelectionMatchFindAction'), });
}); registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
registry.registerCommand( execute: async () => {
EditContributions.Commands.INCREASE_FONT_SIZE, const settings = await this.settingsService.settings();
{ if (settings.autoScaleInterface) {
execute: async () => { settings.interfaceScale = settings.interfaceScale + 1;
const settings = await this.settingsService.settings(); } else {
if (settings.autoScaleInterface) { settings.editorFontSize = settings.editorFontSize + 1;
settings.interfaceScale = settings.interfaceScale + 1; }
} else { await this.settingsService.update(settings);
settings.editorFontSize = settings.editorFontSize + 1; await this.settingsService.save();
} },
await this.settingsService.update(settings); });
await this.settingsService.save(); registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
}, execute: async () => {
} const settings = await this.settingsService.settings();
); if (settings.autoScaleInterface) {
registry.registerCommand( settings.interfaceScale = settings.interfaceScale - 1;
EditContributions.Commands.DECREASE_FONT_SIZE, } else {
{ settings.editorFontSize = settings.editorFontSize - 1;
execute: async () => { }
const settings = await this.settingsService.settings(); await this.settingsService.update(settings);
if (settings.autoScaleInterface) { await this.settingsService.save();
settings.interfaceScale = settings.interfaceScale - 1; },
} else { });
settings.editorFontSize = settings.editorFontSize - 1; /* Tools */ registry.registerCommand(
} EditContributions.Commands.AUTO_FORMAT,
await this.settingsService.update(settings); { execute: () => this.run('editor.action.formatDocument') }
await this.settingsService.save(); );
}, registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
} execute: async () => {
); const value = await this.currentValue();
/* Tools */ registry.registerCommand( if (value !== undefined) {
EditContributions.Commands.AUTO_FORMAT, this.clipboardService.writeText(`[code]
{ execute: () => this.run('editor.action.formatDocument') }
);
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`[code]
${value} ${value}
[/code]`); [/code]`);
} }
}, },
}); });
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, { registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
execute: async () => { execute: async () => {
const value = await this.currentValue(); const value = await this.currentValue();
if (value !== undefined) { if (value !== undefined) {
this.clipboardService.writeText(`\`\`\`cpp this.clipboardService.writeText(`\`\`\`cpp
${value} ${value}
\`\`\``); \`\`\``);
}
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.CUT.id,
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.COPY.id,
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
label: 'Copy for Forum',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
label: 'Copy for GitHub',
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.PASTE.id,
order: '4',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.SELECT_ALL.id,
order: '5',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.GO_TO_LINE.id,
label: 'Go to Line...',
order: '6',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
label: 'Comment/Uncomment',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.INDENT_LINES.id,
label: 'Increase Indent',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.OUTDENT_LINES.id,
label: 'Decrease Indent',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: 'Increase Font Size',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: 'Decrease Font Size',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: 'Find',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_NEXT.id,
label: 'Find Next',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
label: 'Find Previous',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.USE_FOR_FIND.id,
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
order: '3',
});
// `Tools`
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.TOGGLE_COMMENT.id,
keybinding: 'CtrlCmd+/',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_NEXT.id,
keybinding: 'CtrlCmd+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_PREVIOUS.id,
keybinding: 'CtrlCmd+Shift+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.USE_FOR_FIND.id,
keybinding: 'CtrlCmd+E',
});
// `Tools`
registry.registerKeybinding({
command: EditContributions.Commands.AUTO_FORMAT.id,
keybinding: 'CtrlCmd+T',
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
);
}
protected async currentValue(): Promise<string | undefined> {
const currentEditor = await this.current();
if (currentEditor) {
const selection = currentEditor.getSelection();
if (!selection || selection.isEmpty()) {
return currentEditor.getValue();
}
return currentEditor.getModel()?.getValueInRange(selection);
} }
return undefined; },
} });
}
protected async run(commandId: string): Promise<any> { registerMenus(registry: MenuModelRegistry): void {
const editor = await this.current(); registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
if (editor) { commandId: CommonCommands.CUT.id,
const action = editor.getAction(commandId); order: '0',
if (action) { });
return action.run(); registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
} commandId: CommonCommands.COPY.id,
} order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
label: 'Copy for Forum',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
label: 'Copy for GitHub',
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.PASTE.id,
order: '4',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.SELECT_ALL.id,
order: '5',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.GO_TO_LINE.id,
label: 'Go to Line...',
order: '6',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
label: 'Comment/Uncomment',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.INDENT_LINES.id,
label: 'Increase Indent',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.OUTDENT_LINES.id,
label: 'Decrease Indent',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: 'Increase Font Size',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: 'Decrease Font Size',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: 'Find',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_NEXT.id,
label: 'Find Next',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
label: 'Find Previous',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.USE_FOR_FIND.id,
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
order: '3',
});
// `Tools`
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.TOGGLE_COMMENT.id,
keybinding: 'CtrlCmd+/',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_NEXT.id,
keybinding: 'CtrlCmd+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_PREVIOUS.id,
keybinding: 'CtrlCmd+Shift+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.USE_FOR_FIND.id,
keybinding: 'CtrlCmd+E',
});
// `Tools`
registry.registerKeybinding({
command: EditContributions.Commands.AUTO_FORMAT.id,
keybinding: 'CtrlCmd+T',
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
);
}
protected async currentValue(): Promise<string | undefined> {
const currentEditor = await this.current();
if (currentEditor) {
const selection = currentEditor.getSelection();
if (!selection || selection.isEmpty()) {
return currentEditor.getValue();
}
return currentEditor.getModel()?.getValueInRange(selection);
} }
return undefined;
}
protected async run(commandId: string): Promise<any> {
const editor = await this.current();
if (editor) {
const action = editor.getAction(commandId);
if (action) {
return action.run();
}
}
}
} }
export namespace EditContributions { export namespace EditContributions {
export namespace Commands { export namespace Commands {
export const COPY_FOR_FORUM: Command = { export const COPY_FOR_FORUM: Command = {
id: 'arduino-copy-for-forum', id: 'arduino-copy-for-forum',
}; };
export const COPY_FOR_GITHUB: Command = { export const COPY_FOR_GITHUB: Command = {
id: 'arduino-copy-for-github', id: 'arduino-copy-for-github',
}; };
export const GO_TO_LINE: Command = { export const GO_TO_LINE: Command = {
id: 'arduino-go-to-line', id: 'arduino-go-to-line',
}; };
export const TOGGLE_COMMENT: Command = { export const TOGGLE_COMMENT: Command = {
id: 'arduino-toggle-comment', id: 'arduino-toggle-comment',
}; };
export const INDENT_LINES: Command = { export const INDENT_LINES: Command = {
id: 'arduino-indent-lines', id: 'arduino-indent-lines',
}; };
export const OUTDENT_LINES: Command = { export const OUTDENT_LINES: Command = {
id: 'arduino-outdent-lines', id: 'arduino-outdent-lines',
}; };
export const FIND: Command = { export const FIND: Command = {
id: 'arduino-find', id: 'arduino-find',
}; };
export const FIND_NEXT: Command = { export const FIND_NEXT: Command = {
id: 'arduino-find-next', id: 'arduino-find-next',
}; };
export const FIND_PREVIOUS: Command = { export const FIND_PREVIOUS: Command = {
id: 'arduino-find-previous', id: 'arduino-find-previous',
}; };
export const USE_FOR_FIND: Command = { export const USE_FOR_FIND: Command = {
id: 'arduino-for-find', id: 'arduino-for-find',
}; };
export const INCREASE_FONT_SIZE: Command = { export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size', id: 'arduino-increase-font-size',
}; };
export const DECREASE_FONT_SIZE: Command = { export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size', id: 'arduino-decrease-font-size',
}; };
export const AUTO_FORMAT: Command = { export const AUTO_FORMAT: Command = {
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`. id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
}; };
} }
} }

View File

@@ -2,13 +2,13 @@ import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import { CommandHandler } from '@theia/core/lib/common/command'; import { CommandHandler } from '@theia/core/lib/common/command';
import { import {
MenuPath, MenuPath,
CompositeMenuNode, CompositeMenuNode,
SubMenuOptions, SubMenuOptions,
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
@@ -16,243 +16,231 @@ import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService } from '../../common/protocol/examples-service'; import { ExamplesService } from '../../common/protocol/examples-service';
import { import {
SketchContribution, SketchContribution,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { Board, Sketch, SketchContainer } from '../../common/protocol'; import { Board, Sketch, SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly menuManager: MainMenuManager; protected readonly menuManager: MainMenuManager;
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: ExamplesService; protected readonly examplesService: ExamplesService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
@postConstruct() @postConstruct()
init(): void { init(): void {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard) this.handleBoardChanged(selectedBoard)
); );
}
protected handleBoardChanged(board: Board | undefined): void {
// NOOP
}
registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
const groupPath =
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
} }
// Registering the same submenu multiple times has no side-effect.
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', {
order: '4',
});
}
protected handleBoardChanged(board: Board | undefined): void { registerRecursively(
// NOOP sketchContainerOrPlaceholder:
} | SketchContainer
| (Sketch | SketchContainer)[]
| string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
subMenuOptions?: SubMenuOptions | undefined
): void {
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 sketches: Sketch[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
registerMenus(registry: MenuModelRegistry): void { if (SketchContainer.is(sketchContainerOrPlaceholder)) {
try { const { label } = sketchContainerOrPlaceholder;
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222. submenuPath = [...menuPath, label];
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1; this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index]; sketches.push(...sketchContainerOrPlaceholder.sketches);
const groupPath = children.push(...sketchContainerOrPlaceholder.children);
index === 0 } else {
? [] for (const sketchOrContainer of sketchContainerOrPlaceholder) {
: ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index); if (SketchContainer.is(sketchOrContainer)) {
const parent: CompositeMenuNode = (registry as any).findGroup( children.push(sketchOrContainer);
groupPath } else {
); sketches.push(sketchOrContainer);
const examples = new CompositeMenuNode(menuId, '', { order: '4' }); }
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
} }
// Registering the same submenu multiple times has no side-effect. }
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300 children.forEach((child) =>
registry.registerSubmenu( this.registerRecursively(child, submenuPath, pushToDispose)
ArduinoMenus.FILE__EXAMPLES_SUBMENU, );
'Examples', for (const sketch of sketches) {
{ order: '4' } const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(
':'
)}--${uri}`;
const command = { id: commandId };
const handler = this.createHandler(uri);
pushToDispose.push(
this.commandRegistry.registerCommand(command, handler)
); );
this.menuRegistry.registerMenuAction(submenuPath, {
commandId,
label: sketch.name,
order: sketch.name.toLocaleLowerCase(),
});
pushToDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
);
}
} }
}
registerRecursively( protected createHandler(uri: string): CommandHandler {
sketchContainerOrPlaceholder: return {
| SketchContainer execute: async () => {
| (Sketch | SketchContainer)[] const sketch = await this.sketchService.cloneExample(uri);
| string, return this.commandService.executeCommand(
menuPath: MenuPath, OpenSketch.Commands.OPEN_SKETCH.id,
pushToDispose: DisposableCollection = new DisposableCollection(), sketch
subMenuOptions?: SubMenuOptions | undefined );
): void { },
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 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;
const commandId = `arduino-open-example-${submenuPath.join(
':'
)}--${uri}`;
const command = { id: commandId };
const handler = this.createHandler(uri);
pushToDispose.push(
this.commandRegistry.registerCommand(command, handler)
);
this.menuRegistry.registerMenuAction(submenuPath, {
commandId,
label: sketch.name,
order: sketch.name.toLocaleLowerCase(),
});
pushToDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
);
}
}
}
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
},
};
}
} }
@injectable() @injectable()
export class BuiltInExamples extends Examples { export class BuiltInExamples extends Examples {
onStart(): void { onStart(): void {
this.register(); // no `await` this.register(); // no `await`
} }
protected async register(): Promise<void> { protected async register(): Promise<void> {
let sketchContainers: SketchContainer[] | undefined; let sketchContainers: SketchContainer[] | undefined;
try { try {
sketchContainers = await this.examplesService.builtIns(); sketchContainers = await this.examplesService.builtIns();
} catch (e) { } catch (e) {
console.error('Could not initialize built-in examples.', e); console.error('Could not initialize built-in examples.', e);
this.messageService.error( this.messageService.error('Could not initialize built-in examples.');
'Could not initialize built-in examples.' return;
);
return;
}
this.toDispose.dispose();
for (const container of ['Built-in examples', ...sketchContainers]) {
this.registerRecursively(
container,
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
this.toDispose
);
}
this.menuManager.update();
} }
this.toDispose.dispose();
for (const container of ['Built-in examples', ...sketchContainers]) {
this.registerRecursively(
container,
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
this.toDispose
);
}
this.menuManager.update();
}
} }
@injectable() @injectable()
export class LibraryExamples extends Examples { export class LibraryExamples extends Examples {
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
onStart(): void { onStart(): void {
this.register(); // no `await` this.register(); // no `await`
this.notificationCenter.onLibraryInstalled(() => this.register()); this.notificationCenter.onLibraryInstalled(() => this.register());
this.notificationCenter.onLibraryUninstalled(() => this.register()); this.notificationCenter.onLibraryUninstalled(() => this.register());
} }
protected handleBoardChanged(board: Board | undefined): void { protected handleBoardChanged(board: Board | undefined): void {
this.register(board); this.register(board);
} }
protected async register( protected async register(
board: Board | undefined = this.boardsServiceClient.boardsConfig board: Board | undefined = this.boardsServiceClient.boardsConfig
.selectedBoard .selectedBoard
): Promise<void> { ): Promise<void> {
return this.queue.add(async () => { return this.queue.add(async () => {
this.toDispose.dispose(); this.toDispose.dispose();
const fqbn = board?.fqbn; const fqbn = board?.fqbn;
const name = board?.name; const name = board?.name;
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed. // Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
const { user, current, any } = await this.examplesService.installed( const { user, current, any } = await this.examplesService.installed({
{ fqbn } fqbn,
); });
if (user.length) { if (user.length) {
(user as any).unshift('Examples from Custom Libraries'); (user as any).unshift('Examples from Custom Libraries');
} }
if (name && fqbn && current.length) { if (name && fqbn && current.length) {
(current as any).unshift(`Examples for ${name}`); (current as any).unshift(`Examples for ${name}`);
} }
if (any.length) { if (any.length) {
(any as any).unshift('Examples for any board'); (any as any).unshift('Examples for any board');
} }
for (const container of user) { for (const container of user) {
this.registerRecursively( this.registerRecursively(
container, container,
ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP,
this.toDispose this.toDispose
); );
} }
for (const container of current) { for (const container of current) {
this.registerRecursively( this.registerRecursively(
container, container,
ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP, ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP,
this.toDispose this.toDispose
); );
} }
for (const container of any) { for (const container of any) {
this.registerRecursively( this.registerRecursively(
container, container,
ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP, ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP,
this.toDispose this.toDispose
); );
} }
this.menuManager.update(); this.menuManager.update();
}); });
} }
} }

View File

@@ -6,167 +6,160 @@ import { CommandHandler } from '@theia/core/lib/common/command';
import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service'; import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry, CommandRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class Help extends Contribution { export class Help extends Contribution {
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(WindowService) @inject(WindowService)
protected readonly windowService: WindowService; protected readonly windowService: WindowService;
@inject(QuickInputService) @inject(QuickInputService)
protected readonly quickInputService: QuickInputService; protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
const open = (url: string) => const open = (url: string) =>
this.windowService.openNewWindow(url, { external: true }); this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) => const createOpenHandler = (url: string) =>
<CommandHandler>{ <CommandHandler>{
execute: () => open(url), execute: () => open(url),
}; };
registry.registerCommand( registry.registerCommand(
Help.Commands.GETTING_STARTED, Help.Commands.GETTING_STARTED,
createOpenHandler('https://www.arduino.cc/en/Guide') createOpenHandler('https://www.arduino.cc/en/Guide')
); );
registry.registerCommand( registry.registerCommand(
Help.Commands.ENVIRONMENT, Help.Commands.ENVIRONMENT,
createOpenHandler('https://www.arduino.cc/en/Guide/Environment') createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
); );
registry.registerCommand( registry.registerCommand(
Help.Commands.TROUBLESHOOTING, Help.Commands.TROUBLESHOOTING,
createOpenHandler('https://support.arduino.cc/hc/en-us') createOpenHandler('https://support.arduino.cc/hc/en-us')
); );
registry.registerCommand( registry.registerCommand(
Help.Commands.REFERENCE, Help.Commands.REFERENCE,
createOpenHandler('https://www.arduino.cc/reference/en/') createOpenHandler('https://www.arduino.cc/reference/en/')
); );
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, { registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
execute: async () => { execute: async () => {
let searchFor: string | undefined = undefined; let searchFor: string | undefined = undefined;
const { currentEditor } = this.editorManager; const { currentEditor } = this.editorManager;
if ( if (currentEditor && currentEditor.editor instanceof MonacoEditor) {
currentEditor && const codeEditor = currentEditor.editor.getControl();
currentEditor.editor instanceof MonacoEditor const selection = codeEditor.getSelection();
) { const model = codeEditor.getModel();
const codeEditor = currentEditor.editor.getControl(); if (model && selection && !monaco.Range.isEmpty(selection)) {
const selection = codeEditor.getSelection(); searchFor = model.getValueInRange(selection);
const model = codeEditor.getModel(); }
if ( }
model && if (!searchFor) {
selection && searchFor = await this.quickInputService.open({
!monaco.Range.isEmpty(selection) prompt: 'Search on Arduino.cc',
) { placeHolder: 'Type a keyword',
searchFor = model.getValueInRange(selection); });
} }
} if (searchFor) {
if (!searchFor) { return open(
searchFor = await this.quickInputService.open({ `https://www.arduino.cc/search?q=${encodeURIComponent(
prompt: 'Search on Arduino.cc', searchFor
placeHolder: 'Type a keyword', )}&tab=reference`
}); );
} }
if (searchFor) { },
return open( });
`https://www.arduino.cc/search?q=${encodeURIComponent( registry.registerCommand(
searchFor Help.Commands.FAQ,
)}&tab=reference` createOpenHandler('https://support.arduino.cc/hc/en-us')
); );
} registry.registerCommand(
}, Help.Commands.VISIT_ARDUINO,
}); createOpenHandler('https://www.arduino.cc/')
registry.registerCommand( );
Help.Commands.FAQ, }
createOpenHandler('https://support.arduino.cc/hc/en-us')
);
registry.registerCommand(
Help.Commands.VISIT_ARDUINO,
createOpenHandler('https://www.arduino.cc/')
);
}
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.GETTING_STARTED.id, commandId: Help.Commands.GETTING_STARTED.id,
order: '0', order: '0',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.ENVIRONMENT.id, commandId: Help.Commands.ENVIRONMENT.id,
order: '1', order: '1',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.TROUBLESHOOTING.id, commandId: Help.Commands.TROUBLESHOOTING.id,
order: '2', order: '2',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.REFERENCE.id, commandId: Help.Commands.REFERENCE.id,
order: '3', order: '3',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FIND_IN_REFERENCE.id, commandId: Help.Commands.FIND_IN_REFERENCE.id,
order: '4', order: '4',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FAQ.id, commandId: Help.Commands.FAQ.id,
order: '5', order: '5',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.VISIT_ARDUINO.id, commandId: Help.Commands.VISIT_ARDUINO.id,
order: '6', order: '6',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id, command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F', keybinding: 'CtrlCmd+Shift+F',
}); });
} }
} }
export namespace Help { export namespace Help {
export namespace Commands { export namespace Commands {
export const GETTING_STARTED: Command = { export const GETTING_STARTED: Command = {
id: 'arduino-getting-started', id: 'arduino-getting-started',
label: 'Getting Started', label: 'Getting Started',
category: 'Arduino', category: 'Arduino',
}; };
export const ENVIRONMENT: Command = { export const ENVIRONMENT: Command = {
id: 'arduino-environment', id: 'arduino-environment',
label: 'Environment', label: 'Environment',
category: 'Arduino', category: 'Arduino',
}; };
export const TROUBLESHOOTING: Command = { export const TROUBLESHOOTING: Command = {
id: 'arduino-troubleshooting', id: 'arduino-troubleshooting',
label: 'Troubleshooting', label: 'Troubleshooting',
category: 'Arduino', category: 'Arduino',
}; };
export const REFERENCE: Command = { export const REFERENCE: Command = {
id: 'arduino-reference', id: 'arduino-reference',
label: 'Reference', label: 'Reference',
category: 'Arduino', category: 'Arduino',
}; };
export const FIND_IN_REFERENCE: Command = { export const FIND_IN_REFERENCE: Command = {
id: 'arduino-find-in-reference', id: 'arduino-find-in-reference',
label: 'Find in Reference', label: 'Find in Reference',
category: 'Arduino', category: 'Arduino',
}; };
export const FAQ: Command = { export const FAQ: Command = {
id: 'arduino-faq', id: 'arduino-faq',
label: 'Frequently Asked Questions', label: 'Frequently Asked Questions',
category: 'Arduino', category: 'Arduino',
}; };
export const VISIT_ARDUINO: Command = { export const VISIT_ARDUINO: Command = {
id: 'arduino-visit-arduino', id: 'arduino-visit-arduino',
label: 'Visit Arduino.cc', label: 'Visit Arduino.cc',
category: 'Arduino', category: 'Arduino',
}; };
} }
} }

View File

@@ -5,8 +5,8 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser';
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu'; import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { LibraryPackage, LibraryService } from '../../common/protocol'; import { LibraryPackage, LibraryService } from '../../common/protocol';
@@ -18,209 +18,198 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class IncludeLibrary extends SketchContribution { export class IncludeLibrary extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(LibraryService) @inject(LibraryService)
protected readonly libraryService: LibraryService; protected readonly libraryService: LibraryService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
onStart(): void { onStart(): void {
this.updateMenuActions(); this.updateMenuActions();
this.boardsServiceClient.onBoardsConfigChanged(() => this.boardsServiceClient.onBoardsConfigChanged(() =>
this.updateMenuActions() this.updateMenuActions()
); );
this.notificationCenter.onLibraryInstalled(() => this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
this.updateMenuActions() this.notificationCenter.onLibraryUninstalled(() =>
); this.updateMenuActions()
this.notificationCenter.onLibraryUninstalled(() => );
this.updateMenuActions() }
);
registerMenus(registry: MenuModelRegistry): void {
// `Include Library` submenu
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
// `Manage Libraries...` group.
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...',
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
this.includeLibrary(arg);
}
},
});
}
protected async updateMenuActions(): Promise<void> {
return this.queue.add(async () => {
this.toDispose.dispose();
this.mainMenuManager.update();
const libraries: LibraryPackage[] = [];
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
// Show all libraries, when no board is selected.
// Otherwise, show libraries only for the selected board.
libraries.push(...(await this.libraryService.list({ fqbn })));
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// `Add .ZIP Library...`
// TODO: implement it
// `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(this.registerLibrary(library, userMenuPath));
}
for (const library of rest) {
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
}
this.mainMenuManager.update();
});
}
protected registerLibrary(
libraryOrPlaceholder: LibraryPackage | string,
menuPath: MenuPath
): Disposable {
if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(
menuPath,
libraryOrPlaceholder
);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
);
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId };
const handler = {
execute: () =>
this.commandRegistry.executeCommand(
IncludeLibrary.Commands.INCLUDE_LIBRARY.id,
libraryOrPlaceholder
),
};
const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(menuAction)
)
);
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.
// Otherwise, we pick the editor of the main sketch file.
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
const editor = this.editorManager.currentEditor?.editor;
if (editor instanceof MonacoEditor) {
if (
sketch.additionalFileUris.some((uri) => uri === editor.uri.toString())
) {
codeEditor = editor.getControl();
}
} }
registerMenus(registry: MenuModelRegistry): void { if (!codeEditor) {
// `Include Library` submenu const widget = await this.editorManager.open(new URI(sketch.mainFileUri));
const includeLibMenuPath = [ if (widget.editor instanceof MonacoEditor) {
...ArduinoMenus.SKETCH__UTILS_GROUP, codeEditor = widget.editor.getControl();
'0_include', }
];
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
// `Manage Libraries...` group.
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...',
});
} }
registerCommands(registry: CommandRegistry): void { if (!codeEditor) {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, { return;
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
this.includeLibrary(arg);
}
},
});
} }
protected async updateMenuActions(): Promise<void> { const textModel = codeEditor.getModel();
return this.queue.add(async () => { if (!textModel) {
this.toDispose.dispose(); return;
this.mainMenuManager.update();
const libraries: LibraryPackage[] = [];
const fqbn =
this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
// Show all libraries, when no board is selected.
// Otherwise, show libraries only for the selected board.
libraries.push(...(await this.libraryService.list({ fqbn })));
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// `Add .ZIP Library...`
// TODO: implement it
// `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(
this.registerLibrary(library, userMenuPath)
);
}
for (const library of rest) {
this.toDispose.push(
this.registerLibrary(library, packageMenuPath)
);
}
this.mainMenuManager.update();
});
}
protected registerLibrary(
libraryOrPlaceholder: LibraryPackage | string,
menuPath: MenuPath
): Disposable {
if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(
menuPath,
libraryOrPlaceholder
);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
);
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId };
const handler = {
execute: () =>
this.commandRegistry.executeCommand(
IncludeLibrary.Commands.INCLUDE_LIBRARY.id,
libraryOrPlaceholder
),
};
const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(menuAction)
)
);
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.
// Otherwise, we pick the editor of the main sketch file.
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
const editor = this.editorManager.currentEditor?.editor;
if (editor instanceof MonacoEditor) {
if (
sketch.additionalFileUris.some(
(uri) => uri === editor.uri.toString()
)
) {
codeEditor = editor.getControl();
}
}
if (!codeEditor) {
const widget = await this.editorManager.open(
new URI(sketch.mainFileUri)
);
if (widget.editor instanceof MonacoEditor) {
codeEditor = widget.editor.getControl();
}
}
if (!codeEditor) {
return;
}
const textModel = codeEditor.getModel();
if (!textModel) {
return;
}
const cursorState = codeEditor.getSelections() || [];
const eol = textModel.getEOL();
const includes = library.includes.slice();
includes.push(''); // For the trailing new line.
const text = includes
.map((include) => (include ? `#include <${include}>` : eol))
.join(eol);
textModel.pushStackElement(); // Start a fresh operation.
textModel.pushEditOperations(
cursorState,
[
{
range: new monaco.Range(1, 1, 1, 1),
text,
forceMoveMarkers: true,
},
],
() => cursorState
);
textModel.pushStackElement(); // Make it undoable.
} }
const cursorState = codeEditor.getSelections() || [];
const eol = textModel.getEOL();
const includes = library.includes.slice();
includes.push(''); // For the trailing new line.
const text = includes
.map((include) => (include ? `#include <${include}>` : eol))
.join(eol);
textModel.pushStackElement(); // Start a fresh operation.
textModel.pushEditOperations(
cursorState,
[
{
range: new monaco.Range(1, 1, 1, 1),
text,
forceMoveMarkers: true,
},
],
() => cursorState
);
textModel.pushStackElement(); // Make it undoable.
}
} }
export namespace IncludeLibrary { export namespace IncludeLibrary {
export namespace Commands { export namespace Commands {
export const INCLUDE_LIBRARY: Command = { export const INCLUDE_LIBRARY: Command = {
id: 'arduino-include-library', id: 'arduino-include-library',
}; };
} }
} }

View File

@@ -2,70 +2,69 @@ import { injectable } from 'inversify';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class NewSketch extends SketchContribution { export class NewSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, { registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(), execute: () => this.newSketch(),
}); });
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, { registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id), });
}); }
}
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id, commandId: NewSketch.Commands.NEW_SKETCH.id,
label: 'New', label: 'New',
order: '0', order: '0',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id, command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N', keybinding: 'CtrlCmd+N',
}); });
} }
registerToolbarItems(registry: TabBarToolbarRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({ registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id, id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id, command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
tooltip: 'New', tooltip: 'New',
priority: 3, priority: 3,
}); });
} }
async newSketch(): Promise<void> { async newSketch(): Promise<void> {
try { try {
const sketch = await this.sketchService.createNewSketch(); const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri)); this.workspaceService.open(new URI(sketch.uri));
} catch (e) { } catch (e) {
await this.messageService.error(e.toString()); await this.messageService.error(e.toString());
}
} }
}
} }
export namespace NewSketch { export namespace NewSketch {
export namespace Commands { export namespace Commands {
export const NEW_SKETCH: Command = { export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch', id: 'arduino-new-sketch',
}; };
export const NEW_SKETCH__TOOLBAR: Command = { export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar', id: 'arduino-new-sketch--toolbar',
}; };
} }
} }

View File

@@ -1,14 +1,14 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol'; import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { import {
SketchContribution, SketchContribution,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
Sketch, Sketch,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
@@ -17,78 +17,78 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class OpenRecentSketch extends SketchContribution { export class OpenRecentSketch extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(WorkspaceServer) @inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer; protected readonly workspaceServer: WorkspaceServer;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>(); protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void { onStart(): void {
const refreshMenu = (sketches: Sketch[]) => { const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches); this.register(sketches);
this.mainMenuManager.update(); this.mainMenuManager.update();
}; };
this.notificationCenter.onRecentSketchesChanged(({ sketches }) => this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
refreshMenu(sketches) refreshMenu(sketches)
); );
this.sketchService.recentlyOpenedSketches().then(refreshMenu); this.sketchService.recentlyOpenedSketches().then(refreshMenu);
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu( registry.registerSubmenu(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
'Open Recent', 'Open Recent',
{ order: '2' } { order: '2' }
); );
} }
protected register(sketches: Sketch[]): void { protected register(sketches: Sketch[]): void {
const order = 0; const order = 0;
for (const sketch of sketches) { for (const sketch of sketches) {
const { uri } = sketch; const { uri } = sketch;
const toDispose = this.toDisposeBeforeRegister.get(uri); const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) { if (toDispose) {
toDispose.dispose(); toDispose.dispose();
} }
const command = { id: `arduino-open-recent--${uri}` }; const command = { id: `arduino-open-recent--${uri}` };
const handler = { const handler = {
execute: () => execute: () =>
this.commandRegistry.executeCommand( this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id, OpenSketch.Commands.OPEN_SKETCH.id,
sketch sketch
), ),
}; };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
{ {
commandId: command.id, commandId: command.id,
label: sketch.name, label: sketch.name,
order: String(order), order: String(order),
}
);
this.toDisposeBeforeRegister.set(
sketch.uri,
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
} }
);
this.toDisposeBeforeRegister.set(
sketch.uri,
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
} }
}
} }

View File

@@ -3,54 +3,54 @@ import { remote } from 'electron';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class OpenSketchExternal extends SketchContribution { export class OpenSketchExternal extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, { registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
execute: () => this.openExternal(), execute: () => this.openExternal(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, { registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id, commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
label: 'Show Sketch Folder', label: 'Show Sketch Folder',
order: '0', order: '0',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id, command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
keybinding: 'CtrlCmd+Alt+K', keybinding: 'CtrlCmd+Alt+K',
}); });
} }
protected async openExternal(): Promise<void> { protected async openExternal(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile(); const uri = await this.sketchServiceClient.currentSketchFile();
if (uri) { if (uri) {
const exists = this.fileService.exists(new URI(uri)); const exists = this.fileService.exists(new URI(uri));
if (exists) { if (exists) {
const fsPath = await this.fileService.fsPath(new URI(uri)); const fsPath = await this.fileService.fsPath(new URI(uri));
if (fsPath) { if (fsPath) {
remote.shell.showItemInFolder(fsPath); remote.shell.showItemInFolder(fsPath);
}
}
} }
}
} }
}
} }
export namespace OpenSketchExternal { export namespace OpenSketchExternal {
export namespace Commands { export namespace Commands {
export const OPEN_EXTERNAL: Command = { export const OPEN_EXTERNAL: Command = {
id: 'arduino-open-sketch-external', id: 'arduino-open-sketch-external',
}; };
} }
} }

View File

@@ -3,20 +3,20 @@ import { remote } from 'electron';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser'; import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
Sketch, Sketch,
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service'; import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples'; import { BuiltInExamples } from './examples';
@@ -25,204 +25,194 @@ import { SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export class OpenSketch extends SketchContribution { export class OpenSketch extends SketchContribution {
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer) @inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(BuiltInExamples) @inject(BuiltInExamples)
protected readonly builtInExamples: BuiltInExamples; protected readonly builtInExamples: BuiltInExamples;
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: ExamplesService; protected readonly examplesService: ExamplesService;
@inject(Sketchbook) @inject(Sketchbook)
protected readonly sketchbook: Sketchbook; protected readonly sketchbook: Sketchbook;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: (arg) => execute: (arg) =>
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(), Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
});
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (_: Widget, target: EventTarget) => {
const container = await this.sketchService.getSketches({
exclude: ['**/hardware/**'],
}); });
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, { if (SketchContainer.isEmpty(container)) {
isVisible: (widget) => this.openSketch();
ArduinoToolbar.is(widget) && widget.side === 'left', } else {
execute: async (_: Widget, target: EventTarget) => { this.toDispose.dispose();
const container = await this.sketchService.getSketches({ if (!(target instanceof HTMLElement)) {
exclude: ['**/hardware/**'], return;
}); }
if (SketchContainer.isEmpty(container)) { const { parentElement } = target;
this.openSketch(); if (!parentElement) {
} else { return;
this.toDispose.dispose(); }
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
{ {
commandId: OpenSketch.Commands.OPEN_SKETCH.id, commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...', label: 'Open...',
}
);
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.toDispose
);
}
} catch (e) {
console.error(
'Error when collecting built-in examples.',
e
);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y:
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
};
this.contextMenuRenderer.render(options);
}
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
order: '1',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: 'Open',
priority: 4,
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
}
}
protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
extensions: ['ino', 'pde'],
},
],
});
if (!filePaths.length) {
return undefined;
}
if (filePaths.length > 1) {
this.logger.warn(
`Multiple sketches were selected: ${filePaths}. Using the first one.`
);
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(
new URI(sketchFileUri)
);
const { response } = await remote.dialog.showMessageBox({
title: 'Moving',
type: 'question',
buttons: ['Cancel', 'OK'],
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`,
});
if (response === 1) {
// OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(
name
);
const exists = await this.fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: 'Error',
message: `A folder named "${name}" already exists. Can't open sketch.`,
});
return undefined;
}
await this.fileService.createFolder(newSketchUri);
await this.fileService.move(
new URI(sketchFileUri),
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return this.sketchService.getSketchFolder(
newSketchUri.toString()
);
} }
);
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.toDispose
);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y:
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
};
this.contextMenuRenderer.render(options);
} }
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
order: '1',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: 'Open',
priority: 4,
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
} }
}
protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
extensions: ['ino', 'pde'],
},
],
});
if (!filePaths.length) {
return undefined;
}
if (filePaths.length > 1) {
this.logger.warn(
`Multiple sketches were selected: ${filePaths}. Using the first one.`
);
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
title: 'Moving',
type: 'question',
buttons: ['Cancel', 'OK'],
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`,
});
if (response === 1) {
// OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
const exists = await this.fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: 'Error',
message: `A folder named "${name}" already exists. Can't open sketch.`,
});
return undefined;
}
await this.fileService.createFolder(newSketchUri);
await this.fileService.move(
new URI(sketchFileUri),
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}
}
} }
export namespace OpenSketch { export namespace OpenSketch {
export namespace Commands { export namespace Commands {
export const OPEN_SKETCH: Command = { export const OPEN_SKETCH: Command = {
id: 'arduino-open-sketch', id: 'arduino-open-sketch',
}; };
export const OPEN_SKETCH__TOOLBAR: Command = { export const OPEN_SKETCH__TOOLBAR: Command = {
id: 'arduino-open-sketch--toolbar', id: 'arduino-open-sketch--toolbar',
}; };
} }
} }

View File

@@ -2,49 +2,49 @@ import { injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
@injectable() @injectable()
export class QuitApp extends Contribution { export class QuitApp extends Contribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
if (!isOSX) { if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, { registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit(), execute: () => remote.app.quit(),
}); });
}
} }
}
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it. // On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) { if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
commandId: QuitApp.Commands.QUIT_APP.id, commandId: QuitApp.Commands.QUIT_APP.id,
label: 'Quit', label: 'Quit',
order: '0', order: '0',
}); });
}
} }
}
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
if (!isOSX) { if (!isOSX) {
registry.registerKeybinding({ registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id, command: QuitApp.Commands.QUIT_APP.id,
keybinding: 'CtrlCmd+Q', keybinding: 'CtrlCmd+Q',
}); });
}
} }
}
} }
export namespace QuitApp { export namespace QuitApp {
export namespace Commands { export namespace Commands {
export const QUIT_APP: Command = { export const QUIT_APP: Command = {
id: 'arduino-quit-app', id: 'arduino-quit-app',
}; };
} }
} }

View File

@@ -3,129 +3,126 @@ import { remote } from 'electron';
import * as dateFormat from 'dateformat'; import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class SaveAsSketch extends SketchContribution { export class SaveAsSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, { registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: (args) => this.saveAs(args), execute: (args) => this.saveAs(args),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: 'Save As...',
order: '7',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S',
});
}
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs(
{
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return false;
} }
registerMenus(registry: MenuModelRegistry): void { const isTemp = await this.sketchService.isTemp(sketch);
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { if (!isTemp && !!execOnlyIfTemp) {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, return false;
label: 'Save As...',
order: '7',
});
} }
registerKeybindings(registry: KeybindingRegistry): void { // If target does not exist, propose a `directories.user`/${sketch.name} path
registry.registerKeybinding({ // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, const sketchDirUri = new URI(
keybinding: 'CtrlCmd+Shift+S', (await this.configService.getConfiguration()).sketchDirUri
}); );
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = exists
? sketchDirUri.resolve(
sketchDirUri
.resolve(
`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
)
.toString()
)
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return false;
} }
const destinationUri = await this.fileSystemExt.getUri(filePath);
/** if (!destinationUri) {
* Resolves `true` if the sketch was successfully saved as something. return false;
*/
async saveAs(
{
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return false;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return false;
}
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const sketchDirUri = new URI(
(await this.configService.getConfiguration()).sketchDirUri
);
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = exists
? sketchDirUri.resolve(
sketchDirUri
.resolve(
`${sketch.name}_copy_${dateFormat(
new Date(),
'yyyymmddHHMMss'
)}`
)
.toString()
)
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return false;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return false;
}
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), {
recursive: true,
});
} catch {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
}
return !!workspaceUri;
} }
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), {
recursive: true,
});
} catch {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
}
return !!workspaceUri;
}
} }
export namespace SaveAsSketch { export namespace SaveAsSketch {
export namespace Commands { export namespace Commands {
export const SAVE_AS_SKETCH: Command = { export const SAVE_AS_SKETCH: Command = {
id: 'arduino-save-as-sketch', id: 'arduino-save-as-sketch',
}; };
} }
export interface Options { export interface Options {
readonly execOnlyIfTemp?: boolean; readonly execOnlyIfTemp?: boolean;
readonly openAfterMove?: boolean; readonly openAfterMove?: boolean;
/** /**
* Ignored if `openAfterMove` is `false`. * Ignored if `openAfterMove` is `false`.
*/ */
readonly wipeOriginal?: boolean; readonly wipeOriginal?: boolean;
} }
export namespace Options { export namespace Options {
export const DEFAULT: Options = { export const DEFAULT: Options = {
execOnlyIfTemp: false, execOnlyIfTemp: false,
openAfterMove: true, openAfterMove: true,
wipeOriginal: false, wipeOriginal: false,
}; };
} }
} }

View File

@@ -3,64 +3,64 @@ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribu
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class SaveSketch extends SketchContribution { export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, { registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(), execute: () => this.saveSketch(),
}); });
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, { registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => execute: () =>
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id), registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id, commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: 'Save', label: 'Save',
order: '6', order: '6',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id, command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S', keybinding: 'CtrlCmd+S',
}); });
} }
registerToolbarItems(registry: TabBarToolbarRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({ registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id, id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id, command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
tooltip: 'Save', tooltip: 'Save',
priority: 5, priority: 5,
}); });
} }
async saveSketch(): Promise<void> { async saveSketch(): Promise<void> {
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id); return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
} }
} }
export namespace SaveSketch { export namespace SaveSketch {
export namespace Commands { export namespace Commands {
export const SAVE_SKETCH: Command = { export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch', id: 'arduino-save-sketch',
}; };
export const SAVE_SKETCH__TOOLBAR: Command = { export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar', id: 'arduino-save-sketch--toolbar',
}; };
} }
} }

View File

@@ -1,68 +1,65 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { import {
Command, Command,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry, CommandRegistry,
SketchContribution, SketchContribution,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences, SettingsDialog } from '../settings'; import { Settings as Preferences, SettingsDialog } from '../settings';
@injectable() @injectable()
export class Settings extends SketchContribution { export class Settings extends SketchContribution {
@inject(SettingsDialog) @inject(SettingsDialog)
protected readonly settingsDialog: SettingsDialog; protected readonly settingsDialog: SettingsDialog;
protected settingsOpened = false; protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, { registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => { execute: async () => {
let settings: Preferences | undefined = undefined; let settings: Preferences | undefined = undefined;
try { try {
this.settingsOpened = true; this.settingsOpened = true;
settings = await this.settingsDialog.open(); settings = await this.settingsDialog.open();
} finally { } finally {
this.settingsOpened = false; this.settingsOpened = false;
} }
if (settings) { if (settings) {
await this.settingsService.update(settings); await this.settingsService.update(settings);
await this.settingsService.save(); await this.settingsService.save();
} else { } else {
await this.settingsService.reset(); await this.settingsService.reset();
} }
}, },
isEnabled: () => !this.settingsOpened, isEnabled: () => !this.settingsOpened,
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id, commandId: Settings.Commands.OPEN.id,
label: 'Preferences...', label: 'Preferences...',
order: '0', order: '0',
}); });
registry.registerSubmenu( registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
ArduinoMenus.FILE__ADVANCED_SUBMENU, }
'Advanced'
);
}
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: Settings.Commands.OPEN.id, command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,', keybinding: 'CtrlCmd+,',
}); });
} }
} }
export namespace Settings { export namespace Settings {
export namespace Commands { export namespace Commands {
export const OPEN: Command = { export const OPEN: Command = {
id: 'arduino-settings-open', id: 'arduino-settings-open',
label: 'Open Preferences...', label: 'Open Preferences...',
category: 'Arduino', category: 'Arduino',
}; };
} }
} }

View File

@@ -4,18 +4,18 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
import { WorkspaceCommands } from '@theia/workspace/lib/browser'; import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer'; import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { import {
URI, URI,
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
open, open,
} from './contribution'; } from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
@@ -24,260 +24,254 @@ import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
@injectable() @injectable()
export class SketchControl extends SketchContribution { export class SketchControl extends SketchContribution {
@inject(ApplicationShell) @inject(ApplicationShell)
protected readonly shell: ApplicationShell; protected readonly shell: ApplicationShell;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer) @inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LocalCacheFsProvider) @inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider; protected readonly localCacheFsProvider: LocalCacheFsProvider;
protected readonly toDisposeBeforeCreateNewContextMenu = protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection(); new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand( registry.registerCommand(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR, SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
{ {
isVisible: (widget) => isVisible: (widget) =>
this.shell.getWidgets('main').indexOf(widget) !== -1, this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => { execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose(); this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = const sketch = await this.sketchServiceClient.currentSketch();
await this.sketchServiceClient.currentSketch(); if (!sketch) {
if (!sketch) { return;
return; }
}
const target = document.getElementById( const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
); );
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) {
return; return;
} }
const { parentElement } = target; const { parentElement } = target;
if (!parentElement) { if (!parentElement) {
return; return;
} }
const { mainFileUri, rootFolderFileUris } = const { mainFileUri, rootFolderFileUris } =
await this.sketchService.loadSketch(sketch.uri); await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...rootFolderFileUris]; const uris = [mainFileUri, ...rootFolderFileUris];
const currentSketch = const currentSketch =
await this.sketchesServiceClient.currentSketch(); await this.sketchesServiceClient.currentSketch();
const parentsketchUri = this.editorManager.currentEditor const parentsketchUri = this.editorManager.currentEditor
?.getResourceUri() ?.getResourceUri()
?.toString(); ?.toString();
const parentsketch = const parentsketch = await this.sketchService.getSketchFolder(
await this.sketchService.getSketchFolder( parentsketchUri || ''
parentsketchUri || '' );
);
// if the current file is in the current opened sketch, show extra menus // if the current file is in the current opened sketch, show extra menus
if ( if (
currentSketch && currentSketch &&
parentsketch && parentsketch &&
parentsketch.uri === currentSketch.uri && parentsketch.uri === currentSketch.uri &&
(await this.allowRename(parentsketch.uri)) (await this.allowRename(parentsketch.uri))
) { ) {
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{ {
commandId: WorkspaceCommands.FILE_RENAME.id, commandId: WorkspaceCommands.FILE_RENAME.id,
label: 'Rename', label: 'Rename',
order: '1', order: '1',
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction( this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_RENAME WorkspaceCommands.FILE_RENAME
) )
) )
); );
} else { } else {
const renamePlaceholder = new PlaceholderMenuNode( const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Rename' 'Rename'
); );
this.menuRegistry.registerMenuNode( this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
renamePlaceholder renamePlaceholder
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuNode( this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
renamePlaceholder.id )
) );
) }
);
}
if ( if (
currentSketch && currentSketch &&
parentsketch && parentsketch &&
parentsketch.uri === currentSketch.uri && parentsketch.uri === currentSketch.uri &&
(await this.allowDelete(parentsketch.uri)) (await this.allowDelete(parentsketch.uri))
) { ) {
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{ {
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window. commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: 'Delete', label: 'Delete',
order: '2', order: '2',
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction( this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_DELETE WorkspaceCommands.FILE_DELETE
) )
) )
); );
} else { } else {
const deletePlaceholder = new PlaceholderMenuNode( const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Delete' 'Delete'
); );
this.menuRegistry.registerMenuNode( this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
deletePlaceholder deletePlaceholder
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuNode( this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
deletePlaceholder.id )
) );
) }
);
}
for (let i = 0; i < uris.length; i++) { for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]); const uri = new URI(uris[i]);
// focus on the opened sketch // focus on the opened sketch
const command = { const command = {
id: `arduino-focus-file--${uri.toString()}`, id: `arduino-focus-file--${uri.toString()}`,
}; };
const handler = { const handler = {
execute: () => open(this.openerService, uri), execute: () => open(this.openerService, uri),
}; };
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
registry.registerCommand(command, handler) registry.registerCommand(command, handler)
); );
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP,
{ {
commandId: command.id, commandId: command.id,
label: this.labelProvider.getName(uri), label: this.labelProvider.getName(uri),
order: `${i}`, order: `${i}`,
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command) this.menuRegistry.unregisterMenuAction(command)
) )
); );
} }
const options = { const options = {
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT, menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
anchor: { anchor: {
x: parentElement.getBoundingClientRect().left, x: parentElement.getBoundingClientRect().left,
y: y:
parentElement.getBoundingClientRect().top + parentElement.getBoundingClientRect().top +
parentElement.offsetHeight, parentElement.offsetHeight,
}, },
}; };
this.contextMenuRenderer.render(options); this.contextMenuRenderer.render(options);
}, },
} }
); );
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.NEW_FILE.id,
label: 'New Tab',
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.PREVIOUS_TAB.id,
label: 'Previous Tab',
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.NEXT_TAB.id,
label: 'Next Tab',
order: '0',
}
);
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
keybinding: 'CtrlCmd+Alt+Right',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected async isCloudSketch(uri: string) {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
} }
return false;
}
registerMenus(registry: MenuModelRegistry): void { protected async allowRename(uri: string) {
registry.registerMenuAction( return !this.isCloudSketch(uri);
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, }
{
commandId: WorkspaceCommands.NEW_FILE.id,
label: 'New Tab',
order: '0',
}
);
registry.registerMenuAction( protected async allowDelete(uri: string) {
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, return !this.isCloudSketch(uri);
{ }
commandId: CommonCommands.PREVIOUS_TAB.id,
label: 'Previous Tab',
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.NEXT_TAB.id,
label: 'Next Tab',
order: '0',
}
);
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
keybinding: 'CtrlCmd+Alt+Right',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected async isCloudSketch(uri: string) {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
}
return false;
}
protected async allowRename(uri: string) {
return !this.isCloudSketch(uri);
}
protected async allowDelete(uri: string) {
return !this.isCloudSketch(uri);
}
} }
export namespace SketchControl { export namespace SketchControl {
export namespace Commands { export namespace Commands {
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = { export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
id: 'arduino-open-sketch-control--toolbar', id: 'arduino-open-sketch-control--toolbar',
iconClass: 'fa fa-caret-down', iconClass: 'fa fa-caret-down',
}; };
} }
} }

View File

@@ -10,57 +10,57 @@ import { OpenSketch } from './open-sketch';
@injectable() @injectable()
export class Sketchbook extends Examples { export class Sketchbook extends Examples {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
onStart(): void { onStart(): void {
this.sketchService.getSketches({}).then((container) => { this.sketchService.getSketches({}).then((container) => {
this.register(container); this.register(container);
this.mainMenuManager.update(); this.mainMenuManager.update();
}); });
this.sketchServiceClient.onSketchbookDidChange(() => { this.sketchServiceClient.onSketchbookDidChange(() => {
this.sketchService.getSketches({}).then((container) => { this.sketchService.getSketches({}).then((container) => {
this.register(container); this.register(container);
this.mainMenuManager.update(); this.mainMenuManager.update();
}); });
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu( registry.registerSubmenu(
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
'Sketchbook', 'Sketchbook',
{ order: '3' } { order: '3' }
);
}
protected register(container: SketchContainer): void {
this.toDispose.dispose();
this.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
this.toDispose
);
}
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
); );
} },
};
protected register(container: SketchContainer): void { }
this.toDispose.dispose();
this.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
this.toDispose
);
}
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
},
};
}
} }

View File

@@ -0,0 +1,136 @@
import { inject, injectable } from 'inversify';
import {
Command,
MenuModelRegistry,
CommandRegistry,
Contribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { UploadCertificateDialog } from '../dialogs/certificate-uploader/certificate-uploader-dialog';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoPreferences } from '../arduino-preferences';
import {
arduinoCert,
certificateList,
} from '../dialogs/certificate-uploader/utils';
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
@injectable()
export class UploadCertificate extends Contribution {
@inject(UploadCertificateDialog)
protected readonly dialog: UploadCertificateDialog;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
await this.dialog.open();
} finally {
this.dialogOpened = false;
}
},
isEnabled: () => !this.dialogOpened,
});
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
execute: async (certToRemove) => {
const certs = this.arduinoPreferences.get('arduino.board.certificates');
this.preferenceService.set(
'arduino.board.certificates',
certificateList(certs)
.filter((c) => c !== certToRemove)
.join(','),
PreferenceScope.User
);
},
isEnabled: (certToRemove) => certToRemove !== arduinoCert,
});
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
execute: async ({ fqbn, address, urls }) => {
return this.arduinoFirmwareUploader.uploadCertificates(
`-b ${fqbn} -a ${address} ${urls
.map((url: string) => `-u ${url}`)
.join(' ')}`
);
},
isEnabled: () => true,
});
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
execute: async (args: any) => {
this.contextMenuRenderer.render({
menuPath: ArduinoMenus.ROOT_CERTIFICATES__CONTEXT,
anchor: {
x: args.x,
y: args.y,
},
args: [args.cert],
});
},
isEnabled: () => true,
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadCertificate.Commands.OPEN.id,
label: UploadCertificate.Commands.OPEN.label,
order: '1',
});
registry.registerMenuAction(ArduinoMenus.ROOT_CERTIFICATES__CONTEXT, {
commandId: UploadCertificate.Commands.REMOVE_CERT.id,
label: UploadCertificate.Commands.REMOVE_CERT.label,
order: '1',
});
}
}
export namespace UploadCertificate {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-certificate-open',
label: 'Upload SSL Root Certificates',
category: 'Arduino',
};
export const OPEN_CERT_CONTEXT: Command = {
id: 'arduino-certificate-open-context',
label: 'Open context',
category: 'Arduino',
};
export const REMOVE_CERT: Command = {
id: 'arduino-certificate-remove',
label: 'Remove',
category: 'Arduino',
};
export const UPLOAD_CERT: Command = {
id: 'arduino-certificate-upload',
label: 'Upload',
category: 'Arduino',
};
}
}

View File

@@ -0,0 +1,49 @@
import { inject, injectable } from 'inversify';
import {
Command,
MenuModelRegistry,
CommandRegistry,
Contribution,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { UploadFirmwareDialog } from '../dialogs/firmware-uploader/firmware-uploader-dialog';
@injectable()
export class UploadFirmware extends Contribution {
@inject(UploadFirmwareDialog)
protected readonly dialog: UploadFirmwareDialog;
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadFirmware.Commands.OPEN, {
execute: async () => {
try {
this.dialogOpened = true;
await this.dialog.open();
} finally {
this.dialogOpened = false;
}
},
isEnabled: () => !this.dialogOpened,
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadFirmware.Commands.OPEN.id,
label: UploadFirmware.Commands.OPEN.label,
order: '0',
});
}
}
export namespace UploadFirmware {
export namespace Commands {
export const OPEN: Command = {
id: 'arduino-upload-firmware-open',
label: 'WiFi101 / WiFiNINA Firmware Updater',
category: 'Arduino',
};
}
}

View File

@@ -7,205 +7,200 @@ import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection'; import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class UploadSketch extends SketchContribution { export class UploadSketch extends SketchContribution {
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>(); protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event; readonly onDidChange = this.onDidChangeEmitter.event;
protected uploadInProgress = false; protected uploadInProgress = false;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: () => this.uploadSketch(), execute: () => this.uploadSketch(),
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
}); });
registry.registerCommand( registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
{ {
execute: () => this.uploadSketch(true), execute: () => this.uploadSketch(true),
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
} }
); );
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
isToggled: () => this.uploadInProgress, isToggled: () => this.uploadInProgress,
execute: () => execute: () =>
registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id), registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: 'Upload',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '2',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U',
});
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
keybinding: 'CtrlCmd+Shift+U',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
tooltip: 'Upload',
priority: 1,
onDidChange: this.onDidChange,
});
}
async uploadSketch(usingProgrammer = false): Promise<void> {
// even with buttons disabled, better to double check if an upload is already in progress
if (this.uploadInProgress) {
return;
} }
registerMenus(registry: MenuModelRegistry): void { // toggle the toolbar button and menu item state.
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { // uploadInProgress will be set to false whether the upload fails or not
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, this.uploadInProgress = true;
label: 'Upload', this.onDidChangeEmitter.fire();
order: '1', const sketch = await this.sketchServiceClient.currentSketch();
}); if (!sketch) {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { return;
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '2',
});
} }
let shouldAutoConnect = false;
registerKeybindings(registry: KeybindingRegistry): void { const monitorConfig = this.monitorConnection.monitorConfig;
registry.registerKeybinding({ if (monitorConfig) {
command: UploadSketch.Commands.UPLOAD_SKETCH.id, await this.monitorConnection.disconnect();
keybinding: 'CtrlCmd+U', if (this.monitorConnection.autoConnect) {
}); shouldAutoConnect = true;
registry.registerKeybinding({ }
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id, this.monitorConnection.autoConnect = false;
keybinding: 'CtrlCmd+Shift+U',
});
} }
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
this.sourceOverride(),
]);
registerToolbarItems(registry: TabBarToolbarRegistry): void { let options: CoreService.Upload.Options | undefined = undefined;
registry.registerItem({ const sketchUri = sketch.uri;
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id, const optimizeForDebug = this.editorMode.compileForDebug;
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id, const { selectedPort } = boardsConfig;
tooltip: 'Upload', const port = selectedPort?.address;
priority: 1,
onDidChange: this.onDidChange,
});
}
async uploadSketch(usingProgrammer = false): Promise<void> { if (usingProgrammer) {
// even with buttons disabled, better to double check if an upload is already in progress const programmer = selectedProgrammer;
if (this.uploadInProgress) { options = {
return; sketchUri,
} fqbn,
optimizeForDebug,
programmer,
port,
verbose,
verify,
sourceOverride,
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port,
verbose,
verify,
sourceOverride,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {
await this.coreService.upload(options);
}
this.messageService.info('Done uploading.', { timeout: 3000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
// toggle the toolbar button and menu item state. if (monitorConfig) {
// uploadInProgress will be set to false whether the upload fails or not const { board, port } = monitorConfig;
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
let shouldAutoConnect = false;
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
if (this.monitorConnection.autoConnect) {
shouldAutoConnect = true;
}
this.monitorConnection.autoConnect = false;
}
try { try {
const { boardsConfig } = this.boardsServiceClientImpl; await this.boardsServiceClientImpl.waitUntilAvailable(
const [ Object.assign(board, { port }),
fqbn, 10_000
{ selectedProgrammer }, );
verify, if (shouldAutoConnect) {
verbose, // Enabling auto-connect will trigger a connect.
sourceOverride, this.monitorConnection.autoConnect = true;
] = await Promise.all([ } else {
this.boardsDataStore.appendConfigToFqbn( await this.monitorConnection.connect(monitorConfig);
boardsConfig.selectedBoard?.fqbn }
), } catch (waitError) {
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), this.messageService.error(
this.preferences.get('arduino.upload.verify'), `Could not reconnect to serial monitor. ${waitError.toString()}`
this.preferences.get('arduino.upload.verbose'), );
this.sourceOverride(),
]);
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort?.address;
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
optimizeForDebug,
programmer,
port,
verbose,
verify,
sourceOverride,
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port,
verbose,
verify,
sourceOverride,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {
await this.coreService.upload(options);
}
this.messageService.info('Done uploading.', { timeout: 3000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
if (monitorConfig) {
const { board, port } = monitorConfig;
try {
await this.boardsServiceClientImpl.waitUntilAvailable(
Object.assign(board, { port }),
10_000
);
if (shouldAutoConnect) {
// Enabling auto-connect will trigger a connect.
this.monitorConnection.autoConnect = true;
} else {
await this.monitorConnection.connect(monitorConfig);
}
} catch (waitError) {
this.messageService.error(
`Could not reconnect to serial monitor. ${waitError.toString()}`
);
}
}
} }
}
} }
}
} }
export namespace UploadSketch { export namespace UploadSketch {
export namespace Commands { export namespace Commands {
export const UPLOAD_SKETCH: Command = { export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch', id: 'arduino-upload-sketch',
}; };
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = { export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
id: 'arduino-upload-sketch-using-programmer', id: 'arduino-upload-sketch-using-programmer',
}; };
export const UPLOAD_SKETCH_TOOLBAR: Command = { export const UPLOAD_SKETCH_TOOLBAR: Command = {
id: 'arduino-upload-sketch--toolbar', id: 'arduino-upload-sketch--toolbar',
}; };
} }
} }

View File

@@ -6,140 +6,138 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
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 { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class VerifySketch extends SketchContribution { export class VerifySketch extends SketchContribution {
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>(); protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event; readonly onDidChange = this.onDidChangeEmitter.event;
protected verifyInProgress = false; protected verifyInProgress = false;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch(), execute: () => this.verifySketch(),
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch(true), execute: () => this.verifySketch(true),
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
}); });
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.verifyInProgress, isEnabled: () => !this.verifyInProgress,
isToggled: () => this.verifyInProgress, isToggled: () => this.verifyInProgress,
execute: () => execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id), registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: 'Verify/Compile',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
label: 'Export compiled Binary',
order: '3',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R',
});
registry.registerKeybinding({
command: VerifySketch.Commands.EXPORT_BINARIES.id,
keybinding: 'CtrlCmd+Alt+S',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
tooltip: 'Verify',
priority: 0,
onDidChange: this.onDidChange,
});
}
async verifySketch(exportBinaries?: boolean): Promise<void> {
// even with buttons disabled, better to double check if a verify is already in progress
if (this.verifyInProgress) {
return;
} }
registerMenus(registry: MenuModelRegistry): void { // toggle the toolbar button and menu item state.
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { // verifyInProgress will be set to false whether the compilation fails or not
commandId: VerifySketch.Commands.VERIFY_SKETCH.id, this.verifyInProgress = true;
label: 'Verify/Compile', this.onDidChangeEmitter.fire();
order: '0', const sketch = await this.sketchServiceClient.currentSketch();
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { if (!sketch) {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id, return;
label: 'Export compiled Binary',
order: '3',
});
} }
try {
registerKeybindings(registry: KeybindingRegistry): void { const { boardsConfig } = this.boardsServiceClientImpl;
registry.registerKeybinding({ const [fqbn, sourceOverride] = await Promise.all([
command: VerifySketch.Commands.VERIFY_SKETCH.id, this.boardsDataStore.appendConfigToFqbn(
keybinding: 'CtrlCmd+R', boardsConfig.selectedBoard?.fqbn
}); ),
registry.registerKeybinding({ this.sourceOverride(),
command: VerifySketch.Commands.EXPORT_BINARIES.id, ]);
keybinding: 'CtrlCmd+Alt+S', const verbose = this.preferences.get('arduino.compile.verbose');
}); const compilerWarnings = this.preferences.get('arduino.compile.warnings');
} this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
registerToolbarItems(registry: TabBarToolbarRegistry): void { sketchUri: sketch.uri,
registry.registerItem({ fqbn,
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id, optimizeForDebug: this.editorMode.compileForDebug,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id, verbose,
tooltip: 'Verify', exportBinaries,
priority: 0, sourceOverride,
onDidChange: this.onDidChange, compilerWarnings,
}); });
} this.messageService.info('Done compiling.', { timeout: 3000 });
} catch (e) {
async verifySketch(exportBinaries?: boolean): Promise<void> { this.messageService.error(e.toString());
// even with buttons disabled, better to double check if a verify is already in progress } finally {
if (this.verifyInProgress) { this.verifyInProgress = false;
return; this.onDidChangeEmitter.fire();
}
// 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;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.sourceOverride(),
]);
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get(
'arduino.compile.warnings'
);
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
sourceOverride,
compilerWarnings,
});
this.messageService.info('Done compiling.', { timeout: 3000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
this.verifyInProgress = false;
this.onDidChangeEmitter.fire();
}
} }
}
} }
export namespace VerifySketch { export namespace VerifySketch {
export namespace Commands { export namespace Commands {
export const VERIFY_SKETCH: Command = { export const VERIFY_SKETCH: Command = {
id: 'arduino-verify-sketch', id: 'arduino-verify-sketch',
}; };
export const EXPORT_BINARIES: Command = { export const EXPORT_BINARIES: Command = {
id: 'arduino-export-binaries', id: 'arduino-export-binaries',
}; };
export const VERIFY_SKETCH_TOOLBAR: Command = { export const VERIFY_SKETCH_TOOLBAR: Command = {
id: 'arduino-verify-sketch--toolbar', id: 'arduino-verify-sketch--toolbar',
}; };
} }
} }

View File

@@ -1,462 +1,469 @@
import { injectable } from 'inversify'; import { injectable, inject } from 'inversify';
import * as createPaths from './create-paths'; import * as createPaths from './create-paths';
import { posix, splitSketchPath } from './create-paths'; import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
import { Create, CreateError } from './typings';
export interface ResponseResultProvider { export interface ResponseResultProvider {
(response: Response): Promise<any>; (response: Response): Promise<any>;
} }
export namespace ResponseResultProvider { export namespace ResponseResultProvider {
export const NOOP: ResponseResultProvider = async () => undefined; export const NOOP: ResponseResultProvider = async () => undefined;
export const TEXT: ResponseResultProvider = (response) => response.text(); export const TEXT: ResponseResultProvider = (response) => response.text();
export const JSON: ResponseResultProvider = (response) => response.json(); export const JSON: ResponseResultProvider = (response) => response.json();
} }
type ResourceType = 'f' | 'd'; type ResourceType = 'f' | 'd';
export let sketchCache: Create.Sketch[] = [];
@injectable() @injectable()
export class CreateApi { export class CreateApi {
protected authenticationService: AuthenticationClientService; @inject(SketchCache)
protected arduinoPreferences: ArduinoPreferences; protected sketchCache: SketchCache;
public init( protected authenticationService: AuthenticationClientService;
authenticationService: AuthenticationClientService, protected arduinoPreferences: ArduinoPreferences;
arduinoPreferences: ArduinoPreferences
): CreateApi {
this.authenticationService = authenticationService;
this.arduinoPreferences = arduinoPreferences;
return this; public init(
authenticationService: AuthenticationClientService,
arduinoPreferences: ArduinoPreferences
): CreateApi {
this.authenticationService = authenticationService;
this.arduinoPreferences = arduinoPreferences;
return this;
}
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
return {
href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`,
modified_at: sketch.modified_at,
created_at: sketch.created_at,
name: `${Create.arduino_secrets_file}`,
path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`,
mimetype: 'text/x-c++src; charset=utf-8',
type: 'file',
};
}
async sketch(id: string): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches/byID/${id}`);
url.searchParams.set('user_id', 'me');
const headers = await this.headers();
const result = await this.run<Create.Sketch>(url, {
method: 'GET',
headers,
});
return result;
}
async sketches(): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me');
const headers = await this.headers();
const result = await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
});
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return result.sketches;
}
async createSketch(
posixPath: string,
content: string = CreateApi.defaultInoContent
): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`);
const headers = await this.headers();
const payload = {
ino: btoa(content),
path: posixPath,
user_id: 'me',
};
const init = {
method: 'PUT',
body: JSON.stringify(payload),
headers,
};
const result = await this.run<Create.Sketch>(url, init);
return result;
}
async readDirectory(
posixPath: string,
options: {
recursive?: boolean;
match?: string;
skipSketchCache?: boolean;
} = {}
): Promise<Create.Resource[]> {
const url = new URL(
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
);
if (options.recursive) {
url.searchParams.set('deep', 'true');
} }
if (options.match) {
async findSketchByPath( url.searchParams.set('name_like', options.match);
path: string,
trustCache = true
): Promise<Create.Sketch | undefined> {
const skatches = sketchCache;
const sketch = skatches.find((sketch) => {
const [, spath] = splitSketchPath(sketch.path);
return path === spath;
});
if (trustCache) {
return Promise.resolve(sketch);
}
return await this.sketch({ id: sketch?.id });
} }
const headers = await this.headers();
getSketchSecretStat(sketch: Create.Sketch): Create.Resource { const cachedSketch = this.sketchCache.getSketch(posixPath);
return {
href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`,
modified_at: sketch.modified_at,
name: `${Create.arduino_secrets_file}`,
path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`,
mimetype: 'text/x-c++src; charset=utf-8',
type: 'file',
sketchId: sketch.id,
};
}
async sketch(opt: { const sketchPromise = options.skipSketchCache
id?: string; ? (cachedSketch && this.sketch(cachedSketch.id)) || Promise.resolve(null)
path?: string; : Promise.resolve(this.sketchCache.getSketch(posixPath));
}): Promise<Create.Sketch | undefined> {
let url; return Promise.all([
if (opt.id) { sketchPromise,
url = new URL(`${this.domain()}/sketches/byID/${opt.id}`); this.run<Create.RawResource[]>(url, {
} else if (opt.path) { method: 'GET',
url = new URL(`${this.domain()}/sketches/byPath${opt.path}`); headers,
} else { }),
return; ])
.then(async ([sketch, result]) => {
if (posixPath.length && posixPath !== posix.sep) {
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
result.push(this.getSketchSecretStat(sketch));
}
} }
url.searchParams.set('user_id', 'me'); return result.filter(
const headers = await this.headers(); (res) => !Create.do_not_sync_files.includes(res.name)
const result = await this.run<Create.Sketch>(url, {
method: 'GET',
headers,
});
return result;
}
async sketches(): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me');
const headers = await this.headers();
const result = await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
});
sketchCache = result.sketches;
return result.sketches;
}
async createSketch(
posixPath: string,
content: string = CreateApi.defaultInoContent
): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`);
const headers = await this.headers();
const payload = {
ino: btoa(content),
path: posixPath,
user_id: 'me',
};
const init = {
method: 'PUT',
body: JSON.stringify(payload),
headers,
};
const result = await this.run<Create.Sketch>(url, init);
return result;
}
async readDirectory(
posixPath: string,
options: { recursive?: boolean; match?: string; secrets?: boolean } = {}
): Promise<Create.Resource[]> {
const url = new URL(
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
); );
if (options.recursive) { })
url.searchParams.set('deep', 'true'); .catch((reason) => {
} if (reason?.status === 404) return [] as Create.Resource[];
if (options.match) { else throw reason;
url.searchParams.set('name_like', options.match); });
} }
const headers = await this.headers();
const sketchProm = options.secrets async createDirectory(posixPath: string): Promise<void> {
? this.sketches() const url = new URL(
: Promise.resolve(sketchCache); `${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
);
const headers = await this.headers();
await this.run(url, {
method: 'POST',
headers,
});
}
return Promise.all([ async stat(posixPath: string): Promise<Create.Resource> {
this.run<Create.RawResource[]>(url, { // The root is a directory read.
method: 'GET', if (posixPath === '/') {
headers, throw new Error('Stating the root is not supported');
}),
sketchProm,
])
.then(async ([result, sketches]) => {
if (options.secrets) {
// for every sketch with secrets, create a fake arduino_secrets.h
result.forEach(async (res) => {
if (res.type !== 'sketch') {
return;
}
const [, spath] = createPaths.splitSketchPath(res.path);
const sketch = await this.findSketchByPath(spath);
if (
sketch &&
sketch.secrets &&
sketch.secrets.length > 0
) {
result.push(this.getSketchSecretStat(sketch));
}
});
if (posixPath !== posix.sep) {
const sketch = await this.findSketchByPath(posixPath);
if (
sketch &&
sketch.secrets &&
sketch.secrets.length > 0
) {
result.push(this.getSketchSecretStat(sketch));
}
}
}
const sketchesMap: Record<string, Create.Sketch> =
sketches.reduce((prev, curr) => {
return { ...prev, [curr.path]: curr };
}, {});
// add the sketch id and isPublic to the resource
return result.map((resource) => {
return {
...resource,
sketchId: sketchesMap[resource.path]?.id || '',
isPublic:
sketchesMap[resource.path]?.is_public || false,
};
});
})
.catch((reason) => {
if (reason?.status === 404) return [] as Create.Resource[];
else throw reason;
});
} }
// The RESTful API has different endpoints for files and directories.
// The RESTful API does not provide specific error codes, only HTP 500.
// We query the parent directory and look for the file with the last segment.
const parentPosixPath = createPaths.parentPosix(posixPath);
const basename = createPaths.basename(posixPath);
async createDirectory(posixPath: string): Promise<void> { let resources;
const url = new URL( if (basename === Create.arduino_secrets_file) {
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}` const sketch = this.sketchCache.getSketch(parentPosixPath);
resources = sketch ? [this.getSketchSecretStat(sketch)] : [];
} else {
resources = await this.readDirectory(parentPosixPath, {
match: basename,
});
}
const resource = resources.find(
({ path }) => createPaths.splitSketchPath(path)[1] === posixPath
);
if (!resource) {
throw new CreateError(`Not found: ${posixPath}.`, 404);
}
return resource;
}
private async toggleSecretsInclude(
path: string,
data: string,
mode: 'add' | 'remove'
) {
const includeString = `#include "${Create.arduino_secrets_file}"`;
const includeRegexp = new RegExp(includeString + '\\s*', 'g');
const basename = createPaths.basename(path);
if (mode === 'add') {
const doesIncludeSecrets = includeRegexp.test(data);
if (doesIncludeSecrets) {
return data;
}
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
if (
sketch &&
(sketch.name + '.ino' === basename ||
sketch.name + '.pde' === basename) &&
sketch.secrets &&
sketch.secrets.length > 0
) {
return includeString + '\n' + data;
}
} else if (mode === 'remove') {
return data.replace(includeRegexp, '');
}
return data;
}
async readFile(posixPath: string): Promise<string> {
const basename = createPaths.basename(posixPath);
if (basename === Create.arduino_secrets_file) {
const parentPosixPath = createPaths.parentPosix(posixPath);
//retrieve the sketch id from the cache
const cacheSketch = this.sketchCache.getSketch(parentPosixPath);
if (!cacheSketch) {
throw new Error(`Unable to find sketch ${parentPosixPath} in cache`);
}
// get a fresh copy of the sketch in order to guarantee fresh secrets
const sketch = await this.sketch(cacheSketch.id);
if (!sketch) {
throw new Error(
`Unable to get a fresh copy of the sketch ${cacheSketch.id}`
); );
}
this.sketchCache.addSketch(sketch);
let file = '';
if (sketch && sketch.secrets) {
for (const item of sketch.secrets) {
file += `#define ${item.name} "${item.value}"\r\n`;
}
}
return file;
}
const url = new URL(
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
);
const headers = await this.headers();
const result = await this.run<{ data: string; path: string }>(url, {
method: 'GET',
headers,
});
let { data } = result;
// add includes to main arduino file
data = await this.toggleSecretsInclude(posixPath, atob(data), 'add');
return data;
}
async writeFile(
posixPath: string,
content: string | Uint8Array
): Promise<void> {
const basename = createPaths.basename(posixPath);
if (basename === Create.arduino_secrets_file) {
const parentPosixPath = createPaths.parentPosix(posixPath);
const sketch = this.sketchCache.getSketch(parentPosixPath);
if (sketch) {
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
const headers = await this.headers(); const headers = await this.headers();
await this.run(url, {
method: 'POST',
headers,
});
}
async stat(posixPath: string): Promise<Create.Resource> { // parse the secret file
// The root is a directory read. const secrets = (
if (posixPath === '/') { typeof content === 'string'
throw new Error('Stating the root is not supported'); ? content
} : new TextDecoder().decode(content)
// The RESTful API has different endpoints for files and directories. )
// The RESTful API does not provide specific error codes, only HTP 500. .split(/\r?\n/)
// We query the parent directory and look for the file with the last segment. .reduce((prev, curr) => {
const parentPosixPath = createPaths.parentPosix(posixPath); // check if the line contains a secret
const basename = createPaths.basename(posixPath); const secret = curr.split('SECRET_')[1] || null;
if (!secret) {
let resources; return prev;
if (basename === Create.arduino_secrets_file) {
const sketch = await this.findSketchByPath(parentPosixPath);
resources = sketch ? [this.getSketchSecretStat(sketch)] : [];
} else {
resources = await this.readDirectory(parentPosixPath, {
match: basename,
});
}
resources.sort((left, right) => left.path.length - right.path.length);
const resource = resources.find(({ name }) => name === basename);
if (!resource) {
throw new CreateError(`Not found: ${posixPath}.`, 404);
}
return resource;
}
async readFile(posixPath: string): Promise<string> {
const basename = createPaths.basename(posixPath);
if (basename === Create.arduino_secrets_file) {
const parentPosixPath = createPaths.parentPosix(posixPath);
const sketch = await this.findSketchByPath(parentPosixPath, false);
let file = '';
if (sketch && sketch.secrets) {
for (const item of sketch?.secrets) {
file += `#define ${item.name} "${item.value}"\r\n`;
}
} }
return file; const regexp = /(\S*)\s+([\S\s]*)/g;
} const tokens = regexp.exec(secret) || [];
const name = tokens[1].length > 0 ? `SECRET_${tokens[1]}` : '';
const url = new URL( let value = '';
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}` if (tokens[2].length > 0) {
); value = JSON.parse(
const headers = await this.headers(); JSON.stringify(
const result = await this.run<{ data: string }>(url, { tokens[2].replace(/^['"]?/g, '').replace(/['"]?$/g, '')
method: 'GET',
headers,
});
const { data } = result;
return atob(data);
}
async writeFile(
posixPath: string,
content: string | Uint8Array
): Promise<void> {
const basename = createPaths.basename(posixPath);
if (basename === Create.arduino_secrets_file) {
const parentPosixPath = createPaths.parentPosix(posixPath);
const sketch = await this.findSketchByPath(parentPosixPath);
if (sketch) {
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
const headers = await this.headers();
// parse the secret file
const secrets = (
typeof content === 'string'
? content
: new TextDecoder().decode(content)
) )
.split(/\r?\n/) );
.reduce((prev, curr) => {
// check if the line contains a secret
const secret = curr.split('SECRET_')[1] || null;
if (!secret) {
return prev;
}
const regexp = /(\S*)\s+([\S\s]*)/g;
const tokens = regexp.exec(secret) || [];
const name =
tokens[1].length > 0 ? `SECRET_${tokens[1]}` : '';
let value = '';
if (tokens[2].length > 0) {
value = JSON.parse(
JSON.stringify(
tokens[2]
.replace(/^['"]?/g, '')
.replace(/['"]?$/g, '')
)
);
}
if (name.length === 0 || value.length === 0) {
return prev;
}
return [...prev, { name, value }];
}, []);
const payload = {
id: sketch.id,
libraries: sketch.libraries,
secrets: { data: secrets },
};
// replace the sketch in the cache, so other calls will not overwrite each other
sketchCache = sketchCache.filter((skt) => skt.id !== sketch.id);
sketchCache.push({ ...sketch, secrets });
const init = {
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init);
} }
return;
}
const url = new URL( if (name.length === 0) {
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}` return prev;
); }
const headers = await this.headers();
const data = btoa( return [...prev, { name, value }];
typeof content === 'string' }, []);
? content
: new TextDecoder().decode(content) const payload = {
); id: sketch.id,
const payload = { data }; libraries: sketch.libraries,
secrets: { data: secrets },
};
// replace the sketch in the cache with the one we are pushing
// TODO: we should do a get after the POST, in order to be sure the cache
// is updated the most recent metadata
this.sketchCache.addSketch(sketch);
const init = { const init = {
method: 'POST', method: 'POST',
body: JSON.stringify(payload), body: JSON.stringify(payload),
headers, headers,
}; };
await this.run(url, init); await this.run(url, init);
}
return;
} }
async deleteFile(posixPath: string): Promise<void> { // do not upload "do_not_sync" files/directoris and their descendants
await this.delete(posixPath, 'f'); const segments = posixPath.split(posix.sep) || [];
if (
segments.some((segment) => Create.do_not_sync_files.includes(segment))
) {
return;
} }
async deleteDirectory(posixPath: string): Promise<void> { const url = new URL(
await this.delete(posixPath, 'd'); `${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
} );
const headers = await this.headers();
private async delete(posixPath: string, type: ResourceType): Promise<void> { let data: string =
const url = new URL( typeof content === 'string' ? content : new TextDecoder().decode(content);
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}` data = await this.toggleSecretsInclude(posixPath, data, 'remove');
);
const headers = await this.headers();
await this.run(url, {
method: 'DELETE',
headers,
});
}
async rename(fromPosixPath: string, toPosixPath: string): Promise<void> { const payload = { data: btoa(data) };
const url = new URL(`${this.domain('v3')}/files/mv`); const init = {
const headers = await this.headers(); method: 'POST',
const payload = { body: JSON.stringify(payload),
from: `$HOME/sketches_v2${fromPosixPath}`, headers,
to: `$HOME/sketches_v2${toPosixPath}`, };
}; await this.run(url, init);
const init = { }
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init, ResponseResultProvider.NOOP);
}
async editSketch({ async deleteFile(posixPath: string): Promise<void> {
id, await this.delete(posixPath, 'f');
params, }
}: {
id: string;
params: Record<string, unknown>;
}): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches/${id}`);
const headers = await this.headers(); async deleteDirectory(posixPath: string): Promise<void> {
const result = await this.run<Create.Sketch>(url, { await this.delete(posixPath, 'd');
method: 'POST', }
body: JSON.stringify({ id, ...params }),
headers,
});
return result;
}
async copy(fromPosixPath: string, toPosixPath: string): Promise<void> { private async delete(posixPath: string, type: ResourceType): Promise<void> {
const payload = { const url = new URL(
from: `$HOME/sketches_v2${fromPosixPath}`, `${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
to: `$HOME/sketches_v2${toPosixPath}`, );
}; const headers = await this.headers();
const url = new URL(`${this.domain('v3')}/files/cp`); await this.run(url, {
const headers = await this.headers(); method: 'DELETE',
const init = { headers,
method: 'POST', });
body: JSON.stringify(payload), }
headers,
};
await this.run(url, init, ResponseResultProvider.NOOP);
}
private async run<T>( async rename(fromPosixPath: string, toPosixPath: string): Promise<void> {
requestInfo: RequestInfo | URL, const url = new URL(`${this.domain('v3')}/files/mv`);
init: RequestInit | undefined, const headers = await this.headers();
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON const payload = {
): Promise<T> { from: `$HOME/sketches_v2${fromPosixPath}`,
const response = await fetch( to: `$HOME/sketches_v2${toPosixPath}`,
requestInfo instanceof URL ? requestInfo.toString() : requestInfo, };
init const init = {
); method: 'POST',
if (!response.ok) { body: JSON.stringify(payload),
let details: string | undefined = undefined; headers,
try { };
details = await response.json(); await this.run(url, init, ResponseResultProvider.NOOP);
} catch (e) { }
console.error('Cloud not get the error details.', e);
}
const { statusText, status } = response;
throw new CreateError(statusText, status, details);
}
const result = await resultProvider(response);
return result;
}
private async headers(): Promise<Record<string, string>> { async editSketch({
const token = await this.token(); id,
return { params,
'content-type': 'application/json', }: {
accept: 'application/json', id: string;
authorization: `Bearer ${token}`, params: Record<string, unknown>;
}; }): Promise<Create.Sketch> {
} const url = new URL(`${this.domain()}/sketches/${id}`);
private domain(apiVersion = 'v2'): string { const headers = await this.headers();
const endpoint = const result = await this.run<Create.Sketch>(url, {
this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint']; method: 'POST',
return `${endpoint}/${apiVersion}`; body: JSON.stringify({ id, ...params }),
} headers,
});
return result;
}
private async token(): Promise<string> { async copy(fromPosixPath: string, toPosixPath: string): Promise<void> {
return this.authenticationService.session?.accessToken || ''; const payload = {
from: `$HOME/sketches_v2${fromPosixPath}`,
to: `$HOME/sketches_v2${toPosixPath}`,
};
const url = new URL(`${this.domain('v3')}/files/cp`);
const headers = await this.headers();
const init = {
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init, ResponseResultProvider.NOOP);
}
private async run<T>(
requestInfo: RequestInfo | URL,
init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> {
const response = await fetch(
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
init
);
if (!response.ok) {
let details: string | undefined = undefined;
try {
details = await response.json();
} catch (e) {
console.error('Cloud not get the error details.', e);
}
const { statusText, status } = response;
throw new CreateError(statusText, status, details);
} }
const result = await resultProvider(response);
return result;
}
private async headers(): Promise<Record<string, string>> {
const token = await this.token();
return {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${token}`,
};
}
private domain(apiVersion = 'v2'): string {
const endpoint = this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint'];
return `${endpoint}/${apiVersion}`;
}
private async token(): Promise<string> {
return this.authenticationService.session?.accessToken || '';
}
} }
export namespace CreateApi { export namespace CreateApi {
export const defaultInoContent = `/* export const defaultInoContent = `/*
*/ */
@@ -470,75 +477,3 @@ void loop() {
`; `;
} }
export namespace Create {
export interface Sketch {
readonly name: string;
readonly path: string;
readonly modified_at: string;
readonly created_at: string;
readonly secrets?: { name: string; value: string }[];
readonly id: string;
readonly is_public: boolean;
// readonly board_fqbn: '',
// readonly board_name: '',
// readonly board_type: 'serial' | 'network' | 'cloud' | '',
readonly href?: string;
readonly libraries: string[];
// readonly tutorials: string[] | null;
// readonly types: string[] | null;
// readonly user_id: string;
}
export type ResourceType = 'sketch' | 'folder' | 'file';
export const arduino_secrets_file = 'arduino_secrets.h';
export interface Resource {
readonly name: string;
/**
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
*/
readonly path: string;
readonly type: ResourceType;
readonly sketchId: string;
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
readonly children?: number; // For 'sketch' and 'folder' types.
readonly size?: number; // For 'sketch' type only.
readonly isPublic?: boolean; // For 'sketch' type only.
readonly mimetype?: string; // For 'file' type.
readonly href?: string;
}
export namespace Resource {
export function is(arg: any): arg is Resource {
return (
!!arg &&
'name' in arg &&
typeof arg['name'] === 'string' &&
'path' in arg &&
typeof arg['path'] === 'string' &&
'type' in arg &&
typeof arg['type'] === 'string' &&
'modified_at' in arg &&
typeof arg['modified_at'] === 'string' &&
(arg['type'] === 'sketch' ||
arg['type'] === 'folder' ||
arg['type'] === 'file')
);
}
}
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
}
export class CreateError extends Error {
constructor(
message: string,
readonly status: number,
readonly details?: string
) {
super(message);
Object.setPrototypeOf(this, CreateError.prototype);
}
}

View File

@@ -2,207 +2,197 @@ import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Event } from '@theia/core/lib/common/event'; import { Event } from '@theia/core/lib/common/event';
import { 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'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
Stat, Stat,
FileType, FileType,
FileChange, FileChange,
FileWriteOptions, FileWriteOptions,
FileDeleteOptions, FileDeleteOptions,
FileOverwriteOptions, FileOverwriteOptions,
FileSystemProvider, FileSystemProvider,
FileSystemProviderError, FileSystemProviderError,
FileSystemProviderErrorCode, FileSystemProviderErrorCode,
FileSystemProviderCapabilities, FileSystemProviderCapabilities,
WatchOptions, WatchOptions,
} from '@theia/filesystem/lib/common/files'; } from '@theia/filesystem/lib/common/files';
import { import {
FileService, FileService,
FileServiceContribution, FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service'; } from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
import { Create, CreateApi } from './create-api'; import { CreateApi } from './create-api';
import { CreateUri } from './create-uri'; import { CreateUri } from './create-uri';
import { SketchesService } from '../../common/protocol'; import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
import { Create } from './typings';
export const REMOTE_ONLY_FILES = ['sketch.json'];
@injectable() @injectable()
export class CreateFsProvider export class CreateFsProvider
implements implements
FileSystemProvider, FileSystemProvider,
FrontendApplicationContribution, FrontendApplicationContribution,
FileServiceContribution FileServiceContribution
{ {
@inject(AuthenticationClientService) @inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService; protected readonly authenticationService: AuthenticationClientService;
@inject(CreateApi) @inject(CreateApi)
protected readonly createApi: CreateApi; protected readonly createApi: CreateApi;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
@inject(ArduinoPreferences) @inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences; protected readonly arduinoPreferences: ArduinoPreferences;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
readonly onFileWatchError: Event<void> = Event.None; readonly onFileWatchError: Event<void> = Event.None;
readonly onDidChangeFile: Event<readonly FileChange[]> = Event.None; readonly onDidChangeFile: Event<readonly FileChange[]> = Event.None;
readonly onDidChangeCapabilities: Event<void> = Event.None; readonly onDidChangeCapabilities: Event<void> = Event.None;
readonly capabilities: FileSystemProviderCapabilities = readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.PathCaseSensitive |
FileSystemProviderCapabilities.Access; FileSystemProviderCapabilities.Access;
onStop(): void { onStop(): void {
this.toDispose.dispose(); this.toDispose.dispose();
} }
registerFileSystemProviders(service: FileService): void { registerFileSystemProviders(service: FileService): void {
service.onWillActivateFileSystemProvider((event) => { service.onWillActivateFileSystemProvider((event) => {
if (event.scheme === CreateUri.scheme) { if (event.scheme === CreateUri.scheme) {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
service.registerProvider(CreateUri.scheme, this); service.registerProvider(CreateUri.scheme, this);
})() })()
);
}
});
}
watch(uri: URI, opts: WatchOptions): Disposable {
return Disposable.NULL;
}
async stat(uri: URI): Promise<Stat> {
if (CreateUri.equals(CreateUri.root, uri)) {
this.getCreateApi; // This will throw when not logged in.
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0,
};
}
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
}
async mkdir(uri: URI): Promise<void> {
await this.getCreateApi.createDirectory(uri.path.toString());
}
async readdir(uri: URI): Promise<[string, FileType][]> {
const resources = await this.getCreateApi.readDirectory(
uri.path.toString(),
{
secrets: true,
}
); );
return resources }
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name)) });
.map(({ name, type }) => [name, this.toFileType(type)]); }
watch(uri: URI, opts: WatchOptions): Disposable {
return Disposable.NULL;
}
async stat(uri: URI): Promise<Stat> {
if (CreateUri.equals(CreateUri.root, uri)) {
this.getCreateApi; // This will throw when not logged in.
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0,
};
} }
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
}
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> { async mkdir(uri: URI): Promise<void> {
return; await this.getCreateApi.createDirectory(uri.path.toString());
}
if (!opts.recursive) { async readdir(uri: URI): Promise<[string, FileType][]> {
throw new Error( const resources = await this.getCreateApi.readDirectory(
'Arduino Create file-system provider does not support non-recursive deletion.' uri.path.toString()
); );
} return resources.map(({ name, type }) => [name, this.toFileType(type)]);
const stat = await this.stat(uri); }
if (!stat) {
throw new FileSystemProviderError( async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
'File not found.', if (!opts.recursive) {
FileSystemProviderErrorCode.FileNotFound throw new Error(
); 'Arduino Create file-system provider does not support non-recursive deletion.'
} );
switch (stat.type) {
case FileType.Directory: {
await this.getCreateApi.deleteDirectory(uri.path.toString());
break;
}
case FileType.File: {
await this.getCreateApi.deleteFile(uri.path.toString());
break;
}
default: {
throw new FileSystemProviderError(
`Unexpected file type '${
stat.type
}' for resource: ${uri.toString()}`,
FileSystemProviderErrorCode.Unknown
);
}
}
} }
const stat = await this.stat(uri);
async rename( if (!stat) {
oldUri: URI, throw new FileSystemProviderError(
newUri: URI, 'File not found.',
options: FileOverwriteOptions FileSystemProviderErrorCode.FileNotFound
): Promise<void> { );
await this.getCreateApi.rename( }
oldUri.path.toString(), switch (stat.type) {
newUri.path.toString() case FileType.Directory: {
await this.getCreateApi.deleteDirectory(uri.path.toString());
break;
}
case FileType.File: {
await this.getCreateApi.deleteFile(uri.path.toString());
break;
}
default: {
throw new FileSystemProviderError(
`Unexpected file type '${stat.type}' for resource: ${uri.toString()}`,
FileSystemProviderErrorCode.Unknown
); );
}
}
}
async rename(
oldUri: URI,
newUri: URI,
options: FileOverwriteOptions
): Promise<void> {
await this.getCreateApi.rename(
oldUri.path.toString(),
newUri.path.toString()
);
}
async readFile(uri: URI): Promise<Uint8Array> {
const content = await this.getCreateApi.readFile(uri.path.toString());
return new TextEncoder().encode(content);
}
async writeFile(
uri: URI,
content: Uint8Array,
options: FileWriteOptions
): Promise<void> {
await this.getCreateApi.writeFile(uri.path.toString(), content);
}
async access(uri: URI, mode?: number): Promise<void> {
this.getCreateApi; // Will throw if not logged in.
}
public toFileType(type: Create.ResourceType): FileType {
switch (type) {
case 'file':
return FileType.File;
case 'sketch':
case 'folder':
return FileType.Directory;
default:
return FileType.Unknown;
}
}
private get getCreateApi(): CreateApi {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
} }
async readFile(uri: URI): Promise<Uint8Array> { return this.createApi.init(
const content = await this.getCreateApi.readFile(uri.path.toString()); this.authenticationService,
return new TextEncoder().encode(content); this.arduinoPreferences
} );
}
async writeFile(
uri: URI,
content: Uint8Array,
options: FileWriteOptions
): Promise<void> {
await this.getCreateApi.writeFile(uri.path.toString(), content);
}
async access(uri: URI, mode?: number): Promise<void> {
this.getCreateApi; // Will throw if not logged in.
}
public toFileType(type: Create.ResourceType): FileType {
switch (type) {
case 'file':
return FileType.File;
case 'sketch':
case 'folder':
return FileType.Directory;
default:
return FileType.Unknown;
}
}
private get getCreateApi(): CreateApi {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
}
return this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
}
} }

View File

@@ -2,22 +2,22 @@ export const posix = { sep: '/' };
// TODO: poor man's `path.join(path, '..')` in the browser. // TODO: poor man's `path.join(path, '..')` in the browser.
export function parentPosix(path: string): string { export function parentPosix(path: string): string {
const segments = path.split(posix.sep) || []; const segments = path.split(posix.sep) || [];
segments.pop(); segments.pop();
let modified = segments.join(posix.sep); let modified = segments.join(posix.sep);
if (path.charAt(path.length - 1) === posix.sep) { if (path.charAt(path.length - 1) === posix.sep) {
modified += posix.sep; modified += posix.sep;
} }
return modified; return modified;
} }
export function basename(path: string): string { export function basename(path: string): string {
const segments = path.split(posix.sep) || []; const segments = path.split(posix.sep) || [];
return segments.pop()!; return segments.pop()!;
} }
export function posixSegments(posixPath: string): string[] { export function posixSegments(posixPath: string): string[] {
return posixPath.split(posix.sep).filter((segment) => !!segment); return posixPath.split(posix.sep).filter((segment) => !!segment);
} }
/** /**
@@ -32,28 +32,28 @@ export function posixSegments(posixPath: string): string[] {
* ``` * ```
*/ */
export function splitSketchPath( export function splitSketchPath(
raw: string, raw: string,
sep = '/sketches_v2/' sep = '/sketches_v2/'
): [string, string] { ): [string, string] {
if (!sep) { if (!sep) {
throw new Error('Invalid separator. Cannot be zero length.'); throw new Error('Invalid separator. Cannot be zero length.');
} }
const index = raw.indexOf(sep); const index = raw.indexOf(sep);
if (index === -1) { if (index === -1) {
throw new Error(`Invalid path pattern. Raw path was '${raw}'.`); throw new Error(`Invalid path pattern. Raw path was '${raw}'.`);
} }
const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format. const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format.
const posixPath = raw.substr(index + sep.length - 1); const posixPath = raw.substr(index + sep.length - 1);
if (!posixPath) { if (!posixPath) {
throw new Error(`Could not extract POSIX path from '${raw}'.`); throw new Error(`Could not extract POSIX path from '${raw}'.`);
} }
return [createRoot, posixPath]; return [createRoot, posixPath];
} }
export function toPosixPath(raw: string): string { export function toPosixPath(raw: string): string {
if (raw === posix.sep) { if (raw === posix.sep) {
return posix.sep; // Handles the root resource case. return posix.sep; // Handles the root resource case.
} }
const [, posixPath] = splitSketchPath(raw); const [, posixPath] = splitSketchPath(raw);
return posixPath; return posixPath;
} }

View File

@@ -1,39 +1,37 @@
import { URI as Uri } from 'vscode-uri'; import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Create } from './create-api';
import { toPosixPath, parentPosix, posix } from './create-paths'; import { toPosixPath, parentPosix, posix } from './create-paths';
import { Create } from './typings';
export namespace CreateUri { export namespace CreateUri {
export const scheme = 'arduino-create'; export const scheme = 'arduino-create';
export const root = toUri(posix.sep); export const root = toUri(posix.sep);
export function toUri(posixPathOrResource: string | Create.Resource): URI { export function toUri(posixPathOrResource: string | Create.Resource): URI {
const posixPath = const posixPath =
typeof posixPathOrResource === 'string' typeof posixPathOrResource === 'string'
? posixPathOrResource ? posixPathOrResource
: toPosixPath(posixPathOrResource.path); : toPosixPath(posixPathOrResource.path);
return new URI( return new URI(Uri.parse(posixPath).with({ scheme, authority: 'create' }));
Uri.parse(posixPath).with({ scheme, authority: 'create' }) }
);
}
export function is(uri: URI): boolean { export function is(uri: URI): boolean {
return uri.scheme === scheme; return uri.scheme === scheme;
} }
export function equals(left: URI, right: URI): boolean { export function equals(left: URI, right: URI): boolean {
return is(left) && is(right) && left.toString() === right.toString(); return is(left) && is(right) && left.toString() === right.toString();
} }
export function parent(uri: URI): URI { export function parent(uri: URI): URI {
if (!is(uri)) { if (!is(uri)) {
throw new Error( throw new Error(
`Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.` `Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.`
); );
}
if (equals(uri, root)) {
return uri;
}
return toUri(parentPosix(uri.path.toString()));
} }
if (equals(uri, root)) {
return uri;
}
return toUri(parentPosix(uri.path.toString()));
}
} }

View File

@@ -0,0 +1,73 @@
export namespace Create {
export interface Sketch {
readonly name: string;
readonly path: string;
readonly modified_at: string;
readonly created_at: string;
readonly secrets?: { name: string; value: string }[];
readonly id: string;
readonly is_public: boolean;
readonly board_fqbn: '';
readonly board_name: '';
readonly board_type: 'serial' | 'network' | 'cloud' | '';
readonly href?: string;
readonly libraries: string[];
readonly tutorials: string[] | null;
readonly types: string[] | null;
readonly user_id: string;
}
export type ResourceType = 'sketch' | 'folder' | 'file';
export const arduino_secrets_file = 'arduino_secrets.h';
export const do_not_sync_files = ['.theia', 'sketch.json'];
export interface Resource {
readonly name: string;
/**
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
*/
readonly path: string;
readonly type: ResourceType;
readonly sketchId?: string;
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
readonly created_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
readonly children?: number; // For 'sketch' and 'folder' types.
readonly size?: number; // For 'sketch' type only.
readonly isPublic?: boolean; // For 'sketch' type only.
readonly mimetype?: string; // For 'file' type.
readonly href?: string;
}
export namespace Resource {
export function is(arg: any): arg is Resource {
return (
!!arg &&
'name' in arg &&
typeof arg['name'] === 'string' &&
'path' in arg &&
typeof arg['path'] === 'string' &&
'type' in arg &&
typeof arg['type'] === 'string' &&
'modified_at' in arg &&
typeof arg['modified_at'] === 'string' &&
(arg['type'] === 'sketch' ||
arg['type'] === 'folder' ||
arg['type'] === 'file')
);
}
}
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
}
export class CreateError extends Error {
constructor(
message: string,
readonly status: number,
readonly details?: string
) {
super(message);
Object.setPrototypeOf(this, CreateError.prototype);
}
}

View File

@@ -97,6 +97,7 @@
"editorWhitespace.foreground": "#bfbfbf", "editorWhitespace.foreground": "#bfbfbf",
"editor.lineHighlightBackground": "#434f5410", "editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00", "editor.selectionBackground": "#ffcb00",
"editorWidget.background": "#F7F9F9",
"focusBorder": "#7fcbcd99", "focusBorder": "#7fcbcd99",
"menubar.selectionBackground": "#ffffff", "menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121", "menubar.selectionForeground": "#212121",

View File

@@ -1,175 +0,0 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { clipboard } from 'electron';
import {
AbstractDialog,
ReactWidget,
DialogProps,
} from '@theia/core/lib/browser';
import { CreateApi } from '../create/create-api';
const RadioButton = (props: {
id: string;
changed: (evt: React.BaseSyntheticEvent) => void;
value: string;
isSelected: boolean;
isDisabled: boolean;
label: string;
}) => {
return (
<p className="RadioButton">
<input
id={props.id}
onChange={props.changed}
value={props.value}
type="radio"
checked={props.isSelected}
disabled={props.isDisabled}
/>
<label htmlFor={props.id}>{props.label}</label>
</p>
);
};
export const ShareSketchComponent = ({
treeNode,
createApi,
domain = 'https://create.arduino.cc',
}: {
treeNode: any;
createApi: CreateApi;
domain?: string;
}): React.ReactElement => {
// const [publicVisibility, setPublicVisibility] = React.useState<boolean>(
// treeNode.isPublic
// );
const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
setloading(true);
const sketch = await createApi.editSketch({
id: treeNode.sketchId,
params: {
is_public: event.target.value === 'private' ? false : true,
},
});
// setPublicVisibility(sketch.is_public);
treeNode.isPublic = sketch.is_public;
setloading(false);
};
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
const embedLink = `<iframe src="${sketchLink}?embed" style="height:510px;width:100%;margin:10px 0" frameborder=0></iframe>`;
return (
<div id="widget-container arduino-sharesketch-dialog">
<p>Choose visibility of your Sketch:</p>
<RadioButton
changed={radioChangeHandler}
id="1"
isSelected={treeNode.isPublic === false}
label="Private. Only you can view the Sketch."
value="private"
isDisabled={loading}
/>
<RadioButton
changed={radioChangeHandler}
id="2"
isSelected={treeNode.isPublic === true}
label="Public. Anyone with the link can view the Sketch."
value="public"
isDisabled={loading}
/>
{treeNode.isPublic && (
<div>
<p>Link:</p>
<div className="sketch-link">
<input
type="text"
readOnly
value={sketchLink}
className="theia-input"
/>
<button
onClick={() => clipboard.writeText(sketchLink)}
value="copy"
className="theia-button secondary"
>
Copy
</button>
</div>
<p>Embed:</p>
<div className="sketch-link-embed">
<textarea
readOnly
value={embedLink}
className="theia-input stretch"
/>
</div>
</div>
)}
</div>
);
};
@injectable()
export class ShareSketchWidget extends ReactWidget {
constructor(private treeNode: any, private createApi: CreateApi) {
super();
}
protected render(): React.ReactNode {
return (
<ShareSketchComponent
treeNode={this.treeNode}
createApi={this.createApi}
/>
);
}
}
@injectable()
export class ShareSketchDialogProps extends DialogProps {
readonly node: any;
readonly createApi: CreateApi;
}
@injectable()
export class ShareSketchDialog extends AbstractDialog<void> {
protected widget: ShareSketchWidget;
constructor(
@inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
this.widget = new ShareSketchWidget(props.node, props.createApi);
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
}

View File

@@ -1,69 +0,0 @@
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
ConfirmDialog,
ConfirmDialogProps,
DialogError,
} from '@theia/core/lib/browser/dialogs';
@injectable()
export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps {
readonly onAccept: () => Promise<void>;
}
@injectable()
export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
protected readonly doNotAskAgainCheckbox: HTMLInputElement;
constructor(
@inject(DoNotAskAgainConfirmDialogProps)
protected readonly props: DoNotAskAgainConfirmDialogProps
) {
super(props);
this.controlPanel.removeChild(this.errorMessageNode);
const doNotAskAgainNode = document.createElement('div');
doNotAskAgainNode.setAttribute('style', 'flex: 2');
this.controlPanel.insertBefore(
doNotAskAgainNode,
this.controlPanel.firstChild
);
const doNotAskAgainLabel = document.createElement('label');
doNotAskAgainLabel.classList.add('flex-line');
doNotAskAgainNode.appendChild(doNotAskAgainLabel);
doNotAskAgainLabel.textContent = "Don't ask again";
this.doNotAskAgainCheckbox = document.createElement('input');
this.doNotAskAgainCheckbox.setAttribute('align-self', 'center');
doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox);
this.doNotAskAgainCheckbox.type = 'checkbox';
}
protected async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
if (this.doNotAskAgainCheckbox.checked) {
await this.props.onAccept();
}
this.resolve(value);
Widget.detach(this);
}
}
protected setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}
}
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
export const CertificateAddComponent = ({
addCertificate,
}: {
addCertificate: (cert: string) => void;
}): React.ReactElement => {
const [value, setValue] = React.useState('');
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
return (
<form
className="certificate-add"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
addCertificate(value);
setValue('');
}}
>
<label>
<div>Add URL to fetch SSL certificate</div>
<input
className="theia-input"
placeholder="Enter URL"
type="text"
name="add"
onChange={handleChange}
value={value}
/>
</label>
</form>
);
};

View File

@@ -0,0 +1,51 @@
import * as React from 'react';
export const CertificateListComponent = ({
certificates,
selectedCerts,
setSelectedCerts,
openContextMenu,
}: {
certificates: string[];
selectedCerts: string[];
setSelectedCerts: React.Dispatch<React.SetStateAction<string[]>>;
openContextMenu: (x: number, y: number, cert: string) => void;
}): React.ReactElement => {
const handleOnChange = (event: any) => {
const target = event.target;
const newSelectedCerts = selectedCerts.filter(
(cert) => cert !== target.name
);
if (target.checked) {
newSelectedCerts.push(target.name);
}
setSelectedCerts(newSelectedCerts);
};
const handleContextMenu = (event: React.MouseEvent, cert: string) => {
openContextMenu(event.clientX, event.clientY, cert);
};
return (
<div className="certificate-list">
{certificates.map((certificate, i) => (
<label
key={i}
className="certificate-row"
onContextMenu={(e) => handleContextMenu(e, certificate)}
>
<span className="fl1">{certificate}</span>
<input
type="checkbox"
name={certificate}
checked={selectedCerts.includes(certificate)}
onChange={handleOnChange}
/>
</label>
))}
</div>
);
};

View File

@@ -0,0 +1,157 @@
import * as React from 'react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';
import { SelectBoardComponent } from './select-board-components';
import { CertificateAddComponent } from './certificate-add-new';
export const CertificateUploaderComponent = ({
availableBoards,
certificates,
addCertificate,
updatableFqbns,
uploadCertificates,
openContextMenu,
}: {
availableBoards: AvailableBoard[];
certificates: string[];
addCertificate: (cert: string) => void;
updatableFqbns: string[];
uploadCertificates: (
fqbn: string,
address: string,
urls: string[]
) => Promise<any>;
openContextMenu: (x: number, y: number, cert: string) => void;
}): React.ReactElement => {
const [installFeedback, setInstallFeedback] = React.useState<
'ok' | 'fail' | 'installing' | null
>(null);
const [showAdd, setShowAdd] = React.useState(false);
const [selectedCerts, setSelectedCerts] = React.useState<string[]>([]);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const installCertificates = async () => {
if (!selectedBoard || !selectedBoard.fqbn || !selectedBoard.port) {
return;
}
setInstallFeedback('installing');
try {
await uploadCertificates(
selectedBoard.fqbn,
selectedBoard.port.address,
selectedCerts
);
setInstallFeedback('ok');
} catch {
setInstallFeedback('fail');
}
};
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setSelectedBoard(board);
}
},
[selectedBoard]
);
return (
<>
<div className="dialogSection">
<div className="dialogRow">
<strong className="fl1">1. Select certificate to upload</strong>
<Tippy
content={
<CertificateAddComponent
addCertificate={(cert) => {
addCertificate(cert);
setShowAdd(false);
}}
/>
}
placement="bottom-end"
onClickOutside={() => setShowAdd(false)}
visible={showAdd}
interactive={true}
>
<button
type="button"
className="theia-button primary add-cert-btn"
onClick={() => {
showAdd ? setShowAdd(false) : setShowAdd(true);
}}
>
Add New <span className="fa fa-caret-down caret"></span>
</button>
</Tippy>
</div>
<div className="dialogRow">
<CertificateListComponent
certificates={certificates}
selectedCerts={selectedCerts}
setSelectedCerts={setSelectedCerts}
openContextMenu={openContextMenu}
/>
</div>
</div>
<div className="dialogSection">
<div className="dialogRow">
<strong>2. Select destination board and upload certificate</strong>
</div>
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
</div>
<div className="dialogRow">
<div className="upload-status">
{installFeedback === 'installing' && (
<div className="success">
<div className="spinner" />
Uploading certificates.
</div>
)}
{installFeedback === 'ok' && (
<div className="success">
<i className="fa fa-info status-icon" />
Cetificates uploaded.
</div>
)}
{installFeedback === 'fail' && (
<div className="warn">
<i className="fa fa-exclamation status-icon" />
Upload failed. Please try again.
</div>
)}
</div>
<button
type="button"
className="theia-button primary install-cert-btn"
onClick={installCertificates}
disabled={selectedCerts.length === 0 || !selectedBoard}
>
Upload
</button>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,190 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import { CertificateUploaderComponent } from './certificate-uploader-component';
import { ArduinoPreferences } from '../../arduino-preferences';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { certificateList, sanifyCertString } from './utils';
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
@injectable()
export class UploadCertificateDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected certificates: string[] = [];
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoPreferences.ready.then(() => {
this.certificates = certificateList(
this.arduinoPreferences.get('arduino.board.certificates')
);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.board.certificates' &&
event.newValue !== event.oldValue
) {
this.certificates = certificateList(event.newValue);
this.update();
}
});
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
private addCertificate(certificate: string) {
const certString = sanifyCertString(certificate);
if (certString.length > 0) {
this.certificates.push(sanifyCertString(certificate));
}
this.preferenceService.set(
'arduino.board.certificates',
this.certificates.join(','),
PreferenceScope.User
);
}
protected openContextMenu(x: number, y: number, cert: string): void {
this.commandRegistry.executeCommand(
'arduino-certificate-open-context',
Object.assign({}, { x, y, cert })
);
}
protected uploadCertificates(
fqbn: string,
address: string,
urls: string[]
): Promise<any> {
this.busyCallback(true);
return this.commandRegistry
.executeCommand('arduino-certificate-upload', {
fqbn,
address,
urls,
})
.finally(() => this.busyCallback(false));
}
protected render(): React.ReactNode {
return (
<CertificateUploaderComponent
availableBoards={this.availableBoards}
certificates={this.certificates}
updatableFqbns={this.updatableFqbns}
addCertificate={this.addCertificate.bind(this)}
uploadCertificates={this.uploadCertificates.bind(this)}
openContextMenu={this.openContextMenu.bind(this)}
/>
);
}
}
@injectable()
export class UploadCertificateDialogProps extends DialogProps {}
@injectable()
export class UploadCertificateDialog extends AbstractDialog<void> {
@inject(UploadCertificateDialogWidget)
protected readonly widget: UploadCertificateDialogWidget;
private busy = false;
constructor(
@inject(UploadCertificateDialogProps)
protected readonly props: UploadCertificateDialogProps
) {
super({ title: 'Upload SSL Root Certificates' });
this.contentNode.classList.add('certificate-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
if (this.busy) {
return;
}
super.close();
}
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
} else {
this.closeCrossNode.classList.remove('disabled');
}
}
}

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
type BoardOption = { value: string; label: string };
export const SelectBoardComponent = ({
availableBoards,
updatableFqbns,
onBoardSelect,
selectedBoard,
busy,
}: {
availableBoards: AvailableBoard[];
updatableFqbns: string[];
onBoardSelect: (board: AvailableBoard | null) => void;
selectedBoard: AvailableBoard | null;
busy: boolean;
}): React.ReactElement => {
const [selectOptions, setSelectOptions] = React.useState<BoardOption[]>([]);
const [selectBoardPlaceholder, setSelectBoardPlaceholder] =
React.useState('');
const selectOption = React.useCallback(
(boardOpt: BoardOption) => {
onBoardSelect(
(boardOpt &&
availableBoards.find((board) => board.fqbn === boardOpt.value)) ||
null
);
},
[availableBoards, onBoardSelect]
);
React.useEffect(() => {
// if there is activity going on, skip updating the boards (avoid flickering)
if (busy) {
return;
}
let placeholderTxt = 'Select a board...';
let selBoard = -1;
const updatableBoards = availableBoards.filter(
(board) => board.port && board.fqbn && updatableFqbns.includes(board.fqbn)
);
const boardsList: BoardOption[] = updatableBoards.map((board, i) => {
if (board.selected) {
selBoard = i;
}
return {
label: `${board.name} at ${board.port?.address}`,
value: board.fqbn || '',
};
});
if (boardsList.length === 0) {
placeholderTxt = 'No supported board connected';
}
setSelectBoardPlaceholder(placeholderTxt);
setSelectOptions(boardsList);
if (selectedBoard) {
selBoard = boardsList
.map((boardOpt) => boardOpt.value)
.indexOf(selectedBoard.fqbn || '');
}
selectOption(boardsList[selBoard] || null);
}, [busy, availableBoards, selectOption, updatableFqbns, selectedBoard]);
return (
<ArduinoSelect
id="board-select"
menuPosition="fixed"
isDisabled={selectOptions.length === 0 || busy}
placeholder={selectBoardPlaceholder}
options={selectOptions}
value={
(selectedBoard && {
value: selectedBoard.fqbn,
label: `${selectedBoard.name} at ${selectedBoard.port?.address}`,
}) ||
null
}
tabSelectsValue={false}
onChange={selectOption}
/>
);
};

View File

@@ -0,0 +1,38 @@
export const arduinoCert = 'arduino.cc:443';
export function sanifyCertString(cert: string): string {
const regex = /^(?:.*:\/\/)*(\S+\.+[^:]*):*(\d*)*$/gm;
const m = regex.exec(cert);
if (!m) {
return '';
}
const domain = m[1] || '';
const port = m[2] || '443';
if (domain.length === 0 || port.length === 0) {
return '';
}
return `${domain}:${port}`;
}
export function certificateList(certificates: string): string[] {
let certs = certificates
.split(',')
.map((cert) => sanifyCertString(cert.trim()))
.filter((cert) => {
// remove empty certificates
if (!cert || cert.length === 0) {
return false;
}
return true;
});
// add arduino certificate at the top of the list
certs = certs.filter((cert) => cert !== arduinoCert);
certs.unshift(arduinoCert);
return certs;
}

View File

@@ -0,0 +1,171 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { clipboard } from 'electron';
import {
AbstractDialog,
ReactWidget,
DialogProps,
} from '@theia/core/lib/browser';
import { CreateApi } from '../create/create-api';
const RadioButton = (props: {
id: string;
changed: (evt: React.BaseSyntheticEvent) => void;
value: string;
isSelected: boolean;
isDisabled: boolean;
label: string;
}) => {
return (
<p className="RadioButton">
<input
id={props.id}
onChange={props.changed}
value={props.value}
type="radio"
checked={props.isSelected}
disabled={props.isDisabled}
/>
<label htmlFor={props.id}>{props.label}</label>
</p>
);
};
export const ShareSketchComponent = ({
treeNode,
createApi,
domain = 'https://create.arduino.cc',
}: {
treeNode: any;
createApi: CreateApi;
domain?: string;
}): React.ReactElement => {
const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
setloading(true);
const sketch = await createApi.editSketch({
id: treeNode.sketchId,
params: {
is_public: event.target.value === 'private' ? false : true,
},
});
// setPublicVisibility(sketch.is_public);
treeNode.isPublic = sketch.is_public;
setloading(false);
};
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
const embedLink = `<iframe src="${sketchLink}?embed" style="height:510px;width:100%;margin:10px 0" frameborder=0></iframe>`;
return (
<div id="widget-container arduino-sharesketch-dialog">
<p>Choose visibility of your Sketch:</p>
<RadioButton
changed={radioChangeHandler}
id="1"
isSelected={treeNode.isPublic === false}
label="Private. Only you can view the Sketch."
value="private"
isDisabled={loading}
/>
<RadioButton
changed={radioChangeHandler}
id="2"
isSelected={treeNode.isPublic === true}
label="Public. Anyone with the link can view the Sketch."
value="public"
isDisabled={loading}
/>
{treeNode.isPublic && (
<div>
<p>Link:</p>
<div className="sketch-link">
<input
type="text"
readOnly
value={sketchLink}
className="theia-input"
/>
<button
onClick={() => clipboard.writeText(sketchLink)}
value="copy"
className="theia-button secondary"
>
Copy
</button>
</div>
<p>Embed:</p>
<div className="sketch-link-embed">
<textarea
readOnly
value={embedLink}
className="theia-input stretch"
/>
</div>
</div>
)}
</div>
);
};
@injectable()
export class ShareSketchWidget extends ReactWidget {
constructor(private treeNode: any, private createApi: CreateApi) {
super();
}
protected render(): React.ReactNode {
return (
<ShareSketchComponent
treeNode={this.treeNode}
createApi={this.createApi}
/>
);
}
}
@injectable()
export class ShareSketchDialogProps extends DialogProps {
readonly node: any;
readonly createApi: CreateApi;
}
@injectable()
export class ShareSketchDialog extends AbstractDialog<void> {
protected widget: ShareSketchWidget;
constructor(
@inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
this.widget = new ShareSketchWidget(props.node, props.createApi);
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
}

View File

@@ -0,0 +1,69 @@
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
ConfirmDialog,
ConfirmDialogProps,
DialogError,
} from '@theia/core/lib/browser/dialogs';
@injectable()
export class DoNotAskAgainDialogProps extends ConfirmDialogProps {
readonly onAccept: () => Promise<void>;
}
@injectable()
export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
protected readonly doNotAskAgainCheckbox: HTMLInputElement;
constructor(
@inject(DoNotAskAgainDialogProps)
protected readonly props: DoNotAskAgainDialogProps
) {
super(props);
this.controlPanel.removeChild(this.errorMessageNode);
const doNotAskAgainNode = document.createElement('div');
doNotAskAgainNode.setAttribute('style', 'flex: 2');
this.controlPanel.insertBefore(
doNotAskAgainNode,
this.controlPanel.firstChild
);
const doNotAskAgainLabel = document.createElement('label');
doNotAskAgainLabel.classList.add('flex-line');
doNotAskAgainNode.appendChild(doNotAskAgainLabel);
doNotAskAgainLabel.textContent = "Don't ask again";
this.doNotAskAgainCheckbox = document.createElement('input');
this.doNotAskAgainCheckbox.setAttribute('align-self', 'center');
doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox);
this.doNotAskAgainCheckbox.type = 'checkbox';
}
protected async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
if (this.doNotAskAgainCheckbox.checked) {
await this.props.onAccept();
}
this.resolve(value);
Widget.detach(this);
}
}
protected setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}
}
}

View File

@@ -0,0 +1,204 @@
import * as React from 'react';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SelectBoardComponent } from '../certificate-uploader/select-board-components';
type FirmwareOption = { value: string; label: string };
export const FirmwareUploaderComponent = ({
availableBoards,
firmwareUploader,
updatableFqbns,
flashFirmware,
isOpen,
}: {
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
isOpen: any;
}): React.ReactElement => {
// boolean states for buttons
const [firmwaresFetching, setFirmwaresFetching] = React.useState(false);
const [installFeedback, setInstallFeedback] = React.useState<
'ok' | 'fail' | 'installing' | null
>(null);
const [selectedBoard, setSelectedBoard] =
React.useState<AvailableBoard | null>(null);
const [availableFirmwares, setAvailableFirmwares] = React.useState<
FirmwareInfo[]
>([]);
React.useEffect(() => {
setAvailableFirmwares([]);
}, [isOpen]);
const [selectedFirmware, setSelectedFirmware] =
React.useState<FirmwareOption | null>(null);
const [firmwareOptions, setFirmwareOptions] = React.useState<
FirmwareOption[]
>([]);
const fetchFirmwares = React.useCallback(async () => {
setInstallFeedback(null);
setFirmwaresFetching(true);
if (!selectedBoard) {
return;
}
// fetch the firmwares for the selected board
const firmwaresForFqbn = await firmwareUploader.availableFirmwares(
selectedBoard.fqbn || ''
);
setAvailableFirmwares(firmwaresForFqbn);
const firmwaresOpts = firmwaresForFqbn.map((f) => ({
label: f.firmware_version,
value: f.firmware_version,
}));
setFirmwareOptions(firmwaresOpts);
if (firmwaresForFqbn.length > 0) setSelectedFirmware(firmwaresOpts[0]);
setFirmwaresFetching(false);
}, [firmwareUploader, selectedBoard]);
const installFirmware = React.useCallback(async () => {
setInstallFeedback('installing');
const firmwareToFlash = availableFirmwares.find(
(firmware) => firmware.firmware_version === selectedFirmware?.value
);
try {
const installStatus =
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {
setInstallFeedback('fail');
}
}, [firmwareUploader, selectedBoard, selectedFirmware, availableFirmwares]);
const onBoardSelect = React.useCallback(
(board: AvailableBoard) => {
const newFqbn = (board && board.fqbn) || null;
const prevFqbn = (selectedBoard && selectedBoard.fqbn) || null;
if (newFqbn !== prevFqbn) {
setInstallFeedback(null);
setAvailableFirmwares([]);
setSelectedBoard(board);
}
},
[selectedBoard]
);
return (
<>
<div className="dialogSection">
<div className="dialogRow">
<label htmlFor="board-select">Select board</label>
</div>
<div className="dialogRow">
<div className="fl1">
<SelectBoardComponent
availableBoards={availableBoards}
updatableFqbns={updatableFqbns}
onBoardSelect={onBoardSelect}
selectedBoard={selectedBoard}
busy={installFeedback === 'installing'}
/>
</div>
<button
type="button"
className="theia-button secondary"
disabled={
selectedBoard === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
onClick={fetchFirmwares}
>
Check Updates
</button>
</div>
</div>
{availableFirmwares.length > 0 && (
<>
<div className="dialogSection">
<div className="dialogRow">
<label htmlFor="firmware-select" className="fl1">
Select firmware version
</label>
<ArduinoSelect
id="firmware-select"
menuPosition="fixed"
isDisabled={
!selectedBoard ||
firmwaresFetching ||
installFeedback === 'installing'
}
options={firmwareOptions}
value={selectedFirmware}
tabSelectsValue={false}
onChange={(value) => {
if (value) {
setInstallFeedback(null);
setSelectedFirmware(value);
}
}}
/>
<button
type="button"
className="theia-button primary"
disabled={
selectedFirmware === null ||
firmwaresFetching ||
installFeedback === 'installing'
}
onClick={installFirmware}
>
Install
</button>
</div>
</div>
<div className="dialogSection">
{installFeedback === null && (
<div className="dialogRow warn">
<i className="fa fa-exclamation status-icon" />
Installation will overwrite the Sketch on the board.
</div>
)}
{installFeedback === 'installing' && (
<div className="dialogRow success">
<div className="spinner" />
Installing firmware.
</div>
)}
{installFeedback === 'ok' && (
<div className="dialogRow success">
<i className="fa fa-info status-icon" />
Firmware succesfully installed.
</div>
)}
{installFeedback === 'fail' && (
<div className="dialogRow warn">
<i className="fa fa-exclamation status-icon" />
Installation failed. Please try again.
</div>
)}
</div>
</>
)}
</>
);
};

View File

@@ -0,0 +1,141 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
} from '../../boards/boards-service-provider';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
} from '../../../common/protocol/arduino-firmware-uploader';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
}
@postConstruct()
protected init(): void {
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.updatableFqbns = fqbns;
this.update();
});
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
this.availableBoards = availableBoards;
this.update();
});
}
protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
protected render(): React.ReactNode {
return (
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
);
}
}
@injectable()
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends AbstractDialog<void> {
@inject(UploadFirmwareDialogWidget)
protected readonly widget: UploadFirmwareDialogWidget;
private busy = false;
constructor(
@inject(UploadFirmwareDialogProps)
protected readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
if (this.busy) {
return;
}
this.widget.close();
super.close();
}
busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
} else {
this.closeCrossNode.classList.remove('disabled');
}
}
}

View File

@@ -1,39 +1,37 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
FrontendApplication, FrontendApplication,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { MainMenuManager } from '../common/main-menu-manager'; import { MainMenuManager } from '../common/main-menu-manager';
@injectable() @injectable()
export class EditorMode implements FrontendApplicationContribution { export class EditorMode implements FrontendApplicationContribution {
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
protected app: FrontendApplication; protected app: FrontendApplication;
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
this.app = app; this.app = app;
} }
get compileForDebug(): boolean { get compileForDebug(): boolean {
const value = window.localStorage.getItem( const value = window.localStorage.getItem(EditorMode.COMPILE_FOR_DEBUG_KEY);
EditorMode.COMPILE_FOR_DEBUG_KEY return value === 'true';
); }
return value === 'true';
}
async toggleCompileForDebug(): Promise<void> { async toggleCompileForDebug(): Promise<void> {
const oldState = this.compileForDebug; const oldState = this.compileForDebug;
const newState = !oldState; const newState = !oldState;
window.localStorage.setItem( window.localStorage.setItem(
EditorMode.COMPILE_FOR_DEBUG_KEY, EditorMode.COMPILE_FOR_DEBUG_KEY,
String(newState) String(newState)
); );
this.mainMenuManager.update(); this.mainMenuManager.update();
} }
} }
export namespace EditorMode { export namespace EditorMode {
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug'; export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
} }

View File

@@ -3,8 +3,8 @@ import { Message } from '@phosphor/messaging';
import { addEventListener } from '@theia/core/lib/browser/widgets/widget'; import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs'; import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
import { import {
LibraryPackage, LibraryPackage,
LibraryService, LibraryService,
} from '../../common/protocol/library-service'; } from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget'; import { ListWidget } from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol'; import { Installable } from '../../common/protocol';
@@ -12,190 +12,184 @@ import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable() @injectable()
export class LibraryListWidget extends ListWidget<LibraryPackage> { export class LibraryListWidget extends ListWidget<LibraryPackage> {
static WIDGET_ID = 'library-list-widget'; static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager'; static WIDGET_LABEL = 'Library Manager';
constructor( constructor(
@inject(LibraryService) protected service: LibraryService, @inject(LibraryService) protected service: LibraryService,
@inject(ListItemRenderer) @inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<LibraryPackage> protected itemRenderer: ListItemRenderer<LibraryPackage>
) { ) {
super({ super({
id: LibraryListWidget.WIDGET_ID, id: LibraryListWidget.WIDGET_ID,
label: LibraryListWidget.WIDGET_LABEL, label: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon', iconClass: 'library-tab-icon',
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: LibraryPackage) => item.name, itemLabel: (item: LibraryPackage) => item.name,
itemDeprecated: (item: LibraryPackage) => item.deprecated, itemDeprecated: (item: LibraryPackage) => item.deprecated,
itemRenderer, itemRenderer,
}); });
}
@postConstruct()
protected init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
this.notificationCenter.onLibraryUninstalled(() =>
this.refresh(undefined)
),
]);
}
protected async install({
item,
progressId,
version,
}: {
item: LibraryPackage;
progressId: string;
version: Installable.Version;
}): Promise<void> {
const dependencies = await this.service.listDependencies({
item,
version,
filterSelf: true,
});
let installDependencies: boolean | undefined = undefined;
if (dependencies.length) {
const message = document.createElement('div');
message.innerHTML = `The library <b>${item.name}:${version}</b> needs ${
dependencies.length === 1
? 'another dependency'
: 'some other dependencies'
} currently not installed:`;
const listContainer = document.createElement('div');
listContainer.style.maxHeight = '300px';
listContainer.style.overflowY = 'auto';
const list = document.createElement('ul');
list.style.listStyleType = 'none';
for (const { name } of dependencies) {
const listItem = document.createElement('li');
listItem.textContent = ` - ${name}`;
listItem.style.fontWeight = 'bold';
list.appendChild(listItem);
}
listContainer.appendChild(list);
message.appendChild(listContainer);
const question = document.createElement('div');
question.textContent = `Would you like to install ${
dependencies.length === 1
? 'the missing dependency'
: 'all the missing dependencies'
}?`;
message.appendChild(question);
const result = await new MessageBoxDialog({
title: `Dependencies for library ${item.name}:${version}`,
message,
buttons: ['Install all', `Install ${item.name} only`, 'Cancel'],
maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open();
if (result) {
const { response } = result;
if (response === 0) {
// All
installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
}
}
} else {
// The lib does not have any dependencies.
installDependencies = false;
} }
@postConstruct() if (typeof installDependencies === 'boolean') {
protected init(): void { await this.service.install({
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() =>
this.refresh(undefined)
),
this.notificationCenter.onLibraryUninstalled(() =>
this.refresh(undefined)
),
]);
}
protected async install({
item, item,
progressId,
version, version,
}: {
item: LibraryPackage;
progressId: string;
version: Installable.Version;
}): Promise<void> {
const dependencies = await this.service.listDependencies({
item,
version,
filterSelf: true,
});
let installDependencies: boolean | undefined = undefined;
if (dependencies.length) {
const message = document.createElement('div');
message.innerHTML = `The library <b>${
item.name
}:${version}</b> needs ${
dependencies.length === 1
? 'another dependency'
: 'some other dependencies'
} currently not installed:`;
const listContainer = document.createElement('div');
listContainer.style.maxHeight = '300px';
listContainer.style.overflowY = 'auto';
const list = document.createElement('ul');
list.style.listStyleType = 'none';
for (const { name } of dependencies) {
const listItem = document.createElement('li');
listItem.textContent = ` - ${name}`;
listItem.style.fontWeight = 'bold';
list.appendChild(listItem);
}
listContainer.appendChild(list);
message.appendChild(listContainer);
const question = document.createElement('div');
question.textContent = `Would you like to install ${
dependencies.length === 1
? 'the missing dependency'
: 'all the missing dependencies'
}?`;
message.appendChild(question);
const result = await new MessageBoxDialog({
title: `Dependencies for library ${item.name}:${version}`,
message,
buttons: ['Install all', `Install ${item.name} only`, 'Cancel'],
maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open();
if (result) {
const { response } = result;
if (response === 0) {
// All
installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
}
}
} else {
// The lib does not have any dependencies.
installDependencies = false;
}
if (typeof installDependencies === 'boolean') {
await this.service.install({
item,
version,
progressId,
installDependencies,
});
this.messageService.info(
`Successfully installed library ${item.name}:${version}`,
{ timeout: 3000 }
);
}
}
protected async uninstall({
item,
progressId, progressId,
}: { installDependencies,
item: LibraryPackage; });
progressId: string; this.messageService.info(
}): Promise<void> { `Successfully installed library ${item.name}:${version}`,
await super.uninstall({ item, progressId }); { timeout: 3000 }
this.messageService.info( );
`Successfully uninstalled library ${item.name}:${item.installedVersion}`,
{ timeout: 3000 }
);
} }
}
protected async uninstall({
item,
progressId,
}: {
item: LibraryPackage;
progressId: string;
}): Promise<void> {
await super.uninstall({ item, progressId });
this.messageService.info(
`Successfully uninstalled library ${item.name}:${item.installedVersion}`,
{ timeout: 3000 }
);
}
} }
class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> { class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
protected response: number; protected response: number;
constructor(protected readonly options: MessageBoxDialog.Options) { constructor(protected readonly options: MessageBoxDialog.Options) {
super(options); super(options);
this.contentNode.appendChild( this.contentNode.appendChild(this.createMessageNode(this.options.message));
this.createMessageNode(this.options.message) (options.buttons || ['OK']).forEach((text, index) => {
); const button = this.createButton(text);
(options.buttons || ['OK']).forEach((text, index) => { button.classList.add(index === 0 ? 'main' : 'secondary');
const button = this.createButton(text); this.controlPanel.appendChild(button);
button.classList.add(index === 0 ? 'main' : 'secondary'); this.toDisposeOnDetach.push(
this.controlPanel.appendChild(button); addEventListener(button, 'click', () => {
this.toDisposeOnDetach.push( this.response = index;
addEventListener(button, 'click', () => { this.accept();
this.response = index; })
this.accept(); );
}) });
); }
});
}
protected onCloseRequest(message: Message): void { protected onCloseRequest(message: Message): void {
super.onCloseRequest(message); super.onCloseRequest(message);
this.accept(); this.accept();
} }
get value(): MessageBoxDialog.Result { get value(): MessageBoxDialog.Result {
return { response: this.response }; return { response: this.response };
} }
protected createMessageNode(message: string | HTMLElement): HTMLElement { protected createMessageNode(message: string | HTMLElement): HTMLElement {
if (typeof message === 'string') { if (typeof message === 'string') {
const messageNode = document.createElement('div'); const messageNode = document.createElement('div');
messageNode.textContent = message; messageNode.textContent = message;
return messageNode; return messageNode;
}
return message;
} }
return message;
}
protected handleEnter(event: KeyboardEvent): boolean | void { protected handleEnter(event: KeyboardEvent): boolean | void {
this.response = 0; this.response = 0;
super.handleEnter(event); super.handleEnter(event);
} }
} }
export namespace MessageBoxDialog { export namespace MessageBoxDialog {
export interface Options extends DialogProps { export interface Options extends DialogProps {
/** /**
* When empty, `['OK']` will be inferred. * When empty, `['OK']` will be inferred.
*/ */
buttons?: string[]; buttons?: string[];
message: string | HTMLElement; message: string | HTMLElement;
} }
export interface Result { export interface Result {
/** /**
* The index of `buttons` that was clicked. * The index of `buttons` that was clicked.
*/ */
readonly response: number; readonly response: number;
} }
} }

View File

@@ -7,33 +7,33 @@ import { ArduinoMenus } from '../menu/arduino-menus';
@injectable() @injectable()
export class LibraryListWidgetFrontendContribution export class LibraryListWidgetFrontendContribution
extends AbstractViewContribution<LibraryListWidget> extends AbstractViewContribution<LibraryListWidget>
implements FrontendApplicationContribution implements FrontendApplicationContribution
{ {
constructor() { constructor() {
super({ super({
widgetId: LibraryListWidget.WIDGET_ID, widgetId: LibraryListWidget.WIDGET_ID,
widgetName: LibraryListWidget.WIDGET_LABEL, widgetName: LibraryListWidget.WIDGET_LABEL,
defaultWidgetOptions: { defaultWidgetOptions: {
area: 'left', area: 'left',
rank: 3, rank: 3,
}, },
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`, toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'CtrlCmd+Shift+I', toggleKeybinding: 'CtrlCmd+Shift+I',
}); });
} }
async initializeLayout(): Promise<void> { async initializeLayout(): Promise<void> {
this.openView(); this.openView();
} }
registerMenus(menus: MenuModelRegistry): void { registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) { if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id, commandId: this.toggleCommand.id,
label: 'Manage Libraries...', label: 'Manage Libraries...',
order: '3', order: '3',
}); });
}
} }
}
} }

View File

@@ -3,165 +3,158 @@ import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import { import {
FileSystemProvider, FileSystemProvider,
FileSystemProviderError, FileSystemProviderError,
FileSystemProviderErrorCode, FileSystemProviderErrorCode,
} from '@theia/filesystem/lib/common/files'; } from '@theia/filesystem/lib/common/files';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider'; import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider';
import { import {
FileService, FileService,
FileServiceContribution, FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service'; } from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
import { AuthenticationSession } from '../../common/protocol/authentication-service'; import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ConfigService } from '../../common/protocol'; import { ConfigService } from '../../common/protocol';
export namespace LocalCacheUri { export namespace LocalCacheUri {
export const scheme = 'arduino-local-cache'; export const scheme = 'arduino-local-cache';
export const root = new URI( export const root = new URI(
Uri.parse('/').with({ scheme, authority: 'create' }) Uri.parse('/').with({ scheme, authority: 'create' })
); );
} }
@injectable() @injectable()
export class LocalCacheFsProvider export class LocalCacheFsProvider
implements implements FileServiceContribution, DelegatingFileSystemProvider.URIConverter
FileServiceContribution,
DelegatingFileSystemProvider.URIConverter
{ {
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(AuthenticationClientService) @inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService; protected readonly authenticationService: AuthenticationClientService;
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`? // TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
readonly ready = new Deferred<void>(); readonly ready = new Deferred<void>();
private _localCacheRoot: URI; private _localCacheRoot: URI;
registerFileSystemProviders(fileService: FileService): void { registerFileSystemProviders(fileService: FileService): void {
fileService.onWillActivateFileSystemProvider(async (event) => { fileService.onWillActivateFileSystemProvider(async (event) => {
if (event.scheme === LocalCacheUri.scheme) { if (event.scheme === LocalCacheUri.scheme) {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
this.init(fileService); this.init(fileService);
const provider = await this.createProvider(fileService); const provider = await this.createProvider(fileService);
fileService.registerProvider( fileService.registerProvider(LocalCacheUri.scheme, provider);
LocalCacheUri.scheme, })()
provider
);
})()
);
}
});
}
to(resource: URI): URI | undefined {
const relativePath = LocalCacheUri.root.relative(resource);
if (relativePath) {
return this.currentUserUri.resolve(relativePath).normalizePath();
}
return undefined;
}
from(resource: URI): URI | undefined {
const relativePath = this.currentUserUri.relative(resource);
if (relativePath) {
return LocalCacheUri.root.resolve(relativePath);
}
return undefined;
}
protected async createProvider(
fileService: FileService
): Promise<FileSystemProvider> {
const delegate = await fileService.activateProvider('file');
await this.ready.promise;
return new DelegatingFileSystemProvider(
delegate,
{
uriConverter: this,
},
new DisposableCollection(
delegate.watch(this.localCacheRoot, {
excludes: [],
recursive: true,
})
)
); );
} }
});
}
protected async init(fileService: FileService): Promise<void> { to(resource: URI): URI | undefined {
const config = await this.configService.getConfiguration(); const relativePath = LocalCacheUri.root.relative(resource);
this._localCacheRoot = new URI(config.dataDirUri); if (relativePath) {
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) { return this.currentUserUri.resolve(relativePath).normalizePath();
this._localCacheRoot = this._localCacheRoot.resolve(segment); }
await fileService.createFolder(this._localCacheRoot); return undefined;
} }
this.session(fileService).then(() => this.ready.resolve());
from(resource: URI): URI | undefined {
const relativePath = this.currentUserUri.relative(resource);
if (relativePath) {
return LocalCacheUri.root.resolve(relativePath);
}
return undefined;
}
protected async createProvider(
fileService: FileService
): Promise<FileSystemProvider> {
const delegate = await fileService.activateProvider('file');
await this.ready.promise;
return new DelegatingFileSystemProvider(
delegate,
{
uriConverter: this,
},
new DisposableCollection(
delegate.watch(this.localCacheRoot, {
excludes: [],
recursive: true,
})
)
);
}
protected async init(fileService: FileService): Promise<void> {
const config = await this.configService.getConfiguration();
this._localCacheRoot = new URI(config.dataDirUri);
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
this._localCacheRoot = this._localCacheRoot.resolve(segment);
await fileService.createFolder(this._localCacheRoot);
}
this.session(fileService).then(() => this.ready.resolve());
this.authenticationService.onSessionDidChange(async (session) => {
if (session) {
await this.ensureExists(session, fileService);
}
});
}
public get currentUserUri(): URI {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
}
return this.toUri(session);
}
private get localCacheRoot(): URI {
return this._localCacheRoot;
}
private async session(
fileService: FileService
): Promise<AuthenticationSession> {
return new Promise<AuthenticationSession>(async (resolve) => {
const { session } = this.authenticationService;
if (session) {
await this.ensureExists(session, fileService);
resolve(session);
return;
}
const toDispose = new DisposableCollection();
toDispose.push(
this.authenticationService.onSessionDidChange(async (session) => { this.authenticationService.onSessionDidChange(async (session) => {
if (session) { if (session) {
await this.ensureExists(session, fileService); await this.ensureExists(session, fileService);
} toDispose.dispose();
}); resolve(session);
} }
})
);
});
}
private get currentUserUri(): URI { private async ensureExists(
const { session } = this.authenticationService; session: AuthenticationSession,
if (!session) { fileService: FileService
throw new FileSystemProviderError( ): Promise<URI> {
'Not logged in.', const uri = this.toUri(session);
FileSystemProviderErrorCode.NoPermissions const exists = await fileService.exists(uri);
); if (!exists) {
} await fileService.createFolder(uri);
return this.toUri(session);
} }
return uri;
}
private get localCacheRoot(): URI { private toUri(session: AuthenticationSession): URI {
return this._localCacheRoot; // Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
} return this._localCacheRoot.resolve(session.id.split('|')[1]);
}
private async session(
fileService: FileService
): Promise<AuthenticationSession> {
return new Promise<AuthenticationSession>(async (resolve) => {
const { session } = this.authenticationService;
if (session) {
await this.ensureExists(session, fileService);
resolve(session);
return;
}
const toDispose = new DisposableCollection();
toDispose.push(
this.authenticationService.onSessionDidChange(
async (session) => {
if (session) {
await this.ensureExists(session, fileService);
toDispose.dispose();
resolve(session);
}
}
)
);
});
}
private async ensureExists(
session: AuthenticationSession,
fileService: FileService
): Promise<URI> {
const uri = this.toUri(session);
const exists = await fileService.exists(uri);
if (!exists) {
await fileService.createFolder(uri);
}
return uri;
}
private toUri(session: AuthenticationSession): URI {
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
return this._localCacheRoot.resolve(session.id.split('|')[1]);
}
} }

View File

@@ -1,148 +1,158 @@
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { import {
MAIN_MENU_BAR, MAIN_MENU_BAR,
MenuModelRegistry, MenuModelRegistry,
MenuNode, MenuNode,
MenuPath, MenuPath,
SubMenuOptions, SubMenuOptions,
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
export namespace ArduinoMenus { export namespace ArduinoMenus {
// Main menu // Main menu
// -- File // -- File
export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch']; export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch'];
export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print']; export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print'];
// XXX: on macOS, the "Preferences" and "Advanced" group is not under `File` // XXX: on macOS, the "Preferences" and "Advanced" group is not under `File`
// The empty path ensures no top level menu is created for the preferences, even if they contains sub menus // The empty path ensures no top level menu is created for the preferences, even if they contains sub menus
export const FILE__PREFERENCES_GROUP = [ export const FILE__PREFERENCES_GROUP = [
...(isOSX ? [''] : CommonMenus.FILE), ...(isOSX ? [''] : CommonMenus.FILE),
'2_preferences', '2_preferences',
]; ];
export const FILE__ADVANCED_GROUP = [ export const FILE__ADVANCED_GROUP = [
...(isOSX ? [''] : CommonMenus.FILE), ...(isOSX ? [''] : CommonMenus.FILE),
'3_advanced', '3_advanced',
]; ];
export const FILE__ADVANCED_SUBMENU = [ export const FILE__ADVANCED_SUBMENU = [
...FILE__ADVANCED_GROUP, ...FILE__ADVANCED_GROUP,
'0_advanced_sub', '0_advanced_sub',
]; ];
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit']; export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
// -- File / Open Recent // -- File / Open Recent
export const FILE__OPEN_RECENT_SUBMENU = [ export const FILE__OPEN_RECENT_SUBMENU = [
...FILE__SKETCH_GROUP, ...FILE__SKETCH_GROUP,
'0_open_recent', '0_open_recent',
]; ];
// -- File / Sketchbook // -- File / Sketchbook
export const FILE__SKETCHBOOK_SUBMENU = [ export const FILE__SKETCHBOOK_SUBMENU = [
...FILE__SKETCH_GROUP, ...FILE__SKETCH_GROUP,
'1_sketchbook', '1_sketchbook',
]; ];
// -- File / Examples // -- File / Examples
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples']; export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples'];
export const EXAMPLES__BUILT_IN_GROUP = [ export const EXAMPLES__BUILT_IN_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'0_built_ins', '0_built_ins',
]; ];
export const EXAMPLES__ANY_BOARD_GROUP = [ export const EXAMPLES__ANY_BOARD_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'1_any_board', '1_any_board',
]; ];
export const EXAMPLES__CURRENT_BOARD_GROUP = [ export const EXAMPLES__CURRENT_BOARD_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'2_current_board', '2_current_board',
]; ];
export const EXAMPLES__USER_LIBS_GROUP = [ export const EXAMPLES__USER_LIBS_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'3_user_libs', '3_user_libs',
]; ];
// -- Edit // -- Edit
// `Copy`, `Copy to Forum`, `Paste`, etc. // `Copy`, `Copy to Forum`, `Paste`, etc.
// Note: `1_undo` is the first group from Theia, we start with `2` // Note: `1_undo` is the first group from Theia, we start with `2`
export const EDIT__TEXT_CONTROL_GROUP = [ export const EDIT__TEXT_CONTROL_GROUP = [
...CommonMenus.EDIT, ...CommonMenus.EDIT,
'2_text_control', '2_text_control',
]; ];
// `Comment/Uncomment`, etc. // `Comment/Uncomment`, etc.
export const EDIT__CODE_CONTROL_GROUP = [ export const EDIT__CODE_CONTROL_GROUP = [
...CommonMenus.EDIT, ...CommonMenus.EDIT,
'3_code_control', '3_code_control',
]; ];
export const EDIT__FONT_CONTROL_GROUP = [ export const EDIT__FONT_CONTROL_GROUP = [
...CommonMenus.EDIT, ...CommonMenus.EDIT,
'4_font_control', '4_font_control',
]; ];
export const EDIT__FIND_GROUP = [...CommonMenus.EDIT, '5_find']; export const EDIT__FIND_GROUP = [...CommonMenus.EDIT, '5_find'];
// -- Sketch // -- Sketch
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
export const SKETCH__MAIN_GROUP = [...SKETCH, '0_main']; export const SKETCH__MAIN_GROUP = [...SKETCH, '0_main'];
export const SKETCH__UTILS_GROUP = [...SKETCH, '1_utils']; export const SKETCH__UTILS_GROUP = [...SKETCH, '1_utils'];
// -- Tools // -- Tools
export const TOOLS = [...MAIN_MENU_BAR, '4_tools']; export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
// `Auto Format`, `Library Manager...`, `Boards Manager...` // `Auto Format`, `Archive Sketch`, `Manage Libraries...`, `Serial Monitor`
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main']; export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
// `Board`, `Port`, and `Get Board Info`. // `WiFi101 / WiFiNINA Firmware Updater`
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection']; export const TOOLS__FIRMWARE_UPLOADER_GROUP = [
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader` ...TOOLS,
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings']; '1_firmware_uploader',
];
// `Board`, `Port`, and `Get Board Info`.
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// -- Help // -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc. // `Getting Started`, `Environment`, `Troubleshooting`, etc.
export const HELP__MAIN_GROUP = [...CommonMenus.HELP, '0_main']; export const HELP__MAIN_GROUP = [...CommonMenus.HELP, '0_main'];
// `Find in reference`, `FAQ`, etc. // `Find in reference`, `FAQ`, etc.
export const HELP__FIND_GROUP = [...CommonMenus.HELP, '1_find']; export const HELP__FIND_GROUP = [...CommonMenus.HELP, '1_find'];
// `Advanced Mode`. // `Advanced Mode`.
// XXX: this will be removed. // XXX: this will be removed.
export const HELP__CONTROL_GROUP = [...CommonMenus.HELP, '2_control']; export const HELP__CONTROL_GROUP = [...CommonMenus.HELP, '2_control'];
// `About` group // `About` group
// XXX: on macOS, the about group is not under `Help` // XXX: on macOS, the about group is not under `Help`
export const HELP__ABOUT_GROUP = [ export const HELP__ABOUT_GROUP = [
...(isOSX ? MAIN_MENU_BAR : CommonMenus.HELP), ...(isOSX ? MAIN_MENU_BAR : CommonMenus.HELP),
'999_about', '999_about',
]; ];
// ------------ // ------------
// Context menus // Context menus
// -- Open // -- Open
export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context']; export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context'];
export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [ export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [
...OPEN_SKETCH__CONTEXT, ...OPEN_SKETCH__CONTEXT,
'0_open', '0_open',
]; ];
export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [ export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [
...OPEN_SKETCH__CONTEXT, ...OPEN_SKETCH__CONTEXT,
'1_recent', '1_recent',
]; ];
export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [ export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [
...OPEN_SKETCH__CONTEXT, ...OPEN_SKETCH__CONTEXT,
'2_examples', '2_examples',
]; ];
// -- Sketch control // -- Sketch control
export const SKETCH_CONTROL__CONTEXT = ['arduino-sketch-control--context']; export const SKETCH_CONTROL__CONTEXT = ['arduino-sketch-control--context'];
// `New Tab`, `Rename`, `Delete` // `New Tab`, `Rename`, `Delete`
export const SKETCH_CONTROL__CONTEXT__MAIN_GROUP = [ export const SKETCH_CONTROL__CONTEXT__MAIN_GROUP = [
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'0_main', '0_main',
]; ];
// `Previous Tab`, `Next Tab` // `Previous Tab`, `Next Tab`
export const SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP = [ export const SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP = [
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'1_navigation', '1_navigation',
]; ];
// Sketch files opened in editors // Sketch files opened in editors
export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [ export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'2_resources', '2_resources',
]; ];
// -- ROOT SSL CERTIFICATES
export const ROOT_CERTIFICATES__CONTEXT = [
'arduino-root-certificates--context',
];
} }
/** /**
@@ -150,49 +160,49 @@ export namespace ArduinoMenus {
* Theia cannot dispose submenu entries with a proper API: https://github.com/eclipse-theia/theia/issues/7299 * Theia cannot dispose submenu entries with a proper API: https://github.com/eclipse-theia/theia/issues/7299
*/ */
export function unregisterSubmenu( export function unregisterSubmenu(
menuPath: string[], menuPath: string[],
menuRegistry: MenuModelRegistry menuRegistry: MenuModelRegistry
): void { ): void {
if (menuPath.length < 2) { if (menuPath.length < 2) {
throw new Error( throw new Error(
`Expected at least two item as a menu-path. Got ${JSON.stringify( `Expected at least two item as a menu-path. Got ${JSON.stringify(
menuPath menuPath
)} instead.` )} instead.`
); );
} }
const toRemove = menuPath[menuPath.length - 1]; const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1); const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation. // This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300 // https://github.com/eclipse-theia/theia/issues/7300
const parent = menuRegistry.getMenu(parentMenuPath); const parent = menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove); const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) { if (index === -1) {
throw new Error( throw new Error(
`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.` `Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`
); );
} }
(parent.children as Array<MenuNode>).splice(index, 1); (parent.children as Array<MenuNode>).splice(index, 1);
} }
/** /**
* Special menu node that is not backed by any commands and is always disabled. * Special menu node that is not backed by any commands and is always disabled.
*/ */
export class PlaceholderMenuNode implements MenuNode { export class PlaceholderMenuNode implements MenuNode {
constructor( constructor(
protected readonly menuPath: MenuPath, protected readonly menuPath: MenuPath,
readonly label: string, readonly label: string,
protected options: SubMenuOptions = { order: '0' } protected options: SubMenuOptions = { order: '0' }
) {} ) {}
get icon(): string | undefined { get icon(): string | undefined {
return this.options?.iconClass; return this.options?.iconClass;
} }
get sortString(): string { get sortString(): string {
return this.options?.order || this.label; return this.options?.order || this.label;
} }
get id(): string { get id(): string {
return [...this.menuPath, 'placeholder'].join('-'); return [...this.menuPath, 'placeholder'].join('-');
} }
} }

View File

@@ -4,17 +4,17 @@ import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { import {
MonitorService, MonitorService,
MonitorConfig, MonitorConfig,
MonitorError, MonitorError,
Status, Status,
} from '../../common/protocol/monitor-service'; } from '../../common/protocol/monitor-service';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
Port, Port,
Board, Board,
BoardsService, BoardsService,
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl'; import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config'; import { BoardsConfig } from '../boards/boards-config';
@@ -23,343 +23,332 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class MonitorConnection { export class MonitorConnection {
@inject(MonitorModel) @inject(MonitorModel)
protected readonly monitorModel: MonitorModel; protected readonly monitorModel: MonitorModel;
@inject(MonitorService) @inject(MonitorService)
protected readonly monitorService: MonitorService; protected readonly monitorService: MonitorService;
@inject(MonitorServiceClientImpl) @inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl; protected readonly monitorServiceClient: MonitorServiceClientImpl;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider; protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(MessageService) @inject(MessageService)
protected messageService: MessageService; protected messageService: MessageService;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
protected readonly applicationState: FrontendApplicationStateService; protected readonly applicationState: FrontendApplicationStateService;
protected state: MonitorConnection.State | undefined; protected state: MonitorConnection.State | undefined;
/** /**
* Note: The idea is to toggle this property from the UI (`Monitor` view) * Note: The idea is to toggle this property from the UI (`Monitor` view)
* and the boards config and the boards attachment/detachment logic can be at on place, here. * and the boards config and the boards attachment/detachment logic can be at on place, here.
*/ */
protected _autoConnect = false; protected _autoConnect = false;
protected readonly onConnectionChangedEmitter = new Emitter< protected readonly onConnectionChangedEmitter = new Emitter<
MonitorConnection.State | undefined MonitorConnection.State | undefined
>(); >();
/** /**
* This emitter forwards all read events **iff** the connection is established. * This emitter forwards all read events **iff** the connection is established.
*/ */
protected readonly onReadEmitter = new Emitter<{ message: string }>(); protected readonly onReadEmitter = new Emitter<{ message: string }>();
/** /**
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array, * Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
* we adjust the reconnection delay. * we adjust the reconnection delay.
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array. * Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
*/ */
protected monitorErrors: MonitorError[] = []; protected monitorErrors: MonitorError[] = [];
protected reconnectTimeout?: number; protected reconnectTimeout?: number;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.monitorServiceClient.onError(async (error) => { this.monitorServiceClient.onError(async (error) => {
let shouldReconnect = false; let shouldReconnect = false;
if (this.state) { if (this.state) {
const { code, config } = error; const { code, config } = error;
const { board, port } = config; const { board, port } = config;
const options = { timeout: 3000 }; const options = { timeout: 3000 };
switch (code) { switch (code) {
case MonitorError.ErrorCodes.CLIENT_CANCEL: { case MonitorError.ErrorCodes.CLIENT_CANCEL: {
console.debug( console.debug(
`Connection was canceled by client: ${MonitorConnection.State.toString( `Connection was canceled by client: ${MonitorConnection.State.toString(
this.state this.state
)}.` )}.`
);
break;
}
case MonitorError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(
`Connection failed. Serial port is busy: ${Port.toString(
port
)}.`,
options
);
shouldReconnect = this.autoConnect;
this.monitorErrors.push(error);
break;
}
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
`Disconnected ${Board.toString(board, {
useFqbn: false,
})} from ${Port.toString(port)}.`,
options
);
break;
}
case undefined: {
this.messageService.error(
`Unexpected error. Reconnecting ${Board.toString(
board
)} on port ${Port.toString(port)}.`,
options
);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
}
const oldState = this.state;
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
if (shouldReconnect) {
if (this.monitorErrors.length >= 10) {
this.messageService.warn(
`Failed to reconnect ${Board.toString(board, {
useFqbn: false,
})} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
port
)} serial port is busy. after 10 consecutive attempts.`
);
this.monitorErrors.length = 0;
} else {
const attempts = this.monitorErrors.length || 1;
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(
`Reconnecting ${Board.toString(board, {
useFqbn: false,
})} to ${Port.toString(
port
)} in ${attempts} seconds...`,
{ timeout }
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(oldState.config),
timeout
);
}
}
}
});
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
this.notificationCenter.onAttachedBoardsChanged((event) => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (
attached.boards.some(
(board) =>
!!board.port &&
BoardsConfig.Config.sameAs(boardsConfig, board)
)
) {
const { selectedBoard: board, selectedPort: port } =
boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect().then(() =>
this.connect({ board, port, baudRate })
);
}
}
}
});
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
});
}
get connected(): boolean {
return !!this.state;
}
get monitorConfig(): MonitorConfig | undefined {
return this.state ? this.state.config : undefined;
}
get autoConnect(): boolean {
return this._autoConnect;
}
set autoConnect(value: boolean) {
const oldValue = this._autoConnect;
this._autoConnect = value;
// When we enable the auto-connect, we have to connect
if (!oldValue && value) {
// We have to make sure the previous boards config has been restored.
// Otherwise, we might start the auto-connection without configured boards.
this.applicationState
.reachedState('started_contributions')
.then(() => {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
if (this.reconnectTimeout !== undefined) {
window.clearTimeout(this.reconnectTimeout);
this.monitorErrors.length = 0;
}
}
}
async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
if (!Status.isOK(disconnectStatus)) {
return disconnectStatus;
}
}
console.info(
`>>> Creating serial monitor connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
const requestMessage = () => {
this.monitorService.request().then(({ message }) => {
if (this.connected) {
this.onReadEmitter.fire({ message });
requestMessage();
}
});
};
requestMessage();
this.state = { config };
console.info(
`<<< Serial monitor connection created for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
); );
} break;
this.onConnectionChangedEmitter.fire(this.state); }
return Status.isOK(connectStatus); case MonitorError.ErrorCodes.DEVICE_BUSY: {
} this.messageService.warn(
`Connection failed. Serial port is busy: ${Port.toString(port)}.`,
async disconnect(): Promise<Status> { options
if (!this.connected) {
return Status.OK;
}
const stateCopy = deepClone(this.state);
if (!stateCopy) {
return Status.OK;
}
console.log('>>> Disposing existing monitor connection...');
const status = await this.monitorService.disconnect();
if (Status.isOK(status)) {
console.log(
`<<< Disposed connection. Was: ${MonitorConnection.State.toString(
stateCopy
)}`
); );
} else { shouldReconnect = this.autoConnect;
console.warn( this.monitorErrors.push(error);
`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString( break;
stateCopy }
)}` case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
`Disconnected ${Board.toString(board, {
useFqbn: false,
})} from ${Port.toString(port)}.`,
options
); );
break;
}
case undefined: {
this.messageService.error(
`Unexpected error. Reconnecting ${Board.toString(
board
)} on port ${Port.toString(port)}.`,
options
);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
} }
const oldState = this.state;
this.state = undefined; this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state); this.onConnectionChangedEmitter.fire(this.state);
return status; if (shouldReconnect) {
} if (this.monitorErrors.length >= 10) {
this.messageService.warn(
/** `Failed to reconnect ${Board.toString(board, {
* Sends the data to the connected serial monitor. useFqbn: false,
* The desired EOL is appended to `data`, you do not have to add it. })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
* It is a NOOP if connected. port
*/ )} serial port is busy. after 10 consecutive attempts.`
async send(data: string): Promise<Status> { );
if (!this.connected) { this.monitorErrors.length = 0;
return Status.NOT_CONNECTED; } else {
} const attempts = this.monitorErrors.length || 1;
return new Promise<Status>((resolve) => { if (this.reconnectTimeout !== undefined) {
this.monitorService // Clear the previous timer.
.send(data + this.monitorModel.lineEnding) window.clearTimeout(this.reconnectTimeout);
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ message: string }> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(
boardsConfig: BoardsConfig.Config
): Promise<void> {
if (this.autoConnect) {
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then((ports) => {
if (
ports.some((port) =>
Port.equals(port, boardsConfig.selectedPort)
)
) {
new Promise<void>((resolve) => {
// First, disconnect if connected.
if (this.connected) {
this.disconnect().then(() => resolve());
return;
}
resolve();
}).then(() => {
// Then (re-)connect.
const { selectedBoard: board, selectedPort: port } =
boardsConfig;
const { baudRate } = this.monitorModel;
this.connect({ board, port, baudRate });
});
}
});
} }
const timeout = attempts * 1000;
this.messageService.warn(
`Reconnecting ${Board.toString(board, {
useFqbn: false,
})} to ${Port.toString(port)} in ${attempts} seconds...`,
{ timeout }
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(oldState.config),
timeout
);
}
} }
}
});
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
this.notificationCenter.onAttachedBoardsChanged((event) => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (
attached.boards.some(
(board) =>
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
)
) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect().then(() =>
this.connect({ board, port, baudRate })
);
}
}
}
});
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
});
}
get connected(): boolean {
return !!this.state;
}
get monitorConfig(): MonitorConfig | undefined {
return this.state ? this.state.config : undefined;
}
get autoConnect(): boolean {
return this._autoConnect;
}
set autoConnect(value: boolean) {
const oldValue = this._autoConnect;
this._autoConnect = value;
// When we enable the auto-connect, we have to connect
if (!oldValue && value) {
// We have to make sure the previous boards config has been restored.
// Otherwise, we might start the auto-connection without configured boards.
this.applicationState.reachedState('started_contributions').then(() => {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
if (this.reconnectTimeout !== undefined) {
window.clearTimeout(this.reconnectTimeout);
this.monitorErrors.length = 0;
}
} }
}
async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
if (!Status.isOK(disconnectStatus)) {
return disconnectStatus;
}
}
console.info(
`>>> Creating serial monitor connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
const requestMessage = () => {
this.monitorService.request().then(({ message }) => {
if (this.connected) {
this.onReadEmitter.fire({ message });
requestMessage();
}
});
};
requestMessage();
this.state = { config };
console.info(
`<<< Serial monitor connection created for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
);
}
this.onConnectionChangedEmitter.fire(this.state);
return Status.isOK(connectStatus);
}
async disconnect(): Promise<Status> {
if (!this.connected) {
return Status.OK;
}
const stateCopy = deepClone(this.state);
if (!stateCopy) {
return Status.OK;
}
console.log('>>> Disposing existing monitor connection...');
const status = await this.monitorService.disconnect();
if (Status.isOK(status)) {
console.log(
`<<< Disposed connection. Was: ${MonitorConnection.State.toString(
stateCopy
)}`
);
} else {
console.warn(
`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(
stateCopy
)}`
);
}
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
return status;
}
/**
* Sends the data to the connected serial monitor.
* The desired EOL is appended to `data`, you do not have to add it.
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!this.connected) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>((resolve) => {
this.monitorService
.send(data + this.monitorModel.lineEnding)
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ message: string }> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(
boardsConfig: BoardsConfig.Config
): Promise<void> {
if (this.autoConnect) {
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then((ports) => {
if (
ports.some((port) => Port.equals(port, boardsConfig.selectedPort))
) {
new Promise<void>((resolve) => {
// First, disconnect if connected.
if (this.connected) {
this.disconnect().then(() => resolve());
return;
}
resolve();
}).then(() => {
// Then (re-)connect.
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.connect({ board, port, baudRate });
});
}
});
}
}
}
} }
export namespace MonitorConnection { export namespace MonitorConnection {
export interface State { export interface State {
readonly config: MonitorConfig; readonly config: MonitorConfig;
} }
export namespace State { export namespace State {
export function toString(state: State): string { export function toString(state: State): string {
const { config } = state; const { config } = state;
const { board, port } = config; const { board, port } = config;
return `${Board.toString(board)} ${Port.toString(port)}`; return `${Board.toString(board)} ${Port.toString(port)}`;
}
} }
}
} }

View File

@@ -2,143 +2,143 @@ import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event'; import { Emitter, Event } from '@theia/core/lib/common/event';
import { MonitorConfig } from '../../common/protocol/monitor-service'; import { MonitorConfig } from '../../common/protocol/monitor-service';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
LocalStorageService, LocalStorageService,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
@injectable() @injectable()
export class MonitorModel implements FrontendApplicationContribution { export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model'; protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService) @inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService; protected readonly localStorageService: LocalStorageService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly onChangeEmitter: Emitter< protected readonly onChangeEmitter: Emitter<
MonitorModel.State.Change<keyof MonitorModel.State> MonitorModel.State.Change<keyof MonitorModel.State>
>; >;
protected _autoscroll: boolean; protected _autoscroll: boolean;
protected _timestamp: boolean; protected _timestamp: boolean;
protected _baudRate: MonitorConfig.BaudRate; protected _baudRate: MonitorConfig.BaudRate;
protected _lineEnding: MonitorModel.EOL; protected _lineEnding: MonitorModel.EOL;
constructor() { constructor() {
this._autoscroll = true; this._autoscroll = true;
this._timestamp = false; this._timestamp = false;
this._baudRate = MonitorConfig.BaudRate.DEFAULT; this._baudRate = MonitorConfig.BaudRate.DEFAULT;
this._lineEnding = MonitorModel.EOL.DEFAULT; this._lineEnding = MonitorModel.EOL.DEFAULT;
this.onChangeEmitter = new Emitter< this.onChangeEmitter = new Emitter<
MonitorModel.State.Change<keyof MonitorModel.State> MonitorModel.State.Change<keyof MonitorModel.State>
>(); >();
} }
onStart(): void { onStart(): void {
this.localStorageService this.localStorageService
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID) .getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.then((state) => { .then((state) => {
if (state) { if (state) {
this.restoreState(state); this.restoreState(state);
} }
}); });
} }
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> { get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event; return this.onChangeEmitter.event;
} }
get autoscroll(): boolean { get autoscroll(): boolean {
return this._autoscroll; return this._autoscroll;
} }
toggleAutoscroll(): void { toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll; this._autoscroll = !this._autoscroll;
this.storeState(); this.storeState();
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'autoscroll', property: 'autoscroll',
value: this._autoscroll, value: this._autoscroll,
}) })
); );
} }
get timestamp(): boolean { get timestamp(): boolean {
return this._timestamp; return this._timestamp;
} }
toggleTimestamp(): void { toggleTimestamp(): void {
this._timestamp = !this._timestamp; this._timestamp = !this._timestamp;
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'timestamp', property: 'timestamp',
value: this._timestamp, value: this._timestamp,
}) })
); );
} }
get baudRate(): MonitorConfig.BaudRate { get baudRate(): MonitorConfig.BaudRate {
return this._baudRate; return this._baudRate;
} }
set baudRate(baudRate: MonitorConfig.BaudRate) { set baudRate(baudRate: MonitorConfig.BaudRate) {
this._baudRate = baudRate; this._baudRate = baudRate;
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'baudRate', property: 'baudRate',
value: this._baudRate, value: this._baudRate,
}) })
); );
} }
get lineEnding(): MonitorModel.EOL { get lineEnding(): MonitorModel.EOL {
return this._lineEnding; return this._lineEnding;
} }
set lineEnding(lineEnding: MonitorModel.EOL) { set lineEnding(lineEnding: MonitorModel.EOL) {
this._lineEnding = lineEnding; this._lineEnding = lineEnding;
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'lineEnding', property: 'lineEnding',
value: this._lineEnding, value: this._lineEnding,
}) })
); );
} }
protected restoreState(state: MonitorModel.State): void { protected restoreState(state: MonitorModel.State): void {
this._autoscroll = state.autoscroll; this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp; this._timestamp = state.timestamp;
this._baudRate = state.baudRate; this._baudRate = state.baudRate;
this._lineEnding = state.lineEnding; this._lineEnding = state.lineEnding;
} }
protected async storeState(): Promise<void> { protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, { return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll, autoscroll: this._autoscroll,
timestamp: this._timestamp, timestamp: this._timestamp,
baudRate: this._baudRate, baudRate: this._baudRate,
lineEnding: this._lineEnding, lineEnding: this._lineEnding,
}); });
} }
} }
export namespace MonitorModel { export namespace MonitorModel {
export interface State { export interface State {
autoscroll: boolean; autoscroll: boolean;
timestamp: boolean; timestamp: boolean;
baudRate: MonitorConfig.BaudRate; baudRate: MonitorConfig.BaudRate;
lineEnding: EOL; lineEnding: EOL;
} }
export namespace State { export namespace State {
export interface Change<K extends keyof State> { export interface Change<K extends keyof State> {
readonly property: K; readonly property: K;
readonly value: State[K]; readonly value: State[K];
}
} }
}
export type EOL = '' | '\n' | '\r' | '\r\n'; export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL { export namespace EOL {
export const DEFAULT: EOL = '\n'; export const DEFAULT: EOL = '\n';
} }
} }

View File

@@ -1,16 +1,16 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { import {
MonitorServiceClient, MonitorServiceClient,
MonitorError, MonitorError,
} from '../../common/protocol/monitor-service'; } from '../../common/protocol/monitor-service';
@injectable() @injectable()
export class MonitorServiceClientImpl implements MonitorServiceClient { export class MonitorServiceClientImpl implements MonitorServiceClient {
protected readonly onErrorEmitter = new Emitter<MonitorError>(); protected readonly onErrorEmitter = new Emitter<MonitorError>();
readonly onError = this.onErrorEmitter.event; readonly onError = this.onErrorEmitter.event;
notifyError(error: MonitorError): void { notifyError(error: MonitorError): void {
this.onErrorEmitter.fire(error); this.onErrorEmitter.fire(error);
} }
} }

View File

@@ -4,153 +4,153 @@ import { AbstractViewContribution } from '@theia/core/lib/browser';
import { MonitorWidget } from './monitor-widget'; import { MonitorWidget } from './monitor-widget';
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core'; import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
import { import {
TabBarToolbarContribution, TabBarToolbarContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { MonitorModel } from './monitor-model'; import { MonitorModel } from './monitor-model';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
export namespace SerialMonitor { export namespace SerialMonitor {
export namespace Commands { export namespace Commands {
export const AUTOSCROLL: Command = { export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll', id: 'serial-monitor-autoscroll',
label: 'Autoscroll', label: 'Autoscroll',
}; };
export const TIMESTAMP: Command = { export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp', id: 'serial-monitor-timestamp',
label: 'Timestamp', label: 'Timestamp',
}; };
export const CLEAR_OUTPUT: Command = { export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output', id: 'serial-monitor-clear-output',
label: 'Clear Output', label: 'Clear Output',
iconClass: 'clear-all', iconClass: 'clear-all',
}; };
} }
} }
@injectable() @injectable()
export class MonitorViewContribution export class MonitorViewContribution
extends AbstractViewContribution<MonitorWidget> extends AbstractViewContribution<MonitorWidget>
implements TabBarToolbarContribution implements TabBarToolbarContribution
{ {
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle'; static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR = static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
MonitorWidget.ID + ':toggle-toolbar'; MonitorWidget.ID + ':toggle-toolbar';
@inject(MonitorModel) protected readonly model: MonitorModel; @inject(MonitorModel) protected readonly model: MonitorModel;
constructor() { constructor() {
super({ super({
widgetId: MonitorWidget.ID, widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor', widgetName: 'Serial Monitor',
defaultWidgetOptions: { defaultWidgetOptions: {
area: 'bottom', area: 'bottom',
}, },
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR, toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
toggleKeybinding: 'CtrlCmd+Shift+M', toggleKeybinding: 'CtrlCmd+Shift+M',
}); });
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor',
order: '5',
});
} }
}
registerMenus(menus: MenuModelRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
if (this.toggleCommand) { registry.registerItem({
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { id: 'monitor-autoscroll',
commandId: this.toggleCommand.id, render: () => this.renderAutoScrollButton(),
label: 'Serial Monitor', isVisible: (widget) => widget instanceof MonitorWidget,
order: '5', onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
}); });
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output',
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
execute: (widget) => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
} }
} },
});
registerToolbarItems(registry: TabBarToolbarRegistry): void { if (this.toggleCommand) {
registry.registerItem({ commands.registerCommand(this.toggleCommand, {
id: 'monitor-autoscroll', execute: () => this.toggle(),
render: () => this.renderAutoScrollButton(), });
isVisible: (widget) => widget instanceof MonitorWidget, commands.registerCommand(
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/ { id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR },
}); {
registry.registerItem({ isVisible: (widget) =>
id: 'monitor-timestamp', ArduinoToolbar.is(widget) && widget.side === 'right',
render: () => this.renderTimestampButton(), execute: () => this.toggle(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output',
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
execute: (widget) => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
}
},
});
if (this.toggleCommand) {
commands.registerCommand(this.toggleCommand, {
execute: () => this.toggle(),
});
commands.registerCommand(
{ id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR },
{
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'right',
execute: () => this.toggle(),
}
);
} }
);
} }
}
protected async toggle(): Promise<void> { protected async toggle(): Promise<void> {
const widget = this.tryGetWidget(); const widget = this.tryGetWidget();
if (widget) { if (widget) {
widget.dispose(); widget.dispose();
} else { } else {
await this.openView({ activate: true, reveal: true }); await this.openView({ activate: true, reveal: true });
}
} }
}
protected renderAutoScrollButton(): React.ReactNode { protected renderAutoScrollButton(): React.ReactNode {
return ( return (
<React.Fragment key="autoscroll-toolbar-item"> <React.Fragment key="autoscroll-toolbar-item">
<div <div
title="Toggle Autoscroll" title="Toggle Autoscroll"
className={`item enabled fa fa-angle-double-down arduino-monitor ${ className={`item enabled fa fa-angle-double-down arduino-monitor ${
this.model.autoscroll ? 'toggled' : '' this.model.autoscroll ? 'toggled' : ''
}`} }`}
onClick={this.toggleAutoScroll} onClick={this.toggleAutoScroll}
></div> ></div>
</React.Fragment> </React.Fragment>
); );
} }
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll(); protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll(): Promise<void> { protected async doToggleAutoScroll(): Promise<void> {
this.model.toggleAutoscroll(); this.model.toggleAutoscroll();
} }
protected renderTimestampButton(): React.ReactNode { protected renderTimestampButton(): React.ReactNode {
return ( return (
<React.Fragment key="line-ending-toolbar-item"> <React.Fragment key="line-ending-toolbar-item">
<div <div
title="Toggle Timestamp" title="Toggle Timestamp"
className={`item enabled fa fa-clock-o arduino-monitor ${ className={`item enabled fa fa-clock-o arduino-monitor ${
this.model.timestamp ? 'toggled' : '' this.model.timestamp ? 'toggled' : ''
}`} }`}
onClick={this.toggleTimestamp} onClick={this.toggleTimestamp}
></div> ></div>
</React.Fragment> </React.Fragment>
); );
} }
protected readonly toggleTimestamp = () => this.doToggleTimestamp(); protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp(): Promise<void> { protected async doToggleTimestamp(): Promise<void> {
this.model.toggleTimestamp(); this.model.toggleTimestamp();
} }
} }

View File

@@ -6,14 +6,14 @@ import { isOSX } from '@theia/core/lib/common/os';
import { Event, Emitter } from '@theia/core/lib/common/event'; import { Event, Emitter } from '@theia/core/lib/common/event';
import { Key, KeyCode } from '@theia/core/lib/browser/keys'; import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { import {
DisposableCollection, DisposableCollection,
Disposable, Disposable,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { import {
ReactWidget, ReactWidget,
Message, Message,
Widget, Widget,
MessageLoop, MessageLoop,
} from '@theia/core/lib/browser/widgets'; } from '@theia/core/lib/browser/widgets';
import { Board, Port } from '../../common/protocol/boards-service'; import { Board, Port } from '../../common/protocol/boards-service';
import { MonitorConfig } from '../../common/protocol/monitor-service'; import { MonitorConfig } from '../../common/protocol/monitor-service';
@@ -24,379 +24,371 @@ import { MonitorServiceClientImpl } from './monitor-service-client-impl';
@injectable() @injectable()
export class MonitorWidget extends ReactWidget { export class MonitorWidget extends ReactWidget {
static readonly ID = 'serial-monitor'; static readonly ID = 'serial-monitor';
@inject(MonitorModel) @inject(MonitorModel)
protected readonly monitorModel: MonitorModel; protected readonly monitorModel: MonitorModel;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@inject(MonitorServiceClientImpl) @inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl; protected readonly monitorServiceClient: MonitorServiceClientImpl;
protected widgetHeight: number; protected widgetHeight: number;
/** /**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation. * Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/ */
protected focusNode: HTMLElement | undefined; protected focusNode: HTMLElement | undefined;
/** /**
* Guard against re-rendering the view after the close was requested. * Guard against re-rendering the view after the close was requested.
* See: https://github.com/eclipse-theia/theia/issues/6704 * See: https://github.com/eclipse-theia/theia/issues/6704
*/ */
protected closing = false; protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>(); protected readonly clearOutputEmitter = new Emitter<void>();
constructor() { constructor() {
super(); super();
this.id = MonitorWidget.ID; this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor'; this.title.label = 'Serial Monitor';
this.title.iconClass = 'monitor-tab-icon'; this.title.iconClass = 'monitor-tab-icon';
this.title.closable = true; this.title.closable = true;
this.scrollOptions = undefined; this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter); this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push( this.toDispose.push(
Disposable.create(() => { Disposable.create(() => {
this.monitorConnection.autoConnect = false; this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) { if (this.monitorConnection.connected) {
this.monitorConnection.disconnect(); this.monitorConnection.disconnect();
}
})
);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(
this.monitorConnection.onConnectionChanged(() =>
this.clearConsole()
)
);
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
} }
})
);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(
this.monitorConnection.onConnectionChanged(() => this.clearConsole())
);
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
} }
}
protected onResize(msg: Widget.ResizeMessage): void { protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg); super.onResize(msg);
this.widgetHeight = msg.height; this.widgetHeight = msg.height;
this.update(); this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
if (this.closing || !this.isAttached) {
return;
} }
this.focusNode = element;
requestAnimationFrame(() =>
MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest)
);
};
protected onActivateRequest(msg: Message): void { protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
super.onActivateRequest(msg); return [
(this.focusNode || this.node).focus(); {
} label: 'No Line Ending',
value: '',
},
{
label: 'New Line',
value: '\n',
},
{
label: 'Carriage Return',
value: '\r',
},
{
label: 'Both NL & CR',
value: '\r\n',
},
];
}
protected onFocusResolved = (element: HTMLElement | undefined) => { protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
if (this.closing || !this.isAttached) { const baudRates: Array<MonitorConfig.BaudRate> = [
return; 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
} ];
this.focusNode = element; return baudRates.map((baudRate) => ({
requestAnimationFrame(() => label: baudRate + ' baud',
MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest) value: baudRate,
); }));
}; }
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> { protected render(): React.ReactNode {
return [ const { baudRates, lineEndings } = this;
{ const lineEnding =
label: 'No Line Ending', lineEndings.find((item) => item.value === this.monitorModel.lineEnding) ||
value: '', lineEndings[1]; // Defaults to `\n`.
}, const baudRate =
{ baudRates.find((item) => item.value === this.monitorModel.baudRate) ||
label: 'New Line', baudRates[4]; // Defaults to `9600`.
value: '\n', return (
}, <div className="serial-monitor">
{ <div className="head">
label: 'Carriage Return', <div className="send">
value: '\r', <SerialMonitorSendInput
}, monitorConfig={this.monitorConnection.monitorConfig}
{ resolveFocus={this.onFocusResolved}
label: 'Both NL & CR', onSend={this.onSend}
value: '\r\n', />
}, </div>
]; <div className="config">
} <div className="select">
<ArduinoSelect
protected get baudRates(): OptionsType< maxMenuHeight={this.widgetHeight - 40}
SelectOption<MonitorConfig.BaudRate> options={lineEndings}
> { defaultValue={lineEnding}
const baudRates: Array<MonitorConfig.BaudRate> = [ onChange={this.onChangeLineEnding}
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, />
];
return baudRates.map((baudRate) => ({
label: baudRate + ' baud',
value: baudRate,
}));
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const lineEnding =
lineEndings.find(
(item) => item.value === this.monitorModel.lineEnding
) || lineEndings[1]; // Defaults to `\n`.
const baudRate =
baudRates.find(
(item) => item.value === this.monitorModel.baudRate
) || baudRates[4]; // Defaults to `9600`.
return (
<div className="serial-monitor">
<div className="head">
<div className="send">
<SerialMonitorSendInput
monitorConfig={this.monitorConnection.monitorConfig}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>
</div>
<div className="config">
<div className="select">
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
defaultValue={lineEnding}
onChange={this.onChangeLineEnding}
/>
</div>
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
</div>
</div>
<div className="body">
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event}
/>
</div>
</div> </div>
); <div className="select">
} <ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
</div>
</div>
<div className="body">
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event}
/>
</div>
</div>
);
}
protected readonly onSend = (value: string) => this.doSend(value); protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> { protected async doSend(value: string): Promise<void> {
this.monitorConnection.send(value); this.monitorConnection.send(value);
} }
protected readonly onChangeLineEnding = ( protected readonly onChangeLineEnding = (
option: SelectOption<MonitorModel.EOL> option: SelectOption<MonitorModel.EOL>
) => { ) => {
this.monitorModel.lineEnding = option.value; this.monitorModel.lineEnding = option.value;
}; };
protected readonly onChangeBaudRate = ( protected readonly onChangeBaudRate = (
option: SelectOption<MonitorConfig.BaudRate> option: SelectOption<MonitorConfig.BaudRate>
) => { ) => {
this.monitorModel.baudRate = option.value; this.monitorModel.baudRate = option.value;
}; };
} }
export namespace SerialMonitorSendInput { export namespace SerialMonitorSendInput {
export interface Props { export interface Props {
readonly monitorConfig?: MonitorConfig; readonly monitorConfig?: MonitorConfig;
readonly onSend: (text: string) => void; readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void; readonly resolveFocus: (element: HTMLElement | undefined) => void;
} }
export interface State { export interface State {
text: string; text: string;
} }
} }
export class SerialMonitorSendInput extends React.Component< export class SerialMonitorSendInput extends React.Component<
SerialMonitorSendInput.Props, SerialMonitorSendInput.Props,
SerialMonitorSendInput.State SerialMonitorSendInput.State
> { > {
constructor(props: Readonly<SerialMonitorSendInput.Props>) { constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props); super(props);
this.state = { text: '' }; this.state = { text: '' };
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this); this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
} }
render(): React.ReactNode { render(): React.ReactNode {
return ( return (
<input <input
ref={this.setRef} ref={this.setRef}
type="text" type="text"
className={`theia-input ${ className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
this.props.monitorConfig ? '' : 'warning' placeholder={this.placeholder}
}`} value={this.state.text}
placeholder={this.placeholder} onChange={this.onChange}
value={this.state.text} onKeyDown={this.onKeyDown}
onChange={this.onChange} />
onKeyDown={this.onKeyDown} );
/> }
);
}
protected get placeholder(): string { protected get placeholder(): string {
const { monitorConfig } = this.props; const { monitorConfig } = this.props;
if (!monitorConfig) { if (!monitorConfig) {
return 'Not connected. Select a board and a port to connect automatically.'; return 'Not connected. Select a board and a port to connect automatically.';
}
const { board, port } = monitorConfig;
return `Message (${
isOSX ? '⌘' : 'Ctrl'
}+Enter to send message to '${Board.toString(board, {
useFqbn: false,
})}' on '${Port.toString(port)}')`;
} }
const { board, port } = monitorConfig;
return `Message (${
isOSX ? '⌘' : 'Ctrl'
}+Enter to send message to '${Board.toString(board, {
useFqbn: false,
})}' on '${Port.toString(port)}')`;
}
protected setRef = (element: HTMLElement | null) => { protected setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) { if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined); this.props.resolveFocus(element || undefined);
}
};
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ text: event.target.value });
} }
};
protected onSend(): void { protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.props.onSend(this.state.text); this.setState({ text: event.target.value });
this.setState({ text: '' }); }
}
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { protected onSend(): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent); this.props.onSend(this.state.text);
if (keyCode) { this.setState({ text: '' });
const { key, meta, ctrl } = keyCode; }
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend(); protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
} const keyCode = KeyCode.createKeyCode(event.nativeEvent);
} if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend();
}
} }
}
} }
export namespace SerialMonitorOutput { export namespace SerialMonitorOutput {
export interface Props { export interface Props {
readonly monitorModel: MonitorModel; readonly monitorModel: MonitorModel;
readonly monitorConnection: MonitorConnection; readonly monitorConnection: MonitorConnection;
readonly clearConsoleEvent: Event<void>; readonly clearConsoleEvent: Event<void>;
} }
export interface State { export interface State {
content: string; content: string;
timestamp: boolean; timestamp: boolean;
} }
} }
export class SerialMonitorOutput extends React.Component< export class SerialMonitorOutput extends React.Component<
SerialMonitorOutput.Props, SerialMonitorOutput.Props,
SerialMonitorOutput.State SerialMonitorOutput.State
> { > {
/** /**
* Do not touch it. It is used to be able to "follow" the serial monitor log. * Do not touch it. It is used to be able to "follow" the serial monitor log.
*/ */
protected anchor: HTMLElement | null; protected anchor: HTMLElement | null;
protected toDisposeBeforeUnmount = new DisposableCollection(); protected toDisposeBeforeUnmount = new DisposableCollection();
constructor(props: Readonly<SerialMonitorOutput.Props>) { constructor(props: Readonly<SerialMonitorOutput.Props>) {
super(props); super(props);
this.state = { this.state = {
content: '', content: '',
timestamp: this.props.monitorModel.timestamp, timestamp: this.props.monitorModel.timestamp,
}; };
} }
render(): React.ReactNode { render(): React.ReactNode {
return ( return (
<React.Fragment> <React.Fragment>
<div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}> <div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}>
{this.state.content} {this.state.content}
</div> </div>
<div <div
style={{ float: 'left', clear: 'both' }} style={{ float: 'left', clear: 'both' }}
ref={(element) => { ref={(element) => {
this.anchor = element; this.anchor = element;
}} }}
/> />
</React.Fragment> </React.Fragment>
); );
} }
componentDidMount(): void { componentDidMount(): void {
this.scrollToBottom(); this.scrollToBottom();
this.toDisposeBeforeUnmount.pushAll([ this.toDisposeBeforeUnmount.pushAll([
this.props.monitorConnection.onRead(({ message }) => { this.props.monitorConnection.onRead(({ message }) => {
const rawLines = message.split('\n'); const rawLines = message.split('\n');
const lines: string[] = []; const lines: string[] = [];
const timestamp = () => const timestamp = () =>
this.state.timestamp this.state.timestamp
? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` ? `${dateFormat(new Date(), 'H:M:ss.l')} -> `
: ''; : '';
for (let i = 0; i < rawLines.length; i++) { for (let i = 0; i < rawLines.length; i++) {
if (i === 0 && this.state.content.length !== 0) { if (i === 0 && this.state.content.length !== 0) {
lines.push(rawLines[i]); lines.push(rawLines[i]);
} else { } else {
lines.push(timestamp() + rawLines[i]); lines.push(timestamp() + rawLines[i]);
} }
}
const content = this.state.content + lines.join('\n');
this.setState({ content });
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
}),
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
} }
const content = this.state.content + lines.join('\n');
this.setState({ content });
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
}),
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
} }
}
} }
export interface SelectOption<T> { export interface SelectOption<T> {
readonly label: string; readonly label: string;
readonly value: T; readonly value: T;
} }

View File

@@ -4,117 +4,117 @@ 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'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
NotificationServiceClient, NotificationServiceClient,
NotificationServiceServer, NotificationServiceServer,
} from '../common/protocol/notification-service'; } from '../common/protocol/notification-service';
import { import {
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
BoardsPackage, BoardsPackage,
LibraryPackage, LibraryPackage,
Config, Config,
Sketch, Sketch,
} from '../common/protocol'; } from '../common/protocol';
@injectable() @injectable()
export class NotificationCenter export class NotificationCenter
implements NotificationServiceClient, FrontendApplicationContribution implements NotificationServiceClient, FrontendApplicationContribution
{ {
@inject(NotificationServiceServer) @inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>; protected readonly server: JsonRpcProxy<NotificationServiceServer>;
protected readonly indexUpdatedEmitter = new Emitter<void>(); protected readonly indexUpdatedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<void>(); protected readonly daemonStartedEmitter = new Emitter<void>();
protected readonly daemonStoppedEmitter = new Emitter<void>(); protected readonly daemonStoppedEmitter = new Emitter<void>();
protected readonly configChangedEmitter = new Emitter<{ protected readonly configChangedEmitter = new Emitter<{
config: Config | undefined; config: Config | undefined;
}>(); }>();
protected readonly platformInstalledEmitter = new Emitter<{ protected readonly platformInstalledEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
protected readonly platformUninstalledEmitter = new Emitter<{ protected readonly platformUninstalledEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
protected readonly libraryInstalledEmitter = new Emitter<{ protected readonly libraryInstalledEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
}>(); }>();
protected readonly libraryUninstalledEmitter = new Emitter<{ protected readonly libraryUninstalledEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
}>(); }>();
protected readonly attachedBoardsChangedEmitter = protected readonly attachedBoardsChangedEmitter =
new Emitter<AttachedBoardsChangeEvent>(); new Emitter<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{ protected readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[]; sketches: Sketch[];
}>(); }>();
protected readonly toDispose = new DisposableCollection( protected readonly toDispose = new DisposableCollection(
this.indexUpdatedEmitter, this.indexUpdatedEmitter,
this.daemonStartedEmitter, this.daemonStartedEmitter,
this.daemonStoppedEmitter, this.daemonStoppedEmitter,
this.configChangedEmitter, this.configChangedEmitter,
this.platformInstalledEmitter, this.platformInstalledEmitter,
this.platformUninstalledEmitter, this.platformUninstalledEmitter,
this.libraryInstalledEmitter, this.libraryInstalledEmitter,
this.libraryUninstalledEmitter, this.libraryUninstalledEmitter,
this.attachedBoardsChangedEmitter this.attachedBoardsChangedEmitter
); );
readonly onIndexUpdated = this.indexUpdatedEmitter.event; readonly onIndexUpdated = this.indexUpdatedEmitter.event;
readonly onDaemonStarted = this.daemonStartedEmitter.event; readonly onDaemonStarted = this.daemonStartedEmitter.event;
readonly onDaemonStopped = this.daemonStoppedEmitter.event; readonly onDaemonStopped = this.daemonStoppedEmitter.event;
readonly onConfigChanged = this.configChangedEmitter.event; readonly onConfigChanged = this.configChangedEmitter.event;
readonly onPlatformInstalled = this.platformInstalledEmitter.event; readonly onPlatformInstalled = this.platformInstalledEmitter.event;
readonly onPlatformUninstalled = this.platformUninstalledEmitter.event; readonly onPlatformUninstalled = this.platformUninstalledEmitter.event;
readonly onLibraryInstalled = this.libraryInstalledEmitter.event; readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event; readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event; readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event; readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.server.setClient(this); this.server.setClient(this);
} }
onStop(): void { onStop(): void {
this.toDispose.dispose(); this.toDispose.dispose();
} }
notifyIndexUpdated(): void { notifyIndexUpdated(): void {
this.indexUpdatedEmitter.fire(); this.indexUpdatedEmitter.fire();
} }
notifyDaemonStarted(): void { notifyDaemonStarted(): void {
this.daemonStartedEmitter.fire(); this.daemonStartedEmitter.fire();
} }
notifyDaemonStopped(): void { notifyDaemonStopped(): void {
this.daemonStoppedEmitter.fire(); this.daemonStoppedEmitter.fire();
} }
notifyConfigChanged(event: { config: Config | undefined }): void { notifyConfigChanged(event: { config: Config | undefined }): void {
this.configChangedEmitter.fire(event); this.configChangedEmitter.fire(event);
} }
notifyPlatformInstalled(event: { item: BoardsPackage }): void { notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.platformInstalledEmitter.fire(event); this.platformInstalledEmitter.fire(event);
} }
notifyPlatformUninstalled(event: { item: BoardsPackage }): void { notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.platformUninstalledEmitter.fire(event); this.platformUninstalledEmitter.fire(event);
} }
notifyLibraryInstalled(event: { item: LibraryPackage }): void { notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.libraryInstalledEmitter.fire(event); this.libraryInstalledEmitter.fire(event);
} }
notifyLibraryUninstalled(event: { item: LibraryPackage }): void { notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.libraryUninstalledEmitter.fire(event); this.libraryUninstalledEmitter.fire(event);
} }
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.attachedBoardsChangedEmitter.fire(event); this.attachedBoardsChangedEmitter.fire(event);
} }
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void { notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.recentSketchesChangedEmitter.fire(event); this.recentSketchesChangedEmitter.fire(event);
} }
} }

View File

@@ -3,35 +3,34 @@ import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution'; import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { import {
ResponseService, ResponseService,
OutputMessage, OutputMessage,
ProgressMessage, ProgressMessage,
} from '../common/protocol/response-service'; } from '../common/protocol/response-service';
@injectable() @injectable()
export class ResponseServiceImpl implements ResponseService { export class ResponseServiceImpl implements ResponseService {
@inject(OutputContribution) @inject(OutputContribution)
protected outputContribution: OutputContribution; protected outputContribution: OutputContribution;
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager; protected outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
new Emitter<ProgressMessage>(); readonly onProgressDidChange = this.progressDidChangeEmitter.event;
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
appendToOutput(message: OutputMessage): void { appendToOutput(message: OutputMessage): void {
const { chunk } = message; const { chunk } = message;
const channel = this.outputChannelManager.getChannel('Arduino'); const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true }); channel.show({ preserveFocus: true });
channel.append(chunk); channel.append(chunk);
} }
clearArduinoChannel(): void { clearArduinoChannel(): void {
this.outputChannelManager.getChannel('Arduino').clear(); this.outputChannelManager.getChannel('Arduino').clear();
} }
reportProgress(progress: ProgressMessage): void { reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress); this.progressDidChangeEmitter.fire(progress);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { StorageService } from '@theia/core/lib/browser/storage-service'; import { StorageService } from '@theia/core/lib/browser/storage-service';
import { import {
Command, Command,
CommandContribution, CommandContribution,
CommandRegistry, CommandRegistry,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
/** /**
@@ -11,27 +11,27 @@ import {
*/ */
@injectable() @injectable()
export class StorageWrapper implements CommandContribution { export class StorageWrapper implements CommandContribution {
@inject(StorageService) @inject(StorageService)
protected storageService: StorageService; protected storageService: StorageService;
registerCommands(commands: CommandRegistry): void { registerCommands(commands: CommandRegistry): void {
commands.registerCommand(StorageWrapper.Commands.GET_DATA, { commands.registerCommand(StorageWrapper.Commands.GET_DATA, {
execute: (key: string, defaultValue?: any) => execute: (key: string, defaultValue?: any) =>
this.storageService.getData(key, defaultValue), this.storageService.getData(key, defaultValue),
}); });
commands.registerCommand(StorageWrapper.Commands.SET_DATA, { commands.registerCommand(StorageWrapper.Commands.SET_DATA, {
execute: (key: string, value: any) => execute: (key: string, value: any) =>
this.storageService.setData(key, value), this.storageService.setData(key, value),
}); });
} }
} }
export namespace StorageWrapper { export namespace StorageWrapper {
export namespace Commands { export namespace Commands {
export const SET_DATA: Command = { export const SET_DATA: Command = {
id: 'arduino-store-wrapper-set', id: 'arduino-store-wrapper-set',
}; };
export const GET_DATA: Command = { export const GET_DATA: Command = {
id: 'arduino-store-wrapper-get', id: 'arduino-store-wrapper-get',
}; };
} }
} }

View File

@@ -0,0 +1,74 @@
.certificate-uploader-dialog {
width: 600px;
}
.certificate-uploader-dialog .theia-select {
border: none !important;
}
.certificate-uploader-dialog .arduino-select__control {
height: 31px;
background: var(--theia-menubar-selectionBackground) !important;
}
.certificate-uploader-dialog .dialogRow > button{
margin-right: 3px;
}
.certificate-uploader-dialog .certificate-list {
border: 1px solid #BDC7C7;
border-radius: 2px;;
background: var(--theia-menubar-selectionBackground) !important;
overflow: auto;
height: 120px;
flex: 1;
}
.certificate-uploader-dialog .certificate-list .certificate-row {
display: flex;
padding: 6px 10px 5px 10px
}
.certificate-uploader-dialog .certificate-list .certificate-row:hover {
background-color: var(--theia-list-activeSelectionBackground);
}
.certificate-uploader-dialog .upload-status {
display: flex;
align-items: center;
flex: 1;
}
.certificate-uploader-dialog .success {
display: flex;
align-items: center;
color: #1DA086;
}
.certificate-uploader-dialog .warn {
color: #C11F09;
}
.certificate-uploader-dialog .status-icon {
margin-right: 10px;
}
.certificate-uploader-dialog .add-cert-btn {
display: flex;
align-items: center;
justify-content: space-between;
}
.certificate-uploader-dialog .add-cert-btn .caret {
margin-left: 6px;
}
.certificate-add {
padding: 16px;
background-color: var(--theia-list-hoverBackground);
border-radius: 3px;
border: 1px solid #BDC7C7;
}
.certificate-add input {
margin-top: 12px;
padding: 0 12px;
width: 100%;
box-sizing: border-box;
}

View File

@@ -0,0 +1,65 @@
.p-Widget.dialogOverlay .dialogBlock {
border-radius: 3px;
padding: 0 28px;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.25);
min-height: 0px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
padding: 36px 0 28px;
font-weight: 500;
background-color: transparent;
font-size: var(--theia-ui-font-size2);
color: var(--theia-list-inactiveSelectionForeground);
min-height: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogControl {
padding: 0 0 36px;
min-height: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent {
padding: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
padding: 0 0 12px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
margin-top: 28px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
margin-top: 0;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow {
margin-top: 16px;
display: flex;
align-items: center;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
background: var(--theia-icon-loading) center center no-repeat;
animation: theia-spin 1.25s linear infinite;
width: 30px;
height: 30px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
margin-top: 0px;
}
.fl1{
flex: 1;
}
.status-icon {
margin-right: 6px;
font-size: 17px;
}
.fa.disabled {
opacity: .4;
}

View File

@@ -0,0 +1,32 @@
.firmware-uploader-dialog {
width: 600px;
}
.firmware-uploader-dialog .theia-select {
border: none !important;
}
.firmware-uploader-dialog .arduino-select__control {
height: 31px;
background: var(--theia-menubar-selectionBackground) !important;
}
.firmware-uploader-dialog .dialogRow > button{
width: 33%;
margin-right: 3px;
}
.firmware-uploader-dialog #firmware-select {
flex: unset;
}
.firmware-uploader-dialog .success {
color: #1DA086;
}
.firmware-uploader-dialog .warn {
color: #C11F09;
}
.firmware-uploader-dialog .status-icon {
margin-right: 10px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 667 KiB

View File

@@ -1,15 +1,19 @@
@import './list-widget.css'; @import './list-widget.css';
@import './boards-config-dialog.css'; @import './boards-config-dialog.css';
@import './main.css'; @import './main.css';
@import './dialogs.css';
@import './monitor.css'; @import './monitor.css';
@import './arduino-select.css'; @import './arduino-select.css';
@import './status-bar.css'; @import './status-bar.css';
@import './terminal.css'; @import './terminal.css';
@import './editor.css'; @import './editor.css';
@import './settings-dialog.css'; @import './settings-dialog.css';
@import './firmware-uploader-dialog.css';
@import './certificate-uploader-dialog.css';
@import './debug.css'; @import './debug.css';
@import './sketchbook.css'; @import './sketchbook.css';
@import './cloud-sketchbook.css'; @import './cloud-sketchbook.css';
@import './fonts.css';
.theia-input.warning:focus { .theia-input.warning:focus {
outline-width: 1px; outline-width: 1px;

View File

@@ -169,10 +169,6 @@
margin-left: 10px; margin-left: 10px;
} }
.p-Widget.dialogOverlay .dialogBlock {
background-color: var(--theia-arduino-foreground);
}
#arduino-open-sketch-control--toolbar--container { #arduino-open-sketch-control--toolbar--container {
background-color: var(--theia-arduino-toolbar-background); background-color: var(--theia-arduino-toolbar-background);
} }

View File

@@ -50,7 +50,9 @@
.additional-urls-dialog .link:hover { .additional-urls-dialog .link:hover {
color: var(--theia-textLink-activeForeground); color: var(--theia-textLink-activeForeground);
} }
.arduino-settings-dialog .react-tabs__tab-panel {
padding-bottom: 25px;
}
.arduino-settings-dialog .react-tabs__tab-list { .arduino-settings-dialog .react-tabs__tab-list {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -4,12 +4,12 @@ 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 { OutputWidget } from '@theia/output/lib/browser/output-widget'; import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { import {
ConnectionStatusService, ConnectionStatusService,
ConnectionStatus, ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service'; } from '@theia/core/lib/browser/connection-status-service';
import { import {
ApplicationShell as TheiaApplicationShell, ApplicationShell as TheiaApplicationShell,
Widget, Widget,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol'; import { Sketch } from '../../../common/protocol';
import { SaveAsSketch } from '../../contributions/save-as-sketch'; import { SaveAsSketch } from '../../contributions/save-as-sketch';
@@ -17,76 +17,75 @@ import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-ser
@injectable() @injectable()
export class ApplicationShell extends TheiaApplicationShell { export class ApplicationShell extends TheiaApplicationShell {
@inject(CommandService) @inject(CommandService)
protected readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(ConnectionStatusService) @inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService; protected readonly connectionStatusService: ConnectionStatusService;
protected track(widget: Widget): void { protected track(widget: Widget): void {
super.track(widget); super.track(widget);
if (widget instanceof OutputWidget) { if (widget instanceof OutputWidget) {
widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700 widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
}
if (widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
this.sketchesServiceClient.currentSketch().then((sketch) => {
if (sketch) {
if (Sketch.isInSketch(widget.editor.uri, sketch)) {
widget.title.closable = false;
}
}
});
}
} }
if (widget instanceof EditorWidget) {
async addWidget( // Make the editor un-closeable asynchronously.
widget: Widget, this.sketchesServiceClient.currentSketch().then((sketch) => {
options: Readonly<TheiaApplicationShell.WidgetOptions> = {} if (sketch) {
): Promise<void> { if (Sketch.isInSketch(widget.editor.uri, sketch)) {
// By default, Theia open a widget **next** to the currently active in the target area. widget.title.closable = false;
// Instead of this logic, we want to open the new widget after the last of the target area. }
if (!widget.id) {
console.error(
'Widgets added to the application shell must have a unique id property.'
);
return;
} }
let ref: Widget | undefined = options.ref; });
const area: TheiaApplicationShell.Area = options.area || 'main';
if (!ref && (area === 'main' || area === 'bottom')) {
const tabBar = this.getTabBarFor(area);
if (tabBar) {
const last = tabBar.titles[tabBar.titles.length - 1];
if (last) {
ref = last.owner;
}
}
}
return super.addWidget(widget, { ...options, ref });
} }
}
async saveAll(): Promise<void> { async addWidget(
if ( widget: Widget,
this.connectionStatusService.currentStatus === options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
ConnectionStatus.OFFLINE ): Promise<void> {
) { // By default, Theia open a widget **next** to the currently active in the target area.
this.messageService.error( // Instead of this logic, we want to open the new widget after the last of the target area.
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.' if (!widget.id) {
); console.error(
return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803 'Widgets added to the application shell must have a unique id property.'
} );
await super.saveAll(); return;
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
options
);
} }
let ref: Widget | undefined = options.ref;
const area: TheiaApplicationShell.Area = options.area || 'main';
if (!ref && (area === 'main' || area === 'bottom')) {
const tabBar = this.getTabBarFor(area);
if (tabBar) {
const last = tabBar.titles[tabBar.titles.length - 1];
if (last) {
ref = last.owner;
}
}
}
return super.addWidget(widget, { ...options, ref });
}
async saveAll(): Promise<void> {
if (
this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE
) {
this.messageService.error(
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.'
);
return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803
}
await super.saveAll();
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
options
);
}
} }

View File

@@ -1,26 +1,26 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { import {
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
MenuBarWidget, MenuBarWidget,
} from '@theia/core/lib/browser/menu/browser-menu-plugin'; } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager'; import { MainMenuManager } from '../../../common/main-menu-manager';
@injectable() @injectable()
export class BrowserMainMenuFactory export class BrowserMainMenuFactory
extends TheiaBrowserMainMenuFactory extends TheiaBrowserMainMenuFactory
implements MainMenuManager implements MainMenuManager
{ {
protected menuBar: MenuBarWidget | undefined; protected menuBar: MenuBarWidget | undefined;
createMenuBar(): MenuBarWidget { createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar(); this.menuBar = super.createMenuBar();
return this.menuBar; return this.menuBar;
} }
update() { update() {
if (this.menuBar) { if (this.menuBar) {
this.menuBar.clearMenus(); this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar); this.fillMenuBar(this.menuBar);
}
} }
}
} }

View File

@@ -1,18 +1,18 @@
import '../../../../src/browser/style/browser-menu.css'; import '../../../../src/browser/style/browser-menu.css';
import { ContainerModule } from 'inversify'; import { ContainerModule } from 'inversify';
import { import {
BrowserMenuBarContribution, BrowserMenuBarContribution,
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
} from '@theia/core/lib/browser/menu/browser-menu-plugin'; } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager'; import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoMenuContribution } from './browser-menu-plugin'; import { ArduinoMenuContribution } from './browser-menu-plugin';
import { BrowserMainMenuFactory } from './browser-main-menu-factory'; import { BrowserMainMenuFactory } from './browser-main-menu-factory';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BrowserMainMenuFactory).toSelf().inSingletonScope(); bind(BrowserMainMenuFactory).toSelf().inSingletonScope();
bind(MainMenuManager).toService(BrowserMainMenuFactory); bind(MainMenuManager).toService(BrowserMainMenuFactory);
rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory); rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory);
rebind(BrowserMenuBarContribution) rebind(BrowserMenuBarContribution)
.to(ArduinoMenuContribution) .to(ArduinoMenuContribution)
.inSingletonScope(); .inSingletonScope();
}); });

View File

@@ -4,8 +4,8 @@ import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser
@injectable() @injectable()
export class ArduinoMenuContribution extends BrowserMenuBarContribution { export class ArduinoMenuContribution extends BrowserMenuBarContribution {
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
const menu = this.factory.createMenuBar(); const menu = this.factory.createMenuBar();
app.shell.addWidget(menu, { area: 'top' }); app.shell.addWidget(menu, { area: 'top' });
} }
} }

View File

@@ -1,34 +1,35 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CommonFrontendContribution as TheiaCommonFrontendContribution, CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; import {
CommonFrontendContribution as TheiaCommonFrontendContribution,
CommonCommands,
} from '@theia/core/lib/browser/common-frontend-contribution';
@injectable() @injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution { export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
registerMenus(registry: MenuModelRegistry): void {
registerMenus(registry: MenuModelRegistry): void { super.registerMenus(registry);
super.registerMenus(registry); for (const command of [
for (const command of [ CommonCommands.SAVE,
CommonCommands.SAVE, CommonCommands.SAVE_ALL,
CommonCommands.SAVE_ALL, CommonCommands.CUT,
CommonCommands.CUT, CommonCommands.COPY,
CommonCommands.COPY, CommonCommands.PASTE,
CommonCommands.PASTE, CommonCommands.COPY_PATH,
CommonCommands.COPY_PATH, CommonCommands.FIND,
CommonCommands.FIND, CommonCommands.REPLACE,
CommonCommands.REPLACE, CommonCommands.AUTO_SAVE,
CommonCommands.AUTO_SAVE, CommonCommands.OPEN_PREFERENCES,
CommonCommands.OPEN_PREFERENCES, CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_ICON_THEME, CommonCommands.SELECT_COLOR_THEME,
CommonCommands.SELECT_COLOR_THEME, CommonCommands.ABOUT_COMMAND,
CommonCommands.ABOUT_COMMAND, CommonCommands.CLOSE_TAB,
CommonCommands.CLOSE_TAB, CommonCommands.CLOSE_OTHER_TABS,
CommonCommands.CLOSE_OTHER_TABS, CommonCommands.CLOSE_ALL_TABS,
CommonCommands.CLOSE_ALL_TABS, CommonCommands.COLLAPSE_PANEL,
CommonCommands.COLLAPSE_PANEL, CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877 ]) {
]) { registry.unregisterMenuAction(command);
registry.unregisterMenuAction(command);
}
} }
}
} }

View File

@@ -2,83 +2,81 @@ import { inject, injectable, postConstruct } from 'inversify';
import { Disposable } from '@theia/core/lib/common/disposable'; import { Disposable } from '@theia/core/lib/common/disposable';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar'; import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution, ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus, ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service'; } from '@theia/core/lib/browser/connection-status-service';
import { ArduinoDaemon } from '../../../common/protocol'; import { ArduinoDaemon } from '../../../common/protocol';
import { NotificationCenter } from '../../notification-center'; import { NotificationCenter } from '../../notification-center';
@injectable() @injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService { export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(ArduinoDaemon) @inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon; protected readonly daemon: ArduinoDaemon;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected isRunning = false; protected isRunning = false;
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
this.schedulePing(); this.schedulePing();
try { try {
this.isRunning = await this.daemon.isRunning(); this.isRunning = await this.daemon.isRunning();
} catch {} } catch {}
this.notificationCenter.onDaemonStarted(() => (this.isRunning = true)); this.notificationCenter.onDaemonStarted(() => (this.isRunning = true));
this.notificationCenter.onDaemonStopped(() => (this.isRunning = false)); this.notificationCenter.onDaemonStopped(() => (this.isRunning = false));
this.wsConnectionProvider.onIncomingMessageActivity(() => { this.wsConnectionProvider.onIncomingMessageActivity(() => {
this.updateStatus(this.isRunning); this.updateStatus(this.isRunning);
this.schedulePing(); this.schedulePing();
}); });
} }
} }
@injectable() @injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution { export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(ArduinoDaemon) @inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon; protected readonly daemon: ArduinoDaemon;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected isRunning = false; protected isRunning = false;
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
try { try {
this.isRunning = await this.daemon.isRunning(); this.isRunning = await this.daemon.isRunning();
} catch {} } catch {}
this.notificationCenter.onDaemonStarted(() => (this.isRunning = true)); this.notificationCenter.onDaemonStarted(() => (this.isRunning = true));
this.notificationCenter.onDaemonStopped(() => (this.isRunning = false)); this.notificationCenter.onDaemonStopped(() => (this.isRunning = false));
}
protected onStateChange(state: ConnectionStatus): void {
if (!this.isRunning && state === ConnectionStatus.ONLINE) {
return;
} }
super.onStateChange(state);
}
protected onStateChange(state: ConnectionStatus): void { protected handleOffline(): void {
if (!this.isRunning && state === ConnectionStatus.ONLINE) { this.statusBar.setElement('connection-status', {
return; alignment: StatusBarAlignment.LEFT,
} text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline',
super.onStateChange(state); tooltip: this.isRunning
} ? 'Cannot connect to the backend.'
: 'Cannot connect to the CLI daemon.',
protected handleOffline(): void { priority: 5000,
this.statusBar.setElement('connection-status', { });
alignment: StatusBarAlignment.LEFT, this.toDisposeOnOnline.push(
text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline', Disposable.create(() => this.statusBar.removeElement('connection-status'))
tooltip: this.isRunning );
? 'Cannot connect to the backend.' document.body.classList.add('theia-mod-offline');
: 'Cannot connect to the CLI daemon.', this.toDisposeOnOnline.push(
priority: 5000, Disposable.create(() =>
}); document.body.classList.remove('theia-mod-offline')
this.toDisposeOnOnline.push( )
Disposable.create(() => );
this.statusBar.removeElement('connection-status') }
)
);
document.body.classList.add('theia-mod-offline');
this.toDisposeOnOnline.push(
Disposable.create(() =>
document.body.classList.remove('theia-mod-offline')
)
);
}
} }

View File

@@ -8,32 +8,30 @@ import { ArduinoCommands } from '../../arduino-commands';
@injectable() @injectable()
export class FrontendApplication extends TheiaFrontendApplication { export class FrontendApplication extends TheiaFrontendApplication {
@inject(FileService) @inject(FileService)
protected readonly fileService: FileService; protected readonly fileService: FileService;
@inject(WorkspaceService) @inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService; protected readonly workspaceService: WorkspaceService;
@inject(CommandService) @inject(CommandService)
protected readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
protected async initializeLayout(): Promise<void> { protected async initializeLayout(): Promise<void> {
await super.initializeLayout(); await super.initializeLayout();
const roots = await this.workspaceService.roots; const roots = await this.workspaceService.roots;
for (const root of roots) { for (const root of roots) {
const exists = await this.fileService.exists(root.resource); const exists = await this.fileService.exists(root.resource);
if (exists) { if (exists) {
this.sketchesService.markAsRecentlyOpened( this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
root.resource.toString() await this.commandService.executeCommand(
); // no await, will get the notification later and rebuild the menu ArduinoCommands.OPEN_SKETCH_FILES.id,
await this.commandService.executeCommand( root.resource
ArduinoCommands.OPEN_SKETCH_FILES.id, );
root.resource }
);
}
}
} }
}
} }

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