Compare commits

..

46 Commits

Author SHA1 Message Date
Akos Kitta
4907ef2a47 Prepared 2.0.0-rc9.3.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-05 11:17:38 +02:00
github-actions[bot]
9ae3402631 Updated translation files 2022-09-05 10:22:13 +02:00
Akos Kitta
d0dfc656e6 Improved the scrolling UX in list widgets
- Fixed scrollbar does not reach end of list widget.
 - Estimated row heights to provide better scroll UX.
 - Last item's `<select>` must be visible.

Closes #1380
Closes #1381
Closes #1387

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-05 10:20:05 +02:00
Akos Kitta
df3a34eec6 Coerce a semver when calculating updatables.
Closes #1390

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-05 10:00:29 +02:00
Akos Kitta
20cc34ca9d Use CLI 0.27.0-rc.1.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-02 16:24:48 +02:00
Akos Kitta
1b7f86b231 Install the Arduino_BuiltIn to built-in location
Closes #1055.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-02 16:24:48 +02:00
Akos Kitta
0d545bea0e Show ports if has recognized board attached to it.
Closes #1365

 - Ref: 79ea0fa9a6
 - Ref: 74bfdc4c56

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-01 09:08:20 +02:00
Akos Kitta
204d71b2dd Fixed highlighting of non-unicode chars in Output
Closes #1210

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-31 21:16:07 +02:00
Akos Kitta
5cb9166c83 Implemented filter and update all for libs/boards.
Closes #177
Closes #1188

Co-authored-by: Francesco Spissu <f.spissu@arduino.cc>
Co-authored-by: Per Tillisch <p.tillisch@arduino.cc>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-31 10:07:27 +02:00
github-actions[bot]
7828cc11ac Updated translation files (#1305)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-08-31 09:50:01 +02:00
Akos Kitta
34a7fdb733 Pinned 63f1e18 CLI.
Ref: 63f1e1855a
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 18:35:11 +02:00
Akos Kitta
7c361cf2d1 Can close non-root sketch file editors.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
8beade0867 Fixed sketch content changes when renaming a file.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
3afc2d7e4b Fixed dirty indicator of uncloseable widgets.
Closes #1034.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
d40401437a removed space from discovery json log.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
10ac7fd50a Removed File > Close Editor.
Closes arduino/arduino-ide#660

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
07962e81d4 Moved uncloseable widget tracking to manager.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
785775327b Updated translations.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
80dfa5b7dd Restored logic to close current closable widget
and then the window.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
40425d49e0 Unified the sketch close and the app quit logic.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
0c87fa9877 Update currentSketch when files change.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
5b79320302 do not try to restore temp sketches.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
1da2dfc349 No save dialog prompt if closing untouched sketch.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Akos Kitta
d7bbfc515d init
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-26 14:18:39 +02:00
Francesco Spissu
0c22884729 Error message if upload is not possible (#1353) 2022-08-26 11:24:03 +02:00
Akos Kitta
fc9107c084 Use addressLabel in the UI.
- for the boards dropdown, and
 - for the `Tools` > `Port` menu.

Closes #1331

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-25 11:49:50 +02:00
Akos Kitta
474d5e5975 Added a workaround for missing port#properties.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-25 11:49:50 +02:00
Akos Kitta
f7f644cf36 Use port properties from the discovery.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Closes #740
2022-08-25 11:49:50 +02:00
Dave Simpson
b5f9aa0f15 change hard coded max and min (#1345) 2022-08-24 12:32:42 +02:00
Dave Simpson
cc5cf3b165 fix 180: prevent erroneous "auto-reconnect"(s) in board selector (#1328)
* ensure auto-select doesn't select wrong port

prevent auto-select of unattached boards Co-authored-by: Francesco Spissu <francescospissu@users.noreply.github.com>

* clean up

* add "uploadInProgress" prop to boards change event

* remove unused methods and deps

* [WIP]: leverage upload event to derived new port

* remove timeout

* refine port selection logic

* clean up naming

* refine port selection logic & add delayed clean up

* renaming & refactoring

* method syntax & remove unnecessary methods
2022-08-24 12:31:51 +02:00
per1234
125bd64c91 Install Arduino CLI build dependencies in all dependent workflows
Arduino CLI is a tool dependency of Arduino IDE. For this reason, the necessary Arduino CLI build is acquired whenever
running the `yarn` command in the repository.

The way the Arduino CLI build is acquired depends on the type of version specified as dependency in the
`arduino.cli.version` field of the arduino-ide-extension package metadata:

- Release/nightly: download pre-built standard distribution
- Git ref: build from source

This means that, in the latter case, all build dependencies of Arduino CLI must be present. While the Go module
dependencies are automatically installed during the build, the build tool dependencies must be installed in advance:

- Go programming language
- Task task runner

Arduino IDE's infrastructure was recently changed to use the Task tool to build Arduino CLI in the supported manner. A
step to install Task was not added to some of the workflows that run `yarn`, which caused them to fail when a
non-release version of Arduino CLI was used as a dependency:

arduino-ide-extension: >>> Building the CLI...
arduino-ide-extension: /bin/sh: 1: task: not found
arduino-ide-extension: error Command failed with exit code 1.

A step for the missing tool dependency is hereby added to those workflows.

The lack of an explicit installation of the other dependency, Go did not result in an error because Go is pre-installed
on the GitHub Actions runner. However, the installed version may not match the version Arduino CLI is intended to be
built with and validated for, and the version provided by the runner may change at any time. For this reason, it will be
safest to explicitly set up the appropriate version of Go in the workflows.
2022-08-24 01:11:21 -07:00
per1234
ca47e8a09a Fix inconsistency of input field placeholder text capitalization
The board search input field of the "Select Other Board and Port" dialog uses placeholder text to explain the usage of
the field to the user.

All other placeholder text in the IDE's UI uses sentence case. This specific placeholder was the exception, using
unpleasant caps lock instead.

The inconsistency is resolved by changing the placeholder text to the standard sentence case.
2022-08-22 01:06:18 -07:00
per1234
52804a5b52 Add missing i18n for UI strings
The text of the Arduino IDE user interface has been localized to 12 languages.

Before localization can be accomplished, internationalization must be done in the application's code base:

- Set up infrastructure to export localization data
- Pass all target strings to that infrastructure

While the first of these tasks is completed, the second was not completed for several strings which are part of the user
interface.

Those outstanding strings are hereby internationalized and will be made available for localization.
2022-08-22 01:06:18 -07:00
per1234
3ec62642dd Fix typos in log messages
Several of the log messages contained minor typos.
2022-08-21 05:45:49 -07:00
per1234
1281ad1932 Use more relevant page for "Help > Environment" menu item target
Selecting "Help > Environment" from the Arduino IDE menus opens a page containing usage information for the Arduino IDE
application in the browser.

Previously, the URL used was the same as that of in Arduino IDE 1.x:

https://www.arduino.cc/en/Guide/Environment

The documentation from that page was written for Arduino IDE 1.x. Even though the UI of the two versions is aligned for
the most part, some advancements made for the 2.x major version series resulted in some differences. This means that
documentation targeted at Arduino IDE 1.x is not always applicable to Arduino IDE 2.x.

Fortunately, documentation is now available for each major version series of the IDE. So resolution is only a matter of
pointing the menu item at the correct URL.
2022-08-21 05:45:17 -07:00
Alberto Iannaccone
de32bddc20 Fix dialogs UI scalability (#1311)
* make dialogs scroll when scaling up the UI

* add unit of measure to settings step input

* wrap settings dialog items when scaling up the UI

* fix dialogs width when scaling up the UI

* rework board config UI to make it scale up better

* refactor ide updater dialog: move buttons outside the dialog content

* refactor ide updater dialog: clean-up code and rename events

* fix board config dialog title case and and remove double ellipsis
2022-08-18 16:42:16 +02:00
Akos Kitta
79ea0fa9a6 Show all network and serial ports.
Otherwise, unrecognized network boards are ignored
by IDE2.

Closes #1327

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-18 11:03:27 +02:00
Akos Kitta
683219dc1c Fixed typos.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-18 11:03:27 +02:00
Akos Kitta
d674ab9b73 Handle missing core when getting board user fields
Closes #1142

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-18 10:56:03 +02:00
Alberto Iannaccone
5be1f9d7fe change naming of nightly and snapshot builds (#1326)
replace `-` with `.` to make auto-update work correctly
2022-08-17 17:13:25 +02:00
per1234
9e2b73a045 Use unmodified official ClangFormat configuration as base formatter configuration (#1324)
The Arduino IDE's "Auto Format" feature is configured to produce the standard Arduino sketch formatting style by
default.

The Arduino IDE editor's default settings are compliant with that style. However, the user may adjust the editor
settings. In this case, the Arduino IDE automatically adjusts the Auto Format configuration to align with the user's
preferences.

The formatter configuration is consumed by several other projects in addition to the Arduino IDE. For this reason, the
configuration is hosted and maintained in a centralized location, from which it is pulled by all projects that use it.

Previously, the adjustment of the Arduino IDE formatter configuration according to the editor settings was integrated
into the configuration object itself. This meant that the standardized configuration had to be modified each time it was
pulled in to sync from the upstream source.

Moving the base formatter configuration object to a dedicated file, separated from the handling and adjustment code
allows syncs to be done by simply replacing the existing configuration file with the one automatically generated by the
CI system of the repository where the source configuration is hosted.
2022-08-16 08:09:39 -07:00
per1234
75e00c2bae Document clangd update procedure
Arduino IDE has dependencies on the clangd C++ language server and ClangFormat code formatter tools. These are updated
periodically to benefit from the ongoing development on those projects.

The update procedure requires operations in three different repositories:

- Generate builds in arduino/clang-static-binaries
- Validate and update formatter configuration in arduino/tooling-project-assets
- Update metadata in arduino/arduino-ide

Previously, this was undocumented and the procedure existed only in the form of "institutional memory".

The procedure is now fully documented in the readme of arduino-ide-extension.
2022-08-15 08:42:31 -07:00
Akos Kitta
989300f25d Close core error notification on subsequent action
Closes #1154

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-15 16:55:17 +02:00
Akos Kitta
5226636fed Link compiler errors to editor.
Closes #118

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-15 16:55:17 +02:00
Akos Kitta
8b3f3c69fc Use the refactored CLI in IDE2.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-15 09:19:57 +02:00
Akos Kitta
a39ab47e70 Use Task to build pinned CLI for IDE2.
Closes #1313

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-08-11 09:28:50 +02:00
146 changed files with 6024 additions and 2048 deletions

View File

@@ -28,6 +28,8 @@ on:
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
JOB_TRANSFER_ARTIFACT: build-artifacts
CHANGELOG_ARTIFACTS: changelog
@@ -66,6 +68,17 @@ jobs:
with:
python-version: '3.x'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Package
shell: bash
env:

View File

@@ -1,5 +1,9 @@
name: Check Internationalization
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
on:
push:
@@ -31,6 +35,17 @@ jobs:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Taskfile
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -1,5 +1,9 @@
name: i18n-nightly-push
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
on:
schedule:
# run every day at 1AM
@@ -18,6 +22,17 @@ jobs:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -1,5 +1,9 @@
name: i18n-weekly-pull
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
on:
schedule:
# run every monday at 2AM
@@ -18,6 +22,17 @@ jobs:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -7,6 +7,8 @@ on:
workflow_dispatch:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
NODE_VERSION: 14.x
jobs:
@@ -22,6 +24,17 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
version: 3.x
- name: Install dependencies
run: yarn

View File

@@ -62,6 +62,15 @@ The Config Service knows about your system, like for example the default sketch
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
`yarn --cwd arduino-ide-extension generate-protocol`
### Update **clangd** and **ClangFormat**
The [**clangd** C++ language server](https://clangd.llvm.org/) and the [**ClangFormat** code formatter](https://clang.llvm.org/docs/ClangFormat.html) tool dependencies are managed in parallel. Updating them to a different version is done by the following procedure:
1. If the target version is not already [available from the `arduino/clang-static-binaries` repository](https://github.com/arduino/clang-static-binaries/releases), submit [an issue there](https://github.com/arduino/clang-static-binaries/issues) requesting a build and wait for that to be completed.
1. Validate the **ClangFormat** configuration for the target version by following the instructions [**here**](https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration#clangformat-version-updates)
1. Submit a pull request in the `arduino/arduino-ide` repository to update the version in the `arduino.clangd.version` key of [`package.json`](package.json).
1. Submit a pull request in [the `arduino/tooling-project-assets` repository](https://github.com/arduino/tooling-project-assets) to update the version in the `vars.DEFAULT_CLANG_FORMAT_VERSION` field of [`Taskfile.yml`](https://github.com/arduino/tooling-project-assets/blob/main/Taskfile.yml).
### Customize Icons
ArduinoIde uses a customized version of FontAwesome.
In order to update/replace icons follow the following steps:

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-rc9.2",
"version": "2.0.0-rc9.3",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -160,7 +160,7 @@
],
"arduino": {
"cli": {
"version": "0.26.0-rc.1"
"version": "0.27.0-rc.1"
},
"fwuploader": {
"version": "2.2.0"

View File

@@ -6,7 +6,7 @@
const semver = require('semver');
const moment = require('moment');
const downloader = require('./downloader');
const { goBuildFromGit } = require('./utils');
const { taskBuildFromGit } = require('./utils');
const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json'));
@@ -82,6 +82,6 @@
shell.exit(1);
}
} else {
goBuildFromGit(version, destinationPath, 'CLI');
taskBuildFromGit(version, destinationPath, 'CLI');
}
})();

View File

@@ -1,3 +1,14 @@
/**
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
*
* @param version {object} the version object.
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
*/
exports.taskBuildFromGit = (version, destinationPath, taskName) => {
return buildFromGit('task', version, destinationPath, taskName);
};
/**
* Clones something from GitHub and builds it with `Golang`.
*
@@ -6,6 +17,13 @@
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
*/
exports.goBuildFromGit = (version, destinationPath, taskName) => {
return buildFromGit('go', version, destinationPath, taskName);
};
/**
* The `command` is either `go` or `task`.
*/
function buildFromGit(command, version, destinationPath, taskName) {
const fs = require('fs');
const path = require('path');
const temp = require('temp');
@@ -62,7 +80,7 @@ exports.goBuildFromGit = (version, destinationPath, taskName) => {
}
shell.echo(`>>> Building the ${taskName}...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
if (shell.exec(`${command} build`, { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Done ${taskName} build.`);
@@ -89,4 +107,4 @@ exports.goBuildFromGit = (version, destinationPath, taskName) => {
shell.exit(1);
}
shell.echo(`>>> Verified ${taskName}.`);
};
}

View File

@@ -5,17 +5,14 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { SketchesService } from '../common/protocol';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
} from '@theia/core';
import {
Dialog,
FrontendApplication,
FrontendApplicationContribution,
OnWillStopAction,
} from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
@@ -34,14 +31,9 @@ import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../common/protocol/sketches-service-client-impl';
import { ArduinoPreferences } from './arduino-preferences';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
@@ -63,18 +55,12 @@ export class ArduinoFrontendContribution
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(SketchesService)
private readonly sketchService: SketchesService;
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
private readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@@ -91,7 +77,7 @@ export class ArduinoFrontendContribution
}
}
async onStart(app: FrontendApplication): Promise<void> {
onStart(app: FrontendApplication): void {
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
@@ -303,58 +289,4 @@ export class ArduinoFrontendContribution
}
);
}
// TODO: should be handled by `Close` contribution. https://github.com/arduino/arduino-ide/issues/1016
onWillStop(): OnWillStopAction {
return {
reason: 'temp-sketch',
action: () => {
return this.showTempSketchDialog();
},
};
}
private async showTempSketchDialog(): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return true;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp) {
return true;
}
const messageBoxResult = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize(
'arduino/sketch/saveTempSketch',
'Save your sketch to open it again later.'
),
title: nls.localize(
'theia/core/quitTitle',
'Are you sure you want to quit?'
),
type: 'question',
buttons: [
Dialog.CANCEL,
nls.localizeByDefault('Save As...'),
nls.localizeByDefault("Don't Save"),
],
}
);
const result = messageBoxResult.response;
if (result === 2) {
return true;
} else if (result === 1) {
return !!(await this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: false,
wipeOriginal: true,
}
));
}
return false;
}
}

View File

@@ -141,8 +141,6 @@ import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handl
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
import { OutputWidget } from './theia/output/output-widget';
import { BurnBootloader } from './contributions/burn-bootloader';
import {
ExamplesServicePath,
@@ -215,7 +213,10 @@ import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import {
MonacoEditorFactory,
MonacoEditorProvider as TheiaMonacoEditorProvider,
} from '@theia/monaco/lib/browser/monaco-editor-provider';
import { StorageWrapper } from './storage-wrapper';
import { NotificationManager } from './theia/messages/notifications-manager';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
@@ -314,13 +315,25 @@ import { FirstStartupInstaller } from './contributions/first-startup-installer';
import { OpenSketchFiles } from './contributions/open-sketch-files';
import { InoLanguage } from './contributions/ino-language';
import { SelectedBoard } from './contributions/selected-board';
import { CheckForUpdates } from './contributions/check-for-updates';
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
import { OpenBoardsConfig } from './contributions/open-boards-config';
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
import { MonacoThemeServiceIsReady } from './utils/window';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { StatusBarImpl } from './theia/core/status-bar';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
import { EditorMenuContribution } from './theia/editor/editor-file';
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
import { PreferencesEditorWidget } from './theia/preferences/preference-editor-widget';
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
import {
BoardsFilterRenderer,
LibraryFilterRenderer,
} from './widgets/component-list/filter-renderer';
import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
@@ -362,6 +375,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Renderer for both the library and the core widgets.
bind(ListItemRenderer).toSelf().inSingletonScope();
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
// Library service
bind(LibraryService)
@@ -453,7 +468,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: nls.localize('arduino/common/selectBoard', 'Select Board'),
title: nls.localize(
'arduino/board/boardConfigDialogTitle',
'Select Other Board and Port'
),
});
// Core service
@@ -571,8 +589,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
return container.get(TabBarToolbar);
}
);
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
@@ -637,6 +653,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(WindowContribution).toSelf().inSingletonScope();
rebind(TheiaWindowContribution).toService(WindowContribution);
// To remove `File` > `Close Editor`.
bind(EditorMenuContribution).toSelf().inSingletonScope();
rebind(TheiaEditorMenuContribution).toService(EditorMenuContribution);
// To disable the highlighting of non-unicode characters in the _Output_ view
bind(OutputEditorFactory).toSelf().inSingletonScope();
// Rebind to `TheiaOutputEditorFactory` when https://github.com/eclipse-theia/theia/pull/11615 is available.
rebind(MonacoEditorFactory).toService(OutputEditorFactory);
bind(ArduinoDaemon)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -728,9 +753,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenSketchFiles);
Contribution.configure(bind, InoLanguage);
Contribution.configure(bind, SelectedBoard);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, CheckForIDEUpdates);
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
@@ -836,6 +862,18 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DockPanelRenderer).toSelf();
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
// Avoid running the "reset scroll" interval tasks until the preference editor opens.
rebind(PreferencesWidget)
.toDynamicValue(({ container }) => {
const child = createPreferencesWidgetContainer(container);
child.bind(PreferencesEditorWidget).toSelf().inSingletonScope();
child
.rebind(TheiaPreferencesEditorWidget)
.toService(PreferencesEditorWidget);
return child.get(PreferencesWidget);
})
.inSingletonScope();
// Preferences
bindArduinoPreferences(bind);

View File

@@ -241,6 +241,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.checkForUpdates': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/checkForUpdate',
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
),
default: true,
},
},
};
@@ -270,6 +278,7 @@ export interface ArduinoConfiguration {
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
'arduino.checkForUpdates': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@@ -12,6 +12,7 @@ import { Installable, ResponseServiceClient } from '../../common/protocol';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { nls } from '@theia/core/lib/common';
import { NotificationCenter } from '../notification-center';
import { InstallManually } from '../../common/nls';
interface AutoInstallPromptAction {
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
@@ -231,19 +232,18 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
candidate: BoardsPackage
): AutoInstallPromptActions {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const manualInstall = nls.localize(
'arduino/board/installManually',
'Install Manually'
);
const actions: AutoInstallPromptActions = [
{
key: manualInstall,
key: InstallManually,
handler: () => {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh(candidate.name.toLocaleLowerCase())
widget.refresh({
query: candidate.name.toLocaleLowerCase(),
type: 'All',
})
);
},
},

View File

@@ -1,4 +1,8 @@
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
@@ -28,7 +32,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogProps)
protected override readonly props: BoardsConfigDialogProps
) {
super(props);
super({ ...props, maxWidth: 500 });
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
@@ -65,14 +69,6 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = nls.localize(
'arduino/board/configDialogTitle',
'Select Other Board & Port'
);
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);

View File

@@ -258,14 +258,14 @@ export class BoardsConfig extends React.Component<
override render(): React.ReactNode {
return (
<div className="body">
<>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</div>
</>
);
}
@@ -306,7 +306,10 @@ export class BoardsConfig extends React.Component<
type="search"
value={query}
className="theia-input"
placeholder="SEARCH BOARD"
placeholder={nls.localize(
'arduino/board/searchBoard',
'Search board'
)}
onChange={this.updateBoards}
ref={this.focusNodeSet}
/>
@@ -334,27 +337,19 @@ export class BoardsConfig extends React.Component<
if (this.state.showAllPorts) {
ports = this.state.knownPorts;
} else {
ports = this.state.knownPorts.filter((port) => {
if (port.protocol === 'serial') {
return true;
}
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of this.availableBoards) {
if (board.port?.address === port.address) {
return true;
}
}
});
ports = this.state.knownPorts.filter(
Port.visiblePorts(this.availableBoards)
);
}
return !ports.length ? (
<div className="loading noselect">No ports discovered</div>
<div className="loading noselect">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
<div className="ports list">
{ports.map((port) => (
<Item<Port>
key={`${port.id}`}
key={`${Port.keyOf(port)}`}
item={port}
label={Port.toString(port)}
selected={Port.sameAs(this.state.selectedPort, port)}

View File

@@ -4,22 +4,24 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import {
BoardSearch,
BoardsPackage,
BoardsService,
} from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common';
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardsPackage> {
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<BoardsPackage>
@inject(BoardsService) service: BoardsService,
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
) {
super({
id: BoardsListWidget.WIDGET_ID,
@@ -30,6 +32,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer,
filterRenderer,
defaultSearchOptions: { query: '', type: 'All' },
});
}

View File

@@ -13,6 +13,7 @@ import {
AttachedBoardsChangeEvent,
BoardWithPackage,
BoardUserField,
AvailablePorts,
} from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils';
@@ -21,6 +22,7 @@ import { StorageWrapper } from '../storage-wrapper';
import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Unknown } from '../../common/nls';
@injectable()
export class BoardsServiceProvider implements FrontendApplicationContribution {
@@ -65,11 +67,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected _availablePorts: Port[] = [];
protected _availableBoards: AvailableBoard[] = [];
private lastBoardsConfigOnUpload: BoardsConfig.Config | undefined;
private lastAvailablePortsOnUpload: Port[] | undefined;
private boardConfigToAutoSelect: BoardsConfig.Config | undefined;
/**
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
* This even also fires, when the boards package was not available for the currently selected board,
* Unlike `onAttachedBoardsChanged` this event fires when the user modifies the selected board in the IDE.\
* This event also fires, when the boards package was not available for the currently selected board,
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
* currently selected board.\
* currently selected board.
*
* This event is also emitted when the board package for the currently selected board was uninstalled.
*/
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
@@ -91,11 +98,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
);
this.appStateService.reachedState('ready').then(async () => {
const [attachedBoards, availablePorts] = await Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts(),
const [state] = await Promise.all([
this.boardsService.getState(),
this.loadState(),
]);
const { boards: attachedBoards, ports: availablePorts } =
AvailablePorts.split(state);
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
@@ -111,6 +119,84 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return this._reconciled.promise;
}
snapshotBoardDiscoveryOnUpload(): void {
this.lastBoardsConfigOnUpload = this._boardsConfig;
this.lastAvailablePortsOnUpload = this._availablePorts;
}
clearBoardDiscoverySnapshot(): void {
this.lastBoardsConfigOnUpload = undefined;
this.lastAvailablePortsOnUpload = undefined;
}
private portToAutoSelectCanBeDerived(): boolean {
return Boolean(
this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload
);
}
attemptPostUploadAutoSelect(): void {
setTimeout(() => {
if (this.portToAutoSelectCanBeDerived()) {
this.attemptAutoSelect({
ports: this._availablePorts,
boards: this._availableBoards,
});
}
}, 2000); // 2 second delay same as IDE 1.8
}
private attemptAutoSelect(
newState: AttachedBoardsChangeEvent['newState']
): void {
this.deriveBoardConfigToAutoSelect(newState);
this.tryReconnect();
}
private deriveBoardConfigToAutoSelect(
newState: AttachedBoardsChangeEvent['newState']
): void {
if (!this.portToAutoSelectCanBeDerived()) {
this.boardConfigToAutoSelect = undefined;
return;
}
const oldPorts = this.lastAvailablePortsOnUpload!;
const { ports: newPorts, boards: newBoards } = newState;
const appearedPorts =
oldPorts.length > 0
? newPorts.filter((newPort: Port) =>
oldPorts.every((oldPort: Port) => !Port.sameAs(newPort, oldPort))
)
: newPorts;
for (const port of appearedPorts) {
const boardOnAppearedPort = newBoards.find((board: Board) =>
Port.sameAs(board.port, port)
);
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload!;
if (
boardOnAppearedPort &&
lastBoardsConfigOnUpload.selectedBoard &&
Board.sameAs(
boardOnAppearedPort,
lastBoardsConfigOnUpload.selectedBoard
)
) {
this.clearBoardDiscoverySnapshot();
this.boardConfigToAutoSelect = {
selectedBoard: boardOnAppearedPort,
selectedPort: port,
};
return;
}
}
}
protected notifyAttachedBoardsChanged(
event: AttachedBoardsChangeEvent
): void {
@@ -119,10 +205,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
this.logger.info(AttachedBoardsChangeEvent.toString(event));
this.logger.info('------------------------------------------');
}
this._attachedBoards = event.newState.boards;
this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.reconcileAvailableBoards().then(() => this.tryReconnect());
this.reconcileAvailableBoards().then(() => {
const { uploadInProgress } = event;
// avoid attempting "auto-selection" while an
// upload is in progress
if (!uploadInProgress) {
this.attemptAutoSelect(event.newState);
}
});
}
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
@@ -238,24 +332,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return true;
}
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of this.availableBoards.filter(
({ state }) => state !== AvailableBoard.State.incomplete
)) {
if (
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
this.latestValidBoardsConfig.selectedPort.protocol ===
board.port?.protocol
) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: board.port,
};
return true;
}
}
if (!this.boardConfigToAutoSelect) return false;
this.boardsConfig = this.boardConfigToAutoSelect;
this.boardConfigToAutoSelect = undefined;
return true;
}
return false;
}
@@ -380,6 +462,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return this._availableBoards;
}
/**
* @deprecated Do not use this API, it will be removed. This is a hack to be able to set the missing port `properties` before an upload.
*
* See: https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236.
*/
// TODO: remove this API and fix the selected board config store/restore correctly.
get availablePorts(): Port[] {
return this._availablePorts.slice();
}
async waitUntilAvailable(
what: Board & { port: Port },
timeout?: number
@@ -436,28 +528,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const currentAvailableBoards = this._availableBoards;
const availableBoards: AvailableBoard[] = [];
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
const availableBoardPorts = availablePorts.filter((port) => {
if (port.protocol === 'serial') {
// We always show all serial ports, even if there
// is no recognized board connected to it
return true;
}
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of attachedBoards) {
if (board.port?.address === port.address) {
return true;
}
}
return false;
});
const availableBoardPorts = availablePorts.filter(
Port.visiblePorts(attachedBoards)
);
for (const boardPort of availableBoardPorts) {
const board = attachedBoards.find(({ port }) =>
Port.sameAs(boardPort, port)
);
// "board" will always be falsey for
// port that was originally mapped
// to unknown board and then selected
// manually by user
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
boardPort
);
@@ -476,12 +559,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
availableBoard = {
...lastSelectedBoard,
state: AvailableBoard.State.guessed,
selected: BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard),
selected:
BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard) &&
Port.sameAs(boardPort, boardsConfig.selectedPort), // to avoid double selection
port: boardPort,
};
} else {
availableBoard = {
name: nls.localize('arduino/common/unknown', 'Unknown'),
name: Unknown,
port: boardPort,
state: AvailableBoard.State.incomplete,
};
@@ -491,7 +576,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
if (
boardsConfig.selectedBoard &&
!availableBoards.some(({ selected }) => selected)
availableBoards.every(({ selected }) => !selected)
) {
// If the selected board has the same port of an unknown board
// that is already in availableBoards we might get a duplicate port.

View File

@@ -138,7 +138,7 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
{boardLabel}
</div>
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
{port.address}
{port.addressLabel}
</div>
</div>
{selected ? <div className="fa fa-check" /> : ''}

View File

@@ -1,10 +1,16 @@
import { injectable } from '@theia/core/shared/inversify';
import { BoardsListWidget } from './boards-list-widget';
import { BoardsPackage } from '../../common/protocol/boards-service';
import type {
BoardSearch,
BoardsPackage,
} from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
BoardsPackage,
BoardSearch
> {
constructor() {
super({
widgetId: BoardsListWidget.WIDGET_ID,

View File

@@ -288,7 +288,7 @@ PID: ${PID}`;
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
const [port, boards] = ports[portID];
let label = `${port.address}`;
let label = `${port.addressLabel}`;
if (boards.length) {
const boardsList = boards.map((board) => board.name).join(', ');
label = `${label} (${boardsList})`;
@@ -331,7 +331,7 @@ PID: ${PID}`;
}
};
const grouped = AvailablePorts.byProtocol(availablePorts);
const grouped = AvailablePorts.groupByProtocol(availablePorts);
let protocolOrder = 100;
// We first show serial and network ports, then all the rest
['serial', 'network'].forEach((protocol) => {

View File

@@ -29,6 +29,7 @@ export class BurnBootloader extends CoreServiceContribution {
}
private async burnBootloader(): Promise<void> {
this.clearVisibleNotification();
const options = await this.options();
try {
await this.doWithProgress({

View File

@@ -0,0 +1,68 @@
import { nls } from '@theia/core/lib/common/nls';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
IDEUpdater,
SKIP_IDE_VERSION,
} from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
import { Contribution } from './contribution';
@injectable()
export class CheckForIDEUpdates extends Contribution {
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
override onStart(): void {
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (newValue !== oldValue) {
switch (preferenceName) {
case 'arduino.ide.updateChannel':
case 'arduino.ide.updateBaseUrl':
this.updater.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
);
}
}
}
);
}
override onReady(): void {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (!checkForUpdates) {
return;
}
this.updater
.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
)
.then(() => this.updater.checkForUpdates(true))
.then(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorage.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
})
.catch((e) => {
this.messageService.error(
nls.localize(
'arduino/ide-updater/errorCheckingForUpdates',
'Error while checking for Arduino IDE updates.\n{0}',
e.message
)
);
});
}
}

View File

@@ -1,64 +1,221 @@
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { nls } from '@theia/core/lib/common/nls';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import { InstallManually, Later } from '../../common/nls';
import {
IDEUpdater,
SKIP_IDE_VERSION,
} from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
import { Contribution } from './contribution';
ArduinoComponent,
BoardsPackage,
BoardsService,
LibraryPackage,
LibraryService,
ResponseServiceClient,
Searchable,
} from '../../common/protocol';
import { Installable } from '../../common/protocol/installable';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
import { WindowServiceExt } from '../theia/core/window-service-ext';
import type { ListWidget } from '../widgets/component-list/list-widget';
import { Command, CommandRegistry, Contribution } from './contribution';
const NoUpdates = nls.localize(
'arduino/checkForUpdates/noUpdates',
'There are no recent updates available.'
);
const PromptUpdateBoards = nls.localize(
'arduino/checkForUpdates/promptUpdateBoards',
'Updates are available for some of your boards.'
);
const PromptUpdateLibraries = nls.localize(
'arduino/checkForUpdates/promptUpdateLibraries',
'Updates are available for some of your libraries.'
);
const UpdatingBoards = nls.localize(
'arduino/checkForUpdates/updatingBoards',
'Updating boards...'
);
const UpdatingLibraries = nls.localize(
'arduino/checkForUpdates/updatingLibraries',
'Updating libraries...'
);
const InstallAll = nls.localize(
'arduino/checkForUpdates/installAll',
'Install All'
);
interface Task<T extends ArduinoComponent> {
readonly run: () => Promise<void>;
readonly item: T;
}
const Updatable = { type: 'Updatable' } as const;
@injectable()
export class CheckForUpdates extends Contribution {
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@inject(WindowServiceExt)
private readonly windowService: WindowServiceExt;
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(LibraryService)
private readonly libraryService: LibraryService;
@inject(BoardsListWidgetFrontendContribution)
private readonly boardsContribution: BoardsListWidgetFrontendContribution;
@inject(LibraryListWidgetFrontendContribution)
private readonly librariesContribution: LibraryListWidgetFrontendContribution;
@inject(IDEUpdaterDialog)
private readonly updaterDialog: IDEUpdaterDialog;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
override onStart(): void {
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (newValue !== oldValue) {
switch (preferenceName) {
case 'arduino.ide.updateChannel':
case 'arduino.ide.updateBaseUrl':
this.updater.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
);
}
}
}
);
override registerCommands(register: CommandRegistry): void {
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
execute: () => this.checkForUpdates(false),
});
}
override onReady(): void {
this.updater
.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
)
.then(() => this.updater.checkForUpdates(true))
.then(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorage.getData<string>(
SKIP_IDE_VERSION
);
if (versionToSkip === updateInfo.version) return;
this.updaterDialog.open(updateInfo);
})
.catch((e) => {
this.messageService.error(
nls.localize(
'arduino/ide-updater/errorCheckingForUpdates',
'Error while checking for Arduino IDE updates.\n{0}',
e.message
)
);
override async onReady(): Promise<void> {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (checkForUpdates) {
this.windowService.isFirstWindow().then((firstWindow) => {
if (firstWindow) {
this.checkForUpdates();
}
});
}
}
private async checkForUpdates(silent = true) {
const [boardsPackages, libraryPackages] = await Promise.all([
this.boardsService.search(Updatable),
this.libraryService.search(Updatable),
]);
this.promptUpdateBoards(boardsPackages);
this.promptUpdateLibraries(libraryPackages);
if (!libraryPackages.length && !boardsPackages.length && !silent) {
this.messageService.info(NoUpdates);
}
}
private promptUpdateBoards(items: BoardsPackage[]): void {
this.prompt({
items,
installable: this.boardsService,
viewContribution: this.boardsContribution,
viewSearchOptions: { query: '', ...Updatable },
promptMessage: PromptUpdateBoards,
updatingMessage: UpdatingBoards,
});
}
private promptUpdateLibraries(items: LibraryPackage[]): void {
this.prompt({
items,
installable: this.libraryService,
viewContribution: this.librariesContribution,
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
promptMessage: PromptUpdateLibraries,
updatingMessage: UpdatingLibraries,
});
}
private prompt<
T extends ArduinoComponent,
S extends Searchable.Options
>(options: {
items: T[];
installable: Installable<T>;
viewContribution: AbstractViewContribution<ListWidget<T, S>>;
viewSearchOptions: S;
promptMessage: string;
updatingMessage: string;
}): void {
const {
items,
installable,
viewContribution,
promptMessage: message,
viewSearchOptions,
updatingMessage,
} = options;
if (!items.length) {
return;
}
this.messageService
.info(message, Later, InstallManually, InstallAll)
.then((answer) => {
if (answer === InstallAll) {
const tasks = items.map((item) =>
this.createInstallTask(item, installable)
);
this.executeTasks(updatingMessage, tasks);
} else if (answer === InstallManually) {
viewContribution
.openView({ reveal: true })
.then((widget) => widget.refresh(viewSearchOptions));
}
});
}
private async executeTasks(
message: string,
tasks: Task<ArduinoComponent>[]
): Promise<void> {
if (tasks.length) {
return ExecuteWithProgress.withProgress(
message,
this.messageService,
async (progress) => {
try {
const total = tasks.length;
let count = 0;
for (const { run, item } of tasks) {
try {
await run(); // runs update sequentially. // TODO: is parallel update desired?
} catch (err) {
console.error(err);
this.messageService.error(
`Failed to update ${item.name}. ${err}`
);
} finally {
progress.report({ work: { total, done: ++count } });
}
}
} finally {
progress.cancel();
}
}
);
}
}
private createInstallTask<T extends ArduinoComponent>(
item: T,
installable: Installable<T>
): Task<T> {
const latestVersion = item.availableVersions[0];
return {
item,
run: () =>
Installable.installWithProgress({
installable,
item,
version: latestVersion,
messageService: this.messageService,
responseService: this.responseService,
keepOutput: true,
}),
};
}
}
export namespace CheckForUpdates {
export namespace Commands {
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
{
id: 'arduino-check-for-updates',
label: 'Check for Arduino Updates',
category: 'Arduino',
},
'arduino/checkForUpdates/checkForUpdates'
);
}
}

View File

@@ -1,9 +1,14 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { injectable } from '@theia/core/shared/inversify';
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import type { MaybePromise } from '@theia/core/lib/common/types';
import type {
FrontendApplication,
OnWillStopAction,
} from '@theia/core/lib/browser/frontend-application';
import { nls } from '@theia/core/lib/common/nls';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
SketchContribution,
@@ -11,27 +16,48 @@ import {
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
Sketch,
URI,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './save-as-sketch';
/**
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
*/
@injectable()
export class Close extends SketchContribution {
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
private shell: ApplicationShell | undefined;
protected shell: ApplicationShell;
override onStart(app: FrontendApplication): void {
override onStart(app: FrontendApplication): MaybePromise<void> {
this.shell = app.shell;
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Close.Commands.CLOSE, {
execute: () => remote.getCurrentWindow().close()
execute: () => {
// Close current editor if closeable.
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) {
currentEditor.close();
return;
}
if (this.shell) {
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
}
return remote.getCurrentWindow().close();
},
});
}
@@ -50,6 +76,123 @@ export class Close extends SketchContribution {
});
}
// `FrontendApplicationContribution#onWillStop`
onWillStop(): OnWillStopAction {
return {
reason: 'save-sketch',
action: () => {
return this.showSaveSketchDialog();
},
};
}
/**
* If returns with `true`, IDE2 will close. Otherwise, it won't.
*/
private async showSaveSketchDialog(): Promise<boolean> {
const sketch = await this.isCurrentSketchTemp();
if (!sketch) {
// Normal close workflow: if there are dirty editors prompt the user.
if (!this.shell) {
console.error(
`Could not get the application shell. Something went wrong.`
);
return true;
}
if (this.shell.canSaveAll()) {
const prompt = await this.prompt(false);
switch (prompt) {
case Prompt.DoNotSave:
return true;
case Prompt.Cancel:
return false;
case Prompt.Save: {
await this.shell.saveAll();
return true;
}
default:
throw new Error(`Unexpected prompt: ${prompt}`);
}
}
return true;
}
// If non of the sketch files were ever touched, do not prompt the save dialog. (#1274)
const wereTouched = await Promise.all(
Sketch.uris(sketch).map((uri) => this.wasTouched(uri))
);
if (wereTouched.every((wasTouched) => !Boolean(wasTouched))) {
return true;
}
const prompt = await this.prompt(true);
switch (prompt) {
case Prompt.DoNotSave:
return true;
case Prompt.Cancel:
return false;
case Prompt.Save: {
// If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI.
const result = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: false,
wipeOriginal: true,
markAsRecentlyOpened: true,
}
);
return !!result;
}
default:
throw new Error(`Unexpected prompt: ${prompt}`);
}
}
private async prompt(isTemp: boolean): Promise<Prompt> {
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize(
'arduino/sketch/saveSketch',
'Save your sketch to open it again later.'
),
title: nls.localize(
'theia/core/quitTitle',
'Are you sure you want to quit?'
),
type: 'question',
buttons: [
nls.localizeByDefault("Don't Save"),
Dialog.CANCEL,
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
],
defaultId: 2, // `Save`/`Save As...` button index is the default.
}
);
switch (response) {
case 0:
return Prompt.DoNotSave;
case 1:
return Prompt.Cancel;
case 2:
return Prompt.Save;
default:
throw new Error(`Unexpected response: ${response}`);
}
}
private async isCurrentSketchTemp(): Promise<false | Sketch> {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
const isTemp = await this.sketchService.isTemp(currentSketch);
if (isTemp) {
return currentSketch;
}
}
return false;
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
@@ -59,13 +202,23 @@ export class Close extends SketchContribution {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor.getControl().getModel()?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
if (this.isInteger(versionId) && versionId > 1) {
return true;
}
}
}
return false;
}
private isInteger(arg: unknown): arg is number {
return Number.isInteger(arg);
}
}
enum Prompt {
Save,
DoNotSave,
Cancel,
}
export namespace Close {

View File

@@ -4,11 +4,13 @@ import {
Disposable,
DisposableCollection,
Emitter,
MaybeArray,
MaybePromise,
nls,
notEmpty,
} from '@theia/core';
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
@@ -28,14 +30,15 @@ import * as monaco from '@theia/monaco-editor-core';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
import { OutputUri } from '@theia/output/lib/common/output-uri';
import { CoreError } from '../../common/protocol/core-service';
import { ErrorRevealStrategy } from '../arduino-preferences';
import { InoSelector } from '../ino-selectors';
import { fullRange } from '../utils/monaco';
import { ArduinoOutputSelector, InoSelector } from '../selectors';
import { Contribution } from './contribution';
import { CoreErrorHandler } from './core-error-handler';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
interface ErrorDecoration {
interface ErrorDecorationRef {
/**
* This is the unique ID of the decoration given by `monaco`.
*/
@@ -45,72 +48,89 @@ interface ErrorDecoration {
*/
readonly uri: string;
}
namespace ErrorDecoration {
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
): Promise<monaco.Range | undefined>;
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider: MonacoEditor
): monaco.Range | undefined;
export function rangeOf(
{ id, uri }: ErrorDecoration,
editorProvider:
| ((uri: string) => Promise<MonacoEditor | undefined>)
| MonacoEditor
): MaybePromise<monaco.Range | undefined> {
if (editorProvider instanceof MonacoEditor) {
const control = editorProvider.getControl();
const model = control.getModel();
if (model) {
return control
.getDecorationsInRange(fullRange(model))
?.find(({ id: candidateId }) => id === candidateId)?.range;
}
return undefined;
export namespace ErrorDecorationRef {
export function is(arg: unknown): arg is ErrorDecorationRef {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'uri' in object &&
typeof object['uri'] === 'string' &&
'id' in object &&
typeof object['id'] === 'string'
);
}
return editorProvider(uri).then((editor) => {
if (editor) {
return rangeOf({ id, uri }, editor);
}
return undefined;
});
return false;
}
// export async function rangeOf(
// { id, uri }: ErrorDecoration,
// editorProvider:
// | ((uri: string) => Promise<MonacoEditor | undefined>)
// | MonacoEditor
// ): Promise<monaco.Range | undefined> {
// const editor =
// editorProvider instanceof MonacoEditor
// ? editorProvider
// : await editorProvider(uri);
// if (editor) {
// const control = editor.getControl();
// const model = control.getModel();
// if (model) {
// return control
// .getDecorationsInRange(fullRange(model))
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
// }
// }
// return undefined;
// }
export function sameAs(
left: ErrorDecoration,
right: ErrorDecoration
left: ErrorDecorationRef,
right: ErrorDecorationRef
): boolean {
return left.id === right.id && left.uri === right.uri;
}
}
interface ErrorDecoration extends ErrorDecorationRef {
/**
* The range of the error location the error in the compiler output from the CLI.
*/
readonly rangesInOutput: monaco.Range[];
}
namespace ErrorDecoration {
export function rangeOf(
editorOrModel: MonacoEditor | ITextModel | undefined,
decorations: ErrorDecoration
): monaco.Range | undefined;
export function rangeOf(
editorOrModel: MonacoEditor | ITextModel | undefined,
decorations: ErrorDecoration[]
): (monaco.Range | undefined)[];
export function rangeOf(
editorOrModel: MonacoEditor | ITextModel | undefined,
decorations: ErrorDecoration | ErrorDecoration[]
): MaybePromise<MaybeArray<monaco.Range | undefined>> {
if (editorOrModel) {
const allDecorations = getAllDecorations(editorOrModel);
if (allDecorations) {
if (Array.isArray(decorations)) {
return decorations.map(({ id: decorationId }) =>
findRangeOf(decorationId, allDecorations)
);
} else {
return findRangeOf(decorations.id, allDecorations);
}
}
}
return Array.isArray(decorations)
? decorations.map(() => undefined)
: undefined;
}
function findRangeOf(
decorationId: string,
allDecorations: { id: string; range?: monaco.Range }[]
): monaco.Range | undefined {
return allDecorations.find(
({ id: candidateId }) => candidateId === decorationId
)?.range;
}
function getAllDecorations(
editorOrModel: MonacoEditor | ITextModel
): { id: string; range?: monaco.Range }[] {
if (editorOrModel instanceof MonacoEditor) {
const model = editorOrModel.getControl().getModel();
if (!model) {
return [];
}
return model.getAllDecorations();
}
return editorOrModel.getAllDecorations();
}
}
@injectable()
export class CompilerErrors
extends Contribution
implements monaco.languages.CodeLensProvider
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
{
@inject(EditorManager)
private readonly editorManager: EditorManager;
@@ -119,11 +139,14 @@ export class CompilerErrors
private readonly p2m: ProtocolToMonacoConverter;
@inject(MonacoToProtocolConverter)
private readonly mp2: MonacoToProtocolConverter;
private readonly m2p: MonacoToProtocolConverter;
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
private revealStrategy = ErrorRevealStrategy.Default;
private experimental = false;
private readonly errors: ErrorDecoration[] = [];
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
@@ -131,8 +154,8 @@ export class CompilerErrors
this.currentErrorDidChangEmitter.event;
private readonly toDisposeOnCompilerErrorDidChange =
new DisposableCollection();
private shell: ApplicationShell | undefined;
private revealStrategy = ErrorRevealStrategy.Default;
private currentError: ErrorDecoration | undefined;
private get currentErrorIndex(): number {
const current = this.currentError;
@@ -140,46 +163,75 @@ export class CompilerErrors
return -1;
}
return this.errors.findIndex((error) =>
ErrorDecoration.sameAs(error, current)
ErrorDecorationRef.sameAs(error, current)
);
}
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
monaco.languages.registerCodeLensProvider(InoSelector, this);
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
this.handleCompilerErrorsDidChange(errors)
);
this.onCurrentErrorDidChange(async (error) => {
const range = await ErrorDecoration.rangeOf(error, (uri) =>
this.monacoEditor(uri)
);
if (!range) {
const monacoEditor = await this.monacoEditor(error.uri);
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
if (!monacoRange) {
console.warn(
'compiler-errors',
`Could not find range of decoration: ${error.id}`
);
return;
}
const range = this.m2p.asRange(monacoRange);
const editor = await this.revealLocationInEditor({
uri: error.uri,
range: this.mp2.asRange(range),
range,
});
if (!editor) {
console.warn(
'compiler-errors',
`Failed to mark error ${error.id} as the current one.`
);
} else {
const monacoEditor = this.monacoEditor(editor);
if (monacoEditor) {
monacoEditor.cursor = range.start;
}
}
});
}
override onReady(): MaybePromise<void> {
this.preferences.ready.then(() => {
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.compile.revealRange') {
this.revealStrategy = ErrorRevealStrategy.is(newValue)
? newValue
: ErrorRevealStrategy.Default;
this.experimental = Boolean(
this.preferences['arduino.compile.experimental']
);
const strategy = this.preferences['arduino.compile.revealRange'];
this.revealStrategy = ErrorRevealStrategy.is(strategy)
? strategy
: ErrorRevealStrategy.Default;
this.preferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => {
if (newValue === oldValue) {
return;
}
switch (preferenceName) {
case 'arduino.compile.revealRange': {
this.revealStrategy = ErrorRevealStrategy.is(newValue)
? newValue
: ErrorRevealStrategy.Default;
return;
}
case 'arduino.compile.experimental': {
this.experimental = Boolean(newValue);
this.onDidChangeEmitter.fire(this);
return;
}
}
}
});
);
});
}
@@ -196,9 +248,13 @@ export class CompilerErrors
}
const nextError =
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
this.markAsCurrentError(nextError);
return this.markAsCurrentError(nextError, {
forceReselect: true,
reveal: true,
});
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
isEnabled: () =>
this.experimental && !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
execute: () => {
@@ -212,9 +268,24 @@ export class CompilerErrors
}
const previousError =
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
this.markAsCurrentError(previousError);
return this.markAsCurrentError(previousError, {
forceReselect: true,
reveal: true,
});
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
isEnabled: () =>
this.experimental && !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.MARK_AS_CURRENT, {
execute: (arg: unknown) => {
if (ErrorDecorationRef.is(arg)) {
return this.markAsCurrentError(
{ id: arg.id, uri: new URI(arg.uri).toString() }, // Make sure the URI fragments are encoded. On Windows, `C:` is encoded as `C%3A`.
{ forceReselect: true, reveal: true }
);
}
},
isEnabled: () => !!this.errors.length,
});
}
@@ -229,13 +300,13 @@ export class CompilerErrors
): Promise<monaco.languages.CodeLensList> {
const lenses: monaco.languages.CodeLens[] = [];
if (
this.experimental &&
this.currentError &&
this.currentError.uri === model.uri.toString() &&
this.errors.length > 1
) {
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
this.monacoEditor(uri)
);
const monacoEditor = await this.monacoEditor(model.uri);
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
if (range) {
lenses.push(
{
@@ -268,14 +339,81 @@ export class CompilerErrors
};
}
async provideLinks(
model: monaco.editor.ITextModel,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.ILinksList> {
const links: monaco.languages.ILink[] = [];
if (
model.uri.scheme === OutputUri.SCHEME &&
model.uri.path === '/Arduino'
) {
links.push(
...this.errors
.filter((decoration) => !!decoration.rangesInOutput.length)
.map(({ rangesInOutput, id, uri }) =>
rangesInOutput.map(
(range) =>
<monaco.languages.ILink>{
range,
url: monaco.Uri.parse(`command://`).with({
query: JSON.stringify({ id, uri }),
path: CompilerErrors.Commands.MARK_AS_CURRENT.id,
}),
tooltip: nls.localize(
'arduino/editor/revealError',
'Reveal Error'
),
}
)
)
.reduce((acc, curr) => acc.concat(curr), [])
);
} else {
console.warn('unexpected URI: ' + model.uri.toString());
}
return { links };
}
async resolveLink(
link: monaco.languages.ILink,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.ILink | undefined> {
if (!this.experimental) {
return undefined;
}
const { url } = link;
if (url) {
const candidateUri = new URI(
typeof url === 'string' ? url : url.toString()
);
const candidateId = candidateUri.path.toString();
const error = this.errors.find((error) => error.id === candidateId);
if (error) {
const monacoEditor = await this.monacoEditor(error.uri);
const range = ErrorDecoration.rangeOf(monacoEditor, error);
if (range) {
return {
range,
url: monaco.Uri.parse(error.uri),
};
}
}
}
return undefined;
}
private async handleCompilerErrorsDidChange(
errors: CoreError.ErrorLocation[]
): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose();
const compilerErrorsPerResource = this.groupByResource(
await this.filter(errors)
const groupedErrors = this.groupBy(
errors,
(error: CoreError.ErrorLocation) => error.location.uri
);
const decorations = await this.decorateEditors(compilerErrorsPerResource);
const decorations = await this.decorateEditors(groupedErrors);
this.errors.push(...decorations.errors);
this.toDisposeOnCompilerErrorDidChange.pushAll([
Disposable.create(() => (this.errors.length = 0)),
@@ -283,17 +421,17 @@ export class CompilerErrors
...(await Promise.all([
decorations.dispose,
this.trackEditors(
compilerErrorsPerResource,
groupedErrors,
(editor) =>
editor.editor.onSelectionChanged((selection) =>
editor.onSelectionChanged((selection) =>
this.handleSelectionChange(editor, selection)
),
(editor) =>
editor.onDidDispose(() =>
this.handleEditorDidDispose(editor.editor.uri.toString())
editor.onDispose(() =>
this.handleEditorDidDispose(editor.uri.toString())
),
(editor) =>
editor.editor.onDocumentContentChanged((event) =>
editor.onDocumentContentChanged((event) =>
this.handleDocumentContentChange(editor, event)
)
),
@@ -301,24 +439,13 @@ export class CompilerErrors
]);
const currentError = this.errors[0];
if (currentError) {
await this.markAsCurrentError(currentError);
await this.markAsCurrentError(currentError, {
forceReselect: true,
reveal: true,
});
}
}
private async filter(
errors: CoreError.ErrorLocation[]
): Promise<CoreError.ErrorLocation[]> {
if (!errors.length) {
return [];
}
await this.preferences.ready;
if (this.preferences['arduino.compile.experimental']) {
return errors;
}
// Always shows maximum one error; hence the code lens navigation is unavailable.
return [errors[0]];
}
private async decorateEditors(
errors: Map<string, CoreError.ErrorLocation[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
@@ -342,11 +469,11 @@ export class CompilerErrors
uri: string,
errors: CoreError.ErrorLocation[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.editorManager.getByUri(new URI(uri));
const editor = await this.monacoEditor(uri);
if (!editor) {
return { dispose: Disposable.NULL, errors: [] };
}
const oldDecorations = editor.editor.deltaDecorations({
const oldDecorations = editor.deltaDecorations({
oldDecorations: [],
newDecorations: errors.map((error) =>
this.compilerErrorDecoration(error.location.range)
@@ -355,13 +482,19 @@ export class CompilerErrors
return {
dispose: Disposable.create(() => {
if (editor) {
editor.editor.deltaDecorations({
editor.deltaDecorations({
oldDecorations,
newDecorations: [],
});
}
}),
errors: oldDecorations.map((id) => ({ id, uri })),
errors: oldDecorations.map((id, index) => ({
id,
uri,
rangesInOutput: errors[index].rangesInOutput.map((range) =>
this.p2m.asRange(range)
),
})),
};
}
@@ -371,7 +504,7 @@ export class CompilerErrors
options: {
isWholeLine: true,
className: 'compiler-error',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
},
};
}
@@ -379,11 +512,10 @@ export class CompilerErrors
/**
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
*/
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
const monacoEditor = this.monacoEditor(editor);
if (!monacoEditor) {
return;
}
private handleSelectionChange(
monacoEditor: MonacoEditor,
selection: Range
): void {
const uri = monacoEditor.uri.toString();
const monacoSelection = this.p2m.asRange(selection);
console.log(
@@ -418,12 +550,13 @@ export class CompilerErrors
console.trace('No match');
return undefined;
};
const error = this.errors
.filter((error) => error.uri === uri)
.map((error) => ({
error,
range: ErrorDecoration.rangeOf(error, monacoEditor),
}))
const errorsPerResource = this.errors.filter((error) => error.uri === uri);
const rangesPerResource = ErrorDecoration.rangeOf(
monacoEditor,
errorsPerResource
);
const error = rangesPerResource
.map((range, index) => ({ error: errorsPerResource[index], range }))
.map(({ error, range }) => {
if (range) {
const priority = calculatePriority(range, monacoSelection);
@@ -464,66 +597,77 @@ export class CompilerErrors
}
/**
* If a document change "destroys" the range of the decoration, the decoration must be removed.
* If the text document changes in the line where compiler errors are, the compiler errors will be removed.
*/
private handleDocumentContentChange(
editor: EditorWidget,
monacoEditor: MonacoEditor,
event: TextDocumentChangeEvent
): void {
const monacoEditor = this.monacoEditor(editor);
if (!monacoEditor) {
return;
}
// A decoration location can be "destroyed", hence should be deleted when:
// - deleting range (start != end AND text is empty)
// - inserting text into range (start != end AND text is not empty)
// Filter unrelated delta changes to spare the CPU.
const relevantChanges = event.contentChanges.filter(
({ range: { start, end } }) =>
start.line !== end.line || start.character !== end.character
const errorsPerResource = this.errors.filter(
(error) => error.uri === event.document.uri
);
if (!relevantChanges.length) {
return;
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
const doc = event.document;
if (doc instanceof MonacoEditorModel) {
editorOrModel = doc.textEditorModel;
}
const resolvedMarkers = this.errors
.filter((error) => error.uri === event.document.uri)
.map((error, index) => {
const range = ErrorDecoration.rangeOf(error, monacoEditor);
if (range) {
return { error, range, index };
}
return undefined;
})
.filter(notEmpty);
const decorationIdsToRemove = relevantChanges
const rangesPerResource = ErrorDecoration.rangeOf(
editorOrModel,
errorsPerResource
);
const resolvedDecorations = rangesPerResource.map((range, index) => ({
error: errorsPerResource[index],
range,
}));
const decoratorsToRemove = event.contentChanges
.map(({ range }) => this.p2m.asRange(range))
.map((changeRange) =>
resolvedMarkers.filter(({ range: decorationRange }) =>
changeRange.containsRange(decorationRange)
)
.map((changedRange) =>
resolvedDecorations
.filter(({ range: decorationRange }) => {
if (!decorationRange) {
return false;
}
const affects =
changedRange.startLineNumber <= decorationRange.startLineNumber &&
changedRange.endLineNumber >= decorationRange.endLineNumber;
console.log(
'compiler-errors',
`decoration range: ${decorationRange.toString()}, change range: ${changedRange.toString()}, affects: ${affects}`
);
return affects;
})
.map(({ error }) => {
const index = this.errors.findIndex((candidate) =>
ErrorDecorationRef.sameAs(candidate, error)
);
return index !== -1 ? { error, index } : undefined;
})
.filter(notEmpty)
)
.reduce((acc, curr) => acc.concat(curr), [])
.map(({ error, index }) => {
this.errors.splice(index, 1);
return error.id;
});
if (!decorationIdsToRemove.length) {
return;
.sort((left, right) => left.index - right.index); // highest index last
if (decoratorsToRemove.length) {
let i = decoratorsToRemove.length;
while (i--) {
this.errors.splice(decoratorsToRemove[i].index, 1);
}
monacoEditor.getControl().deltaDecorations(
decoratorsToRemove.map(({ error }) => error.id),
[]
);
this.onDidChangeEmitter.fire(this);
}
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
this.onDidChangeEmitter.fire(this);
}
private async trackEditors(
errors: Map<string, CoreError.ErrorLocation[]>,
...track: ((editor: EditorWidget) => Disposable)[]
...track: ((editor: MonacoEditor) => Disposable)[]
): Promise<Disposable> {
return new DisposableCollection(
...(await Promise.all(
Array.from(errors.keys()).map(async (uri) => {
const editor = await this.editorManager.getByUri(new URI(uri));
const editor = await this.monacoEditor(uri);
if (!editor) {
return Disposable.NULL;
}
@@ -533,15 +677,18 @@ export class CompilerErrors
);
}
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
private async markAsCurrentError(
ref: ErrorDecorationRef,
options?: { forceReselect?: boolean; reveal?: boolean }
): Promise<void> {
const index = this.errors.findIndex((candidate) =>
ErrorDecoration.sameAs(candidate, error)
ErrorDecorationRef.sameAs(candidate, ref)
);
if (index < 0) {
console.warn(
'compiler-errors',
`Failed to mark error ${
error.id
ref.id
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
({ id }) => id
)}`
@@ -550,15 +697,18 @@ export class CompilerErrors
}
const newError = this.errors[index];
if (
options?.forceReselect ||
!this.currentError ||
!ErrorDecoration.sameAs(this.currentError, newError)
!ErrorDecorationRef.sameAs(this.currentError, newError)
) {
this.currentError = this.errors[index];
console.log(
'compiler-errors',
`Current error changed to ${this.currentError.id}`
);
this.currentErrorDidChangEmitter.fire(this.currentError);
if (options?.reveal) {
this.currentErrorDidChangEmitter.fire(this.currentError);
}
this.onDidChangeEmitter.fire(this);
}
}
@@ -593,32 +743,33 @@ export class CompilerErrors
}
console.warn(
'compiler-errors',
`could not found editor widget for URI: ${uri}`
`could not find editor widget for URI: ${uri}`
);
return undefined;
}
private groupByResource(
errors: CoreError.ErrorLocation[]
): Map<string, CoreError.ErrorLocation[]> {
return errors.reduce((acc, curr) => {
const {
location: { uri },
} = curr;
let errors = acc.get(uri);
if (!errors) {
errors = [];
acc.set(uri, errors);
private groupBy<K, V>(
elements: V[],
extractKey: (element: V) => K
): Map<K, V[]> {
return elements.reduce((acc, curr) => {
const key = extractKey(curr);
let values = acc.get(key);
if (!values) {
values = [];
acc.set(key, values);
}
errors.push(curr);
values.push(curr);
return acc;
}, new Map<string, CoreError.ErrorLocation[]>());
}, new Map<K, V[]>());
}
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
private monacoEditor(
uriOrWidget: string | EditorWidget
uri: string | monaco.Uri
): Promise<MonacoEditor | undefined>;
private monacoEditor(
uriOrWidget: string | monaco.Uri | EditorWidget
): MaybePromise<MonacoEditor | undefined> {
if (uriOrWidget instanceof EditorWidget) {
const editor = uriOrWidget.editor;
@@ -646,5 +797,8 @@ export namespace CompilerErrors {
export const PREVIOUS_ERROR: Command = {
id: 'arduino-editor-previous-error',
};
export const MARK_AS_CURRENT: Command = {
id: 'arduino-editor-mark-as-current-error',
};
}
}

View File

@@ -59,6 +59,8 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
export {
Command,
@@ -186,6 +188,22 @@ export abstract class CoreServiceContribution extends SketchContribution {
@inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
/**
* This is the internal (Theia) ID of the notification that is currently visible.
* It's stored here as a field to be able to close it before executing any new core command (such as verify, upload, etc.)
*/
private visibleNotificationId: string | undefined;
protected clearVisibleNotification(): void {
if (this.visibleNotificationId) {
this.notificationManager.clear(this.visibleNotificationId);
this.visibleNotificationId = undefined;
}
}
protected handleError(error: unknown): void {
this.tryToastErrorMessage(error);
}
@@ -204,10 +222,17 @@ export abstract class CoreServiceContribution extends SketchContribution {
} catch {}
}
if (message) {
if (message.includes('Missing FQBN (Fully Qualified Board Name)')) {
message = nls.localize(
'arduino/coreContribution/noBoardSelected',
'No board selected. Please select your Arduino board from the Tools > Board menu.'
);
}
const copyAction = nls.localize(
'arduino/coreContribution/copyError',
'Copy error messages'
);
this.visibleNotificationId = this.notificationId(message, copyAction);
this.messageService.error(message, copyAction).then(async (action) => {
if (action === copyAction) {
const content = await this.outputChannelManager.contentOfChannel(
@@ -241,6 +266,14 @@ export abstract class CoreServiceContribution extends SketchContribution {
});
return result;
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager.getMessageId({
text: message,
actions,
type: MessageType.Error,
});
}
}
export namespace Contribution {

View File

@@ -1,6 +1,10 @@
import { LocalStorageService } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { BoardsService, LibraryService } from '../../common/protocol';
import {
BoardsService,
LibraryLocation,
LibraryService,
} from '../../common/protocol';
import { Contribution } from './contribution';
@injectable()
@@ -57,6 +61,7 @@ export class FirstStartupInstaller extends Contribution {
item: builtInLibrary,
installDependencies: true,
noOverwrite: true, // We don't want to automatically replace custom libraries the user might already have in place
installLocation: LibraryLocation.BUILTIN,
});
} catch (e) {
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/2ea3608453b17b1157f8a1dc892af2e13e40f4f0#diff-1de7569144d4e260f8dde0e0d00a4e2a218c57966d583da1687a70d518986649R95

View File

@@ -2,8 +2,7 @@ import { MaybePromise } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { Formatter } from '../../common/protocol/formatter';
import { InoSelector } from '../ino-selectors';
import { fullRange } from '../utils/monaco';
import { InoSelector } from '../selectors';
import { Contribution, URI } from './contribution';
@injectable()
@@ -40,7 +39,7 @@ export class Format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const range = fullRange(model);
const range = model.getFullModelRange();
const text = await this.format(model, range, options);
return [{ range, text }];
}

View File

@@ -41,7 +41,9 @@ export class Help extends Contribution {
);
registry.registerCommand(
Help.Commands.ENVIRONMENT,
createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
createOpenHandler(
'https://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2'
)
);
registry.registerCommand(
Help.Commands.TROUBLESHOOTING,

View File

@@ -1,6 +1,7 @@
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
import { Later } from '../../common/nls';
import { SketchesError } from '../../common/protocol';
import {
Command,
@@ -41,20 +42,18 @@ export class OpenSketchFiles extends SketchContribution {
sketch.name
);
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
this.messageService
.info(message, nls.localize('arduino/common/later', 'Later'), yes)
.then(async (answer) => {
if (answer === yes) {
this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
this.messageService.info(message, Later, yes).then((answer) => {
if (answer === yes) {
this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
}
} catch (err) {
if (SketchesError.NotFound.is(err)) {

View File

@@ -57,6 +57,7 @@ export class SaveAsSketch extends SketchContribution {
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
@@ -102,18 +103,22 @@ export class SaveAsSketch extends SketchContribution {
});
if (workspaceUri) {
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
if (markAsRecentlyOpened) {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
}
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), {
recursive: true,
});
} catch {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
}
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
// This window will navigate away.
// Explicitly stop the contribution to dispose the file watcher before deleting the temp sketch.
// Otherwise, users might see irrelevant _Unable to watch for file changes in this large workspace._ notification.
// https://github.com/arduino/arduino-ide/issues/39.
this.sketchServiceClient.onStop();
// TODO: consider implementing the temp sketch deletion the following way:
// Open the other sketch with a `delete the temp sketch` startup-task.
this.sketchService.notifyDeleteSketch(sketch); // This is a notification and will execute on the backend.
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
@@ -170,12 +175,14 @@ export namespace SaveAsSketch {
* Ignored if `openAfterMove` is `false`.
*/
readonly wipeOriginal?: boolean;
readonly markAsRecentlyOpened?: boolean;
}
export namespace Options {
export const DEFAULT: Options = {
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
markAsRecentlyOpened: false,
};
}
}

View File

@@ -30,10 +30,7 @@ export class SketchFilesTracker extends SketchContribution {
override onReady(): void {
this.sketchServiceClient.currentSketch().then(async (sketch) => {
if (
CurrentSketch.isValid(sketch) &&
!(await this.sketchService.isTemp(sketch))
) {
if (CurrentSketch.isValid(sketch)) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {

View File

@@ -1,6 +1,6 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService } from '../../common/protocol';
import { BoardUserField, CoreService, Port } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
@@ -12,7 +12,7 @@ import {
CoreServiceContribution,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
@@ -61,10 +61,11 @@ export class UploadSketch extends CoreServiceContribution {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
if (
this.boardRequiresUserFields &&
key &&
!this.cachedUserFields.has(key)
) {
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
await this.boardsServiceProvider.selectedBoardUserFields()
@@ -190,7 +191,9 @@ export class UploadSketch extends CoreServiceContribution {
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
this.onDidChangeEmitter.fire();
this.clearVisibleNotification();
const verifyOptions =
await this.commandService.executeCommand<CoreService.Options.Compile>(
@@ -242,6 +245,7 @@ export class UploadSketch extends CoreServiceContribution {
this.handleError(e);
} finally {
this.uploadInProgress = false;
this.boardsServiceProvider.attemptPostUploadAutoSelect();
this.onDidChangeEmitter.fire();
}
}
@@ -263,7 +267,7 @@ export class UploadSketch extends CoreServiceContribution {
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const port = boardsConfig.selectedPort;
const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
return {
sketch,
fqbn,
@@ -275,7 +279,29 @@ export class UploadSketch extends CoreServiceContribution {
};
}
private userFields() {
/**
* This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740)
* This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236).
*
* Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`.
* This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`.
*/
private maybeUpdatePortProperties(port: Port | undefined): Port | undefined {
if (port) {
const key = Port.keyOf(port);
for (const candidate of this.boardsServiceProvider.availablePorts) {
if (key === Port.keyOf(candidate) && candidate.properties) {
return {
...port,
properties: deepClone(candidate.properties),
};
}
}
}
return port;
}
private userFields(): BoardUserField[] {
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
}

View File

@@ -108,6 +108,7 @@ export class VerifySketch extends CoreServiceContribution {
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
}
this.clearVisibleNotification();
this.coreErrorHandler.reset();
const options = await this.options(params?.exportBinaries);

View File

@@ -1,5 +1,9 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
@@ -153,6 +157,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
'Upload SSL Root Certificates'
),
});
this.node.id = 'certificate-uploader-dialog-container';
this.contentNode.classList.add('certificate-uploader-dialog');
this.acceptButton = undefined;
}

View File

@@ -101,6 +101,7 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}

View File

@@ -1,4 +1,3 @@
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common';
import { shell } from 'electron';
import * as React from '@theia/core/shared/react';
@@ -7,36 +6,32 @@ import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';
export type IDEUpdaterComponentProps = {
updateInfo: UpdateInfo;
windowService: WindowService;
export interface UpdateProgress {
progressInfo?: ProgressInfo | undefined;
downloadFinished?: boolean;
downloadStarted?: boolean;
progress?: ProgressInfo;
error?: Error;
onDownload: () => void;
onClose: () => void;
onSkipVersion: () => void;
onCloseAndInstall: () => void;
};
}
export interface IDEUpdaterComponentProps {
updateInfo: UpdateInfo;
updateProgress: UpdateProgress;
}
export const IDEUpdaterComponent = ({
updateInfo: { version, releaseNotes },
downloadStarted = false,
downloadFinished = false,
windowService,
progress,
error,
onDownload,
onClose,
onSkipVersion,
onCloseAndInstall,
updateInfo,
updateProgress: {
downloadStarted = false,
downloadFinished = false,
progressInfo,
error,
},
}: IDEUpdaterComponentProps): React.ReactElement => {
const changelogDivRef = React.useRef() as React.MutableRefObject<
HTMLDivElement
>;
const { version, releaseNotes } = updateInfo;
const changelogDivRef =
React.useRef() as React.MutableRefObject<HTMLDivElement>;
React.useEffect(() => {
if (!!releaseNotes) {
if (!!releaseNotes && changelogDivRef.current) {
let changelog: string;
if (typeof releaseNotes === 'string') changelog = releaseNotes;
else
@@ -58,12 +53,7 @@ export const IDEUpdaterComponent = ({
changelogDivRef.current
);
}
}, [releaseNotes]);
const closeButton = (
<button onClick={onClose} type="button" className="theia-button secondary">
{nls.localize('arduino/ide-updater/notNowButton', 'Not now')}
</button>
);
}, [updateInfo]);
const DownloadCompleted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloaded">
@@ -80,19 +70,6 @@ export const IDEUpdaterComponent = ({
'Close the software and install the update on your machine.'
)}
</div>
<div className="buttons-container">
{closeButton}
<button
onClick={onCloseAndInstall}
type="button"
className="theia-button close-and-install"
>
{nls.localize(
'arduino/ide-updater/closeAndInstallButton',
'Close and Install'
)}
</button>
</div>
</div>
);
@@ -104,7 +81,7 @@ export const IDEUpdaterComponent = ({
'Downloading the latest version of the Arduino IDE.'
)}
</div>
<ProgressBar percent={progress?.percent} showPercentage />
<ProgressBar percent={progressInfo?.percent} showPercentage />
</div>
);
@@ -130,46 +107,14 @@ export const IDEUpdaterComponent = ({
)}
</div>
{releaseNotes && (
<div className="dialogRow">
<div className="changelog-container" ref={changelogDivRef} />
<div className="dialogRow changelog-container">
<div className="changelog" ref={changelogDivRef} />
</div>
)}
<div className="buttons-container">
<button
onClick={onSkipVersion}
type="button"
className="theia-button secondary skip-version"
>
{nls.localize(
'arduino/ide-updater/skipVersionButton',
'Skip Version'
)}
</button>
<div className="push"></div>
{closeButton}
<button
onClick={onDownload}
type="button"
className="theia-button primary"
>
{nls.localize('arduino/ide-updater/downloadButton', 'Download')}
</button>
</div>
</div>
</div>
);
const onGoToDownloadClick = (
event: React.SyntheticEvent<HTMLAnchorElement, Event>
) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
event.nativeEvent.preventDefault();
windowService.openNewWindow(target.href, { external: true });
onClose();
}
};
const GoToDownloadPage: () => React.ReactElement = () => (
<div className="ide-updater-dialog--go-to-download-page">
<div>
@@ -178,19 +123,6 @@ export const IDEUpdaterComponent = ({
"An update for the Arduino IDE is available, but we're not able to download and install it automatically. Please go to the download page and download the latest version from there."
)}
</div>
<div className="buttons-container">
{closeButton}
<a
className="theia-button primary"
href="https://www.arduino.cc/en/software#experimental-software"
onClick={onGoToDownloadClick}
>
{nls.localize(
'arduino/ide-updater/goToDownloadButton',
'Go To Download'
)}
</a>
</div>
</div>
);

View File

@@ -1,113 +1,57 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { nls } from '@theia/core';
import { IDEUpdaterComponent } from './ide-updater-component';
import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component';
import {
IDEUpdater,
IDEUpdaterClient,
ProgressInfo,
SKIP_IDE_VERSION,
UpdateInfo,
} from '../../../common/protocol/ide-updater';
import { LocalStorageService } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
const DOWNLOAD_PAGE_URL =
'https://www.arduino.cc/en/software#experimental-software';
@injectable()
export class IDEUpdaterDialogWidget extends ReactWidget {
protected isOpen = new Object();
updateInfo: UpdateInfo;
progressInfo: ProgressInfo | undefined;
error: Error | undefined;
downloadFinished: boolean;
downloadStarted: boolean;
onClose: () => void;
private _updateInfo: UpdateInfo;
private _updateProgress: UpdateProgress = {};
@inject(IDEUpdater)
protected readonly updater: IDEUpdater;
@inject(IDEUpdaterClient)
protected readonly updaterClient: IDEUpdaterClient;
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(WindowService)
protected windowService: WindowService;
init(updateInfo: UpdateInfo, onClose: () => void): void {
this.updateInfo = updateInfo;
this.progressInfo = undefined;
this.error = undefined;
this.downloadStarted = false;
this.downloadFinished = false;
this.onClose = onClose;
this.updaterClient.onError((e) => {
this.error = e;
this.update();
});
this.updaterClient.onDownloadProgressChanged((e) => {
this.progressInfo = e;
this.update();
});
this.updaterClient.onDownloadFinished((e) => {
this.downloadFinished = true;
this.update();
});
}
async onSkipVersion(): Promise<void> {
this.localStorageService.setData<string>(
SKIP_IDE_VERSION,
this.updateInfo.version
);
this.close();
}
override close(): void {
super.close();
this.onClose();
}
onDispose(): void {
if (this.downloadStarted && !this.downloadFinished)
this.updater.stopDownload();
}
async onDownload(): Promise<void> {
this.progressInfo = undefined;
this.downloadStarted = true;
this.error = undefined;
this.updater.downloadUpdate();
setUpdateInfo(updateInfo: UpdateInfo): void {
this._updateInfo = updateInfo;
this.update();
}
onCloseAndInstall(): void {
this.updater.quitAndInstall();
mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
get updateInfo(): UpdateInfo {
return this._updateInfo;
}
get updateProgress(): UpdateProgress {
return this._updateProgress;
}
protected render(): React.ReactNode {
return !!this.updateInfo ? (
<form>
<IDEUpdaterComponent
updateInfo={this.updateInfo}
windowService={this.windowService}
downloadStarted={this.downloadStarted}
downloadFinished={this.downloadFinished}
progress={this.progressInfo}
error={this.error}
onClose={this.close.bind(this)}
onSkipVersion={this.onSkipVersion.bind(this)}
onDownload={this.onDownload.bind(this)}
onCloseAndInstall={this.onCloseAndInstall.bind(this)}
/>
</form>
return !!this._updateInfo ? (
<IDEUpdaterComponent
updateInfo={this._updateInfo}
updateProgress={this._updateProgress}
/>
) : null;
}
}
@@ -118,7 +62,19 @@ export class IDEUpdaterDialogProps extends DialogProps {}
@injectable()
export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(IDEUpdaterDialogWidget)
protected readonly widget: IDEUpdaterDialogWidget;
private readonly widget: IDEUpdaterDialogWidget;
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@inject(IDEUpdaterClient)
private readonly updaterClient: IDEUpdaterClient;
@inject(LocalStorageService)
private readonly localStorageService: LocalStorageService;
@inject(WindowService)
private readonly windowService: WindowService;
constructor(
@inject(IDEUpdaterDialogProps)
@@ -130,10 +86,26 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
'Software Update'
),
});
this.node.id = 'ide-updater-dialog-container';
this.contentNode.classList.add('ide-updater-dialog');
this.acceptButton = undefined;
}
@postConstruct()
protected init(): void {
this.updaterClient.onUpdaterDidFail((error) => {
this.appendErrorButtons();
this.widget.mergeUpdateProgress({ error });
});
this.updaterClient.onDownloadProgressDidChange((progressInfo) => {
this.widget.mergeUpdateProgress({ progressInfo });
});
this.updaterClient.onDownloadDidFinish(() => {
this.appendInstallButtons();
this.widget.mergeUpdateProgress({ downloadFinished: true });
});
}
get value(): UpdateInfo {
return this.widget.updateInfo;
}
@@ -143,24 +115,123 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.appendInitialButtons();
super.onAfterAttach(msg);
this.update();
}
private clearButtons(): void {
while (this.controlPanel.firstChild) {
this.controlPanel.removeChild(this.controlPanel.firstChild);
}
this.closeButton = undefined;
}
private appendNotNowButton(): void {
this.appendCloseButton(
nls.localize('arduino/ide-updater/notNowButton', 'Not now')
);
if (this.closeButton) {
this.addCloseAction(this.closeButton, 'click');
}
}
private appendInitialButtons(): void {
this.clearButtons();
const skipVersionButton = this.createButton(
nls.localize('arduino/ide-updater/skipVersionButton', 'Skip Version')
);
skipVersionButton.classList.add('secondary');
skipVersionButton.classList.add('skip-version-button');
this.addAction(skipVersionButton, this.skipVersion.bind(this), 'click');
this.controlPanel.appendChild(skipVersionButton);
this.appendNotNowButton();
const downloadButton = this.createButton(
nls.localize('arduino/ide-updater/downloadButton', 'Download')
);
this.addAction(downloadButton, this.startDownload.bind(this), 'click');
this.controlPanel.appendChild(downloadButton);
downloadButton.focus();
}
private appendInstallButtons(): void {
this.clearButtons();
this.appendNotNowButton();
const closeAndInstallButton = this.createButton(
nls.localize(
'arduino/ide-updater/closeAndInstallButton',
'Close and Install'
)
);
this.addAction(
closeAndInstallButton,
this.closeAndInstall.bind(this),
'click'
);
this.controlPanel.appendChild(closeAndInstallButton);
closeAndInstallButton.focus();
}
private appendErrorButtons(): void {
this.clearButtons();
this.appendNotNowButton();
const goToDownloadPageButton = this.createButton(
nls.localize('arduino/ide-updater/goToDownloadButton', 'Go To Download')
);
this.addAction(
goToDownloadPageButton,
this.openDownloadPage.bind(this),
'click'
);
this.controlPanel.appendChild(goToDownloadPageButton);
goToDownloadPageButton.focus();
}
private openDownloadPage(): void {
this.windowService.openNewWindow(DOWNLOAD_PAGE_URL, { external: true });
this.close();
}
private skipVersion(): void {
this.localStorageService.setData<string>(
SKIP_IDE_VERSION,
this.widget.updateInfo.version
);
this.close();
}
private startDownload(): void {
this.widget.mergeUpdateProgress({
downloadStarted: true,
});
this.clearButtons();
this.updater.downloadUpdate();
}
private closeAndInstall() {
this.updater.quitAndInstall();
this.close();
}
override async open(
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
this.widget.init(data, this.close.bind(this));
this.widget.mergeUpdateProgress({
progressInfo: undefined,
downloadStarted: false,
downloadFinished: false,
error: undefined,
});
this.widget.setUpdateInfo(data);
return super.open();
}
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
@@ -168,6 +239,12 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
override close(): void {
this.widget.dispose();
if (
this.widget.updateProgress?.downloadStarted &&
!this.widget.updateProgress?.downloadFinished
) {
this.updater.stopDownload();
}
super.close();
}
}

View File

@@ -23,8 +23,8 @@ import {
} from '@theia/core/lib/common/i18n/localization';
import SettingsStepInput from './settings-step-input';
const maxScale = 200;
const minScale = -100;
const maxScale = 280;
const minScale = -60;
const scaleStep = 20;
const maxFontSize = 72;
@@ -188,15 +188,17 @@ export class SettingsComponent extends React.Component<
/>
{nls.localize('arduino/preferences/automatic', 'Automatic')}
</label>
<SettingsStepInput
value={scalePercentage}
setSettingsStateValue={this.setInterfaceScale}
step={scaleStep}
maxValue={maxScale}
minValue={minScale}
classNames={{ input: 'theia-input small with-margin' }}
/>
%
<div>
<SettingsStepInput
value={scalePercentage}
setSettingsStateValue={this.setInterfaceScale}
step={scaleStep}
maxValue={maxScale}
minValue={minScale}
unitOfMeasure="%"
classNames={{ input: 'theia-input small with-margin' }}
/>
</div>
</div>
<div className="flex-line">
<select

View File

@@ -7,14 +7,22 @@ interface SettingsStepInputProps {
step: number;
maxValue: number;
minValue: number;
unitOfMeasure?: string;
classNames?: { [key: string]: string };
}
const SettingsStepInput: React.FC<SettingsStepInputProps> = (
props: SettingsStepInputProps
) => {
const { value, setSettingsStateValue, step, maxValue, minValue, classNames } =
props;
const {
value,
setSettingsStateValue,
step,
maxValue,
minValue,
unitOfMeasure,
classNames,
} = props;
const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max);
@@ -86,6 +94,7 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
&#9662;
</button>
</div>
{unitOfMeasure && `${unitOfMeasure}`}
</div>
);
};

View File

@@ -65,7 +65,11 @@ export const UserFieldsComponent = ({
type={field.secret ? 'password' : 'text'}
value={field.value}
className="theia-input"
placeholder={'Enter ' + field.label}
placeholder={nls.localize(
'arduino/userFields/enterField',
'Enter {0}',
field.label
)}
onChange={updateUserField(index)}
/>
</div>

View File

@@ -5,36 +5,43 @@ import { IDEUpdaterClient } from '../../common/protocol/ide-updater';
@injectable()
export class IDEUpdaterClientImpl implements IDEUpdaterClient {
protected readonly onErrorEmitter = new Emitter<Error>();
protected readonly onCheckingForUpdateEmitter = new Emitter<void>();
protected readonly onUpdateAvailableEmitter = new Emitter<UpdateInfo>();
protected readonly onUpdateNotAvailableEmitter = new Emitter<UpdateInfo>();
protected readonly onDownloadProgressEmitter = new Emitter<ProgressInfo>();
protected readonly onDownloadFinishedEmitter = new Emitter<UpdateInfo>();
protected readonly onUpdaterDidFailEmitter = new Emitter<Error>();
protected readonly onUpdaterDidCheckForUpdateEmitter = new Emitter<void>();
protected readonly onUpdaterDidFindUpdateAvailableEmitter =
new Emitter<UpdateInfo>();
protected readonly onUpdaterDidNotFindUpdateAvailableEmitter =
new Emitter<UpdateInfo>();
protected readonly onDownloadProgressDidChangeEmitter =
new Emitter<ProgressInfo>();
protected readonly onDownloadDidFinishEmitter = new Emitter<UpdateInfo>();
readonly onError = this.onErrorEmitter.event;
readonly onCheckingForUpdate = this.onCheckingForUpdateEmitter.event;
readonly onUpdateAvailable = this.onUpdateAvailableEmitter.event;
readonly onUpdateNotAvailable = this.onUpdateNotAvailableEmitter.event;
readonly onDownloadProgressChanged = this.onDownloadProgressEmitter.event;
readonly onDownloadFinished = this.onDownloadFinishedEmitter.event;
readonly onUpdaterDidFail = this.onUpdaterDidFailEmitter.event;
readonly onUpdaterDidCheckForUpdate =
this.onUpdaterDidCheckForUpdateEmitter.event;
readonly onUpdaterDidFindUpdateAvailable =
this.onUpdaterDidFindUpdateAvailableEmitter.event;
readonly onUpdaterDidNotFindUpdateAvailable =
this.onUpdaterDidNotFindUpdateAvailableEmitter.event;
readonly onDownloadProgressDidChange =
this.onDownloadProgressDidChangeEmitter.event;
readonly onDownloadDidFinish = this.onDownloadDidFinishEmitter.event;
notifyError(message: Error): void {
this.onErrorEmitter.fire(message);
notifyUpdaterFailed(message: Error): void {
this.onUpdaterDidFailEmitter.fire(message);
}
notifyCheckingForUpdate(message: void): void {
this.onCheckingForUpdateEmitter.fire(message);
notifyCheckedForUpdate(message: void): void {
this.onUpdaterDidCheckForUpdateEmitter.fire(message);
}
notifyUpdateAvailable(message: UpdateInfo): void {
this.onUpdateAvailableEmitter.fire(message);
notifyUpdateAvailableFound(message: UpdateInfo): void {
this.onUpdaterDidFindUpdateAvailableEmitter.fire(message);
}
notifyUpdateNotAvailable(message: UpdateInfo): void {
this.onUpdateNotAvailableEmitter.fire(message);
notifyUpdateAvailableNotFound(message: UpdateInfo): void {
this.onUpdaterDidNotFindUpdateAvailableEmitter.fire(message);
}
notifyDownloadProgressChanged(message: ProgressInfo): void {
this.onDownloadProgressEmitter.fire(message);
this.onDownloadProgressDidChangeEmitter.fire(message);
}
notifyDownloadFinished(message: UpdateInfo): void {
this.onDownloadFinishedEmitter.fire(message);
this.onDownloadDidFinishEmitter.fire(message);
}
}

View File

@@ -54,8 +54,8 @@ export class IDEUpdaterCommands implements CommandContribution {
export namespace IDEUpdaterCommands {
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
{
id: 'arduino-ide-check-for-updates',
label: 'Check for Arduino IDE updates',
id: 'arduino-check-for-ide-updates',
label: 'Check for Arduino IDE Updates',
category: 'Arduino',
},
'arduino/ide-updater/checkForUpdates'

View File

@@ -1,19 +1,28 @@
import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
import {
injectable,
postConstruct,
inject,
} from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import {
LibraryPackage,
LibrarySearch,
LibraryService,
} from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { nls } from '@theia/core/lib/common';
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<LibraryPackage> {
export class LibraryListWidget extends ListWidget<
LibraryPackage,
LibrarySearch
> {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = nls.localize(
'arduino/library/title',
@@ -21,9 +30,9 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
);
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<LibraryPackage>
@inject(LibraryService) private service: LibraryService,
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
@inject(LibraryFilterRenderer) filterRenderer: LibraryFilterRenderer
) {
super({
id: LibraryListWidget.WIDGET_ID,
@@ -34,6 +43,8 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
itemLabel: (item: LibraryPackage) => item.name,
itemDeprecated: (item: LibraryPackage) => item.deprecated,
itemRenderer,
filterRenderer,
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
});
}
@@ -41,7 +52,9 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
protected override init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryDidInstall(() => this.refresh(undefined)),
this.notificationCenter.onLibraryDidInstall(() =>
this.refresh(undefined)
),
this.notificationCenter.onLibraryDidUninstall(() =>
this.refresh(undefined)
),

View File

@@ -145,7 +145,10 @@ export class MonitorManagerProxyClientImpl
if (
selectedBoard?.fqbn !==
this.lastConnectedBoard?.selectedBoard?.fqbn ||
selectedPort?.id !== this.lastConnectedBoard?.selectedPort?.id
Port.keyOf(selectedPort) !==
(this.lastConnectedBoard.selectedPort
? Port.keyOf(this.lastConnectedBoard.selectedPort)
: undefined)
) {
this.onMonitorShouldResetEmitter.fire(null);
this.lastConnectedBoard = {

View File

@@ -1,4 +1,5 @@
import * as monaco from '@theia/monaco-editor-core';
import { OutputUri } from '@theia/output/lib/common/output-uri';
/**
* Exclusive "ino" document selector for monaco.
*/
@@ -11,3 +12,11 @@ function selectorOf(
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
}));
}
/**
* Selector for the `monaco` resource in the Arduino _Output_ channel.
*/
export const ArduinoOutputSelector: monaco.languages.LanguageSelector = {
scheme: OutputUri.SCHEME,
pattern: '**/Arduino',
};

View File

@@ -5,6 +5,7 @@ import { isOSX } from '@theia/core/lib/common/os';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls';
export namespace SerialMonitorSendInput {
export interface Props {
@@ -86,8 +87,8 @@ export class SerialMonitorSendInput extends React.Component<
? Board.toString(board, {
useFqbn: false,
})
: 'unknown',
port ? port.address : 'unknown'
: Unknown,
port ? port.address : Unknown
);
}

View File

@@ -2,9 +2,11 @@ div#select-board-dialog {
margin: 5px;
}
div#select-board-dialog .selectBoardContainer .body {
div#select-board-dialog .selectBoardContainer {
display: flex;
gap: 10px;
overflow: hidden;
max-height: 100%;
}
.select-board-dialog .head {
@@ -19,12 +21,13 @@ div.dialogContent.select-board-dialog > div.head .title {
margin-bottom: 10px;
}
div#select-board-dialog .selectBoardContainer .body .list .item.selected {
div#select-board-dialog .selectBoardContainer .list .item.selected {
background: var(--theia-secondaryButton-hoverBackground);
}
div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
color: var(--theia-list-activeSelectionIconForeground);
div#select-board-dialog .selectBoardContainer .list .item.selected i {
color: var(--theia-arduino-branding-primary);
}
#select-board-dialog .selectBoardContainer .search,
@@ -34,7 +37,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
background: var(--theia-editor-background);
}
#select-board-dialog .selectBoardContainer .body .search input {
#select-board-dialog .selectBoardContainer .search input {
border: none;
width: 100%;
height: auto;
@@ -46,58 +49,63 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
color: var(--theia-input-foreground);
}
#select-board-dialog .selectBoardContainer .body .search input:focus {
#select-board-dialog .selectBoardContainer .search input:focus {
box-shadow: none;
}
#select-board-dialog .selectBoardContainer .body .container {
#select-board-dialog .selectBoardContainer .container {
flex: 1;
padding: 0px 10px 0px 0px;
min-width: 240px;
max-width: 240px;
overflow: hidden;
max-height: 100%;
}
#select-board-dialog .selectBoardContainer .body .left.container .content {
#select-board-dialog .selectBoardContainer .container .content {
display: flex;
flex-direction: column;
max-height: 100%;
}
#select-board-dialog .selectBoardContainer .left.container .content {
margin: 0 5px 0 0;
}
#select-board-dialog .selectBoardContainer .body .right.container .content {
#select-board-dialog .selectBoardContainer .right.container .content {
margin: 0 0 0 5px;
}
#select-board-dialog .selectBoardContainer .body .container .content .title {
#select-board-dialog .selectBoardContainer .container .content .title {
color: var(--theia-editorWidget-foreground);
padding: 0px 0px 10px 0px;
text-transform: uppercase;
}
#select-board-dialog .selectBoardContainer .body .container .content .footer {
#select-board-dialog .selectBoardContainer .container .content .footer {
padding: 10px 5px 10px 0px;
}
#select-board-dialog .selectBoardContainer .body .container .content .loading {
#select-board-dialog .selectBoardContainer .container .content .loading {
font-size: var(--theia-ui-font-size1);
color: var(--theia-editorWidget-foreground);
padding: 10px 5px 10px 10px;
text-transform: uppercase;
/* The max, min-height comes from `.body .list` 200px + 47px top padding - 2 * 10px top padding */
/* The max, min-height comes from `.list` 200px + 47px top padding - 2 * 10px top padding */
max-height: 227px;
min-height: 227px;
}
#select-board-dialog .selectBoardContainer .body .list .item {
#select-board-dialog .selectBoardContainer .list .item {
padding: 10px 5px 10px 10px;
display: flex;
justify-content: end;
white-space: nowrap;
overflow-x: hidden;
flex: 1 0;
}
#select-board-dialog .selectBoardContainer .body .list .item .selected-icon {
#select-board-dialog .selectBoardContainer .list .item .selected-icon {
margin-left: auto;
}
#select-board-dialog .selectBoardContainer .body .list .item .details {
#select-board-dialog .selectBoardContainer .list .item .details {
font-size: var(--theia-ui-font-size1);
opacity: var(--theia-mod-disabled-opacity);
width: 155px; /* used heuristics for the calculation */
@@ -106,43 +114,36 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
text-overflow: ellipsis;
}
#select-board-dialog .selectBoardContainer .body .list .item.missing {
#select-board-dialog .selectBoardContainer .list .item.missing {
opacity: var(--theia-mod-disabled-opacity);
}
#select-board-dialog .selectBoardContainer .body .list .item:hover {
#select-board-dialog .selectBoardContainer .list .item:hover {
background: var(--theia-secondaryButton-hoverBackground);
}
#select-board-dialog .selectBoardContainer .body .list .label {
max-width: 215px;
width: 215px;
#select-board-dialog .selectBoardContainer .list .label {
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
#select-board-dialog .selectBoardContainer .body .list {
#select-board-dialog .selectBoardContainer .list {
max-height: 200px;
min-height: 200px;
overflow-y: auto;
}
#select-board-dialog .selectBoardContainer .body .ports.list {
#select-board-dialog .selectBoardContainer .ports.list {
margin: 47px 0px 0px 0px; /* 47 is 37 as input height for the `Boards`, plus 10 margin bottom. */
}
#select-board-dialog .selectBoardContainer .body .search {
#select-board-dialog .selectBoardContainer .search {
margin-bottom: 10px;
display: flex;
align-items: center;
padding-right: 5px;
}
.p-Widget.dialogOverlay .dialogContent.select-board-dialog {
width: 500px;
}
.arduino-boards-toolbar-item-container {
align-items: center;
background: var(--theia-arduino-toolbar-dropdown-background);
@@ -264,10 +265,20 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
.hc-black.hc-theia.theia-hc #select-board-dialog .selectBoardContainer .body .list .item:hover {
.hc-black.hc-theia.theia-hc #select-board-dialog .selectBoardContainer .list .item:hover {
outline: 1px dashed var(--theia-focusBorder);
}
.hc-black.hc-theia.theia-hc div#select-board-dialog .selectBoardContainer .body .list .item.selected {
.hc-black.hc-theia.theia-hc div#select-board-dialog .selectBoardContainer .list .item.selected {
outline: 1px solid var(--theia-focusBorder);
}
@media only screen and (max-height: 400px) {
div.dialogContent.select-board-dialog > div.head {
display: none;
}
#select-board-dialog .selectBoardContainer .container .content .title {
display: none;
}
}

View File

@@ -1,4 +1,4 @@
.certificate-uploader-dialog {
#certificate-uploader-dialog-container > .dialogBlock {
width: 600px;
}

View File

@@ -9,11 +9,13 @@
total = padding + margin = 96px
*/
max-width: calc(100% - 96px) !important;
min-width: unset;
max-height: 560px;
padding: 0 28px;
}
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
padding: 36px 0 28px;
padding: 20px 0;
font-weight: 500;
background-color: transparent;
font-size: var(--theia-ui-font-size2);
@@ -28,6 +30,7 @@
.p-Widget.dialogOverlay .dialogBlock .dialogContent {
padding: 0;
overflow: auto;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent > input {
@@ -75,3 +78,10 @@
.fa.disabled {
opacity: .4;
}
@media only screen and (max-height: 560px) {
.p-Widget.dialogOverlay .dialogBlock {
max-height: 400px;
}
}

View File

@@ -1,8 +1,7 @@
/* Show the dirty indicator on unclosable widgets. On hover, it should still show the dot instead of the X. */
/* https://github.com/arduino/arduino-pro-ide/issues/380 */
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
background-size: 13px;
background-image: var(--theia-icon-circle);
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.a-mod-uncloseable.theia-mod-dirty > .p-TabBar-tabCloseIcon:before {
content: "\ea71";
}
.monaco-list-row.show-file-icons.focused {

View File

@@ -1,4 +1,4 @@
.firmware-uploader-dialog {
#firmware-uploader-dialog-container > .dialogBlock {
width: 600px;
}
@@ -11,7 +11,6 @@
}
.firmware-uploader-dialog .dialogRow > button{
width: 33%;
margin-right: 3px;
}

View File

@@ -1,4 +1,4 @@
.ide-updater-dialog {
#ide-updater-dialog-container > .dialogBlock {
width: 546px;
}
@@ -10,6 +10,10 @@
display: flex;
}
.ide-updater-dialog--downloading {
flex: 1;
}
.ide-updater-dialog--logo-container {
margin-right: 28px;
}
@@ -23,37 +27,49 @@
.dialogContent.ide-updater-dialog
.ide-updater-dialog--content
.ide-updater-dialog--new-version-text.dialogSection {
display: flex;
flex: 1;
flex-direction: column;
margin-top: 0;
min-width: 0;
}
.ide-updater-dialog .changelog-container {
.ide-updater-dialog .changelog {
color: var(--theia-editor-foreground);
background-color: var(--theia-editor-background);
border: 1px solid var(--theia-editorWidget-border);
border-radius: 2px;
font-size: 12px;
height: 180px;
overflow: auto;
padding: 0 12px;
cursor: text;
}
.ide-updater-dialog .changelog-container a {
.dialogContent.ide-updater-dialog
.ide-updater-dialog--content
.ide-updater-dialog--new-version-text
.dialogRow.changelog-container {
align-items: flex-start;
border: 1px solid var(--theia-editorWidget-border);
border-radius: 2px;
overflow: auto;
max-height: 180px;
}
.ide-updater-dialog .changelog a {
color: var(--theia-textLink-foreground);
}
.ide-updater-dialog .changelog-container a:hover {
.ide-updater-dialog .changelog a:hover {
text-decoration: underline;
cursor: pointer;
}
.ide-updater-dialog .changelog-container code {
.ide-updater-dialog .changelog code {
background: var(--theia-textBlockQuote-background);
border-radius: 2px;
padding: 0 2px;
}
.ide-updater-dialog .changelog-container a code {
.ide-updater-dialog .changelog a code {
color: var(--theia-textLink-foreground);
}
@@ -77,3 +93,14 @@
.ide-updater-dialog .buttons-container .push {
margin-right: auto;
}
.ide-updater-dialog--content {
max-height: 100%;
overflow: hidden;
display: flex;
}
#ide-updater-dialog-container .skip-version-button {
margin-left: 79px;
margin-right: auto;
}

View File

@@ -8,13 +8,35 @@
}
.arduino-list-widget .search-bar {
margin: 0px 10px 10px 15px;
margin: 0px 10px 5px 15px;
}
.arduino-list-widget .search-bar:focus {
border-color: var(--theia-focusBorder);
}
.arduino-list-widget .filter-bar {
margin: 0px 10px 5px 15px;
}
.arduino-list-widget .filter-bar > * {
padding: 5px 5px 0px 0px;
}
.arduino-list-widget .filter-bar .filter {
display: flex;
align-items: center;
}
.arduino-list-widget .filter-bar .filter > select {
width: 120px;
}
.arduino-list-widget .filter-bar .filter-label {
display: flex;
width: 50px;
}
.filterable-list-container {
display: flex;
flex-direction: column;
@@ -22,34 +44,21 @@
height: 100%; /* This has top be 100% down to the `scrollContainer`. */
}
.filterable-list-container .items-container {
height: 100%; /* This has to be propagated down from the widget. */
position: relative; /* To fix the `top` of the vertical toolbar. */
}
.filterable-list-container .items-container > div:nth-child(odd) {
.filterable-list-container .items-container > div > div:nth-child(odd) {
background-color: var(--theia-sideBar-background);
filter: contrast(105%);
}
.filterable-list-container .items-container > div:nth-child(even) {
.filterable-list-container .items-container > div > div:nth-child(even) {
background-color: var(--theia-sideBar-background);
filter: contrast(95%);
}
.filterable-list-container .items-container > div:hover {
.filterable-list-container .items-container > div > div:hover {
background-color: var(--theia-sideBar-background);
filter: contrast(90%);
}
/* Perfect scrollbar does not like if we explicitly set the `background-color` of the contained elements.
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
https://github.com/arduino/arduino-pro-ide/issues/82 */
.arduino-list-widget .filterable-list-container .items-container .ps__rail-y {
z-index: 1;
}
.component-list-item {
padding: 10px 10px 10px 15px;
font-size: var(--theia-ui-font-size1);
@@ -113,7 +122,7 @@ https://github.com/arduino/arduino-pro-ide/issues/82 */
.component-list-item[min-width~="170px"] .footer {
padding: 5px 5px 0px 0px;
min-height: 30px;
min-height: 35px;
display: flex;
flex-direction: row-reverse;
}
@@ -122,10 +131,6 @@ https://github.com/arduino/arduino-pro-ide/issues/82 */
flex-direction: column-reverse;
}
.component-list-item .footer > * {
display: none
}
.component-list-item:hover .footer > * {
display: inline-block;
margin: 5px 0px 0px 10px;
@@ -155,4 +160,4 @@ https://github.com/arduino/arduino-pro-ide/issues/82 */
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
border: 1px solid var(--theia-button-border);
}
}

View File

@@ -21,6 +21,7 @@
display: flex;
align-items: center;
white-space: nowrap;
flex-wrap: wrap;
}
.arduino-settings-dialog .with-margin {

View File

@@ -12,7 +12,7 @@
display: none;
flex-direction: column;
position: absolute;
right: 0px;
right: 14px;
top: 50%;
transform: translate(0px, -50%);
height: calc(100% - 4px);

View File

@@ -1,73 +1,30 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { EditorWidget } from '@theia/editor/lib/browser';
import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import {
ConnectionStatusService,
ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service';
import {
ApplicationShell as TheiaApplicationShell,
DockPanel,
DockPanelRenderer as TheiaDockPanelRenderer,
Panel,
SaveOptions,
SHELL_TABBAR_CONTEXT_MENU,
TabBar,
Widget,
SHELL_TABBAR_CONTEXT_MENU,
} from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../../common/protocol/sketches-service-client-impl';
import { nls } from '@theia/core/lib/common';
import URI from '@theia/core/lib/common/uri';
ConnectionStatus,
ConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import { nls } from '@theia/core/lib/common/nls';
import { MessageService } from '@theia/core/lib/common/message-service';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ToolbarAwareTabBar } from './tab-bars';
@injectable()
export class ApplicationShell extends TheiaApplicationShell {
@inject(CommandService)
private readonly commandService: CommandService;
@inject(MessageService)
private readonly messageService: MessageService;
@inject(SketchesServiceClientImpl)
private readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(ConnectionStatusService)
private readonly connectionStatusService: ConnectionStatusService;
protected override track(widget: Widget): void {
super.track(widget);
if (widget instanceof OutputWidget) {
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 (CurrentSketch.isValid(sketch)) {
if (!this.isSketchFile(widget.editor.uri, sketch.uri)) {
return;
}
if (Sketch.isInSketch(widget.editor.uri, sketch)) {
widget.title.closable = false;
}
}
});
}
}
private isSketchFile(uri: URI, sketchUriString: string): boolean {
const sketchUri = new URI(sketchUriString);
if (uri.parent.isEqual(sketchUri)) {
return true;
}
return false;
}
override async addWidget(
widget: Widget,
options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
@@ -106,7 +63,7 @@ export class ApplicationShell extends TheiaApplicationShell {
return topPanel;
}
override async saveAll(): Promise<void> {
override async saveAll(options?: SaveOptions): Promise<void> {
if (
this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE
) {
@@ -118,12 +75,7 @@ export class ApplicationShell extends TheiaApplicationShell {
);
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
);
return super.saveAll(options);
}
}

View File

@@ -5,6 +5,7 @@ import {
CommonCommands,
} from '@theia/core/lib/browser/common-frontend-contribution';
import { CommandRegistry } from '@theia/core/lib/common/command';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application';
@injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
@@ -48,4 +49,9 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
registry.unregisterMenuAction(command);
}
}
override onWillStop(): OnWillStopAction | undefined {
// This is NOOP here. All window close and app quit requests are handled in the `Close` contribution.
return undefined;
}
}

View File

@@ -1,11 +1,78 @@
import type { MaybePromise } from '@theia/core';
import type { Widget } from '@theia/core/lib/browser';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { injectable } from '@theia/core/shared/inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { EditorWidget } from '@theia/editor/lib/browser';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import deepEqual = require('deep-equal');
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class WidgetManager extends TheiaWidgetManager {
@inject(SketchesServiceClientImpl)
private readonly sketchesServiceClient: SketchesServiceClientImpl;
@postConstruct()
protected init(): void {
this.sketchesServiceClient.onCurrentSketchDidChange((sketch) =>
this.maybeSetWidgetUncloseable(
sketch,
...Array.from(this.widgets.values())
)
);
}
override getOrCreateWidget<T extends Widget>(
factoryId: string,
options?: unknown
): Promise<T> {
const unresolvedWidget = super.getOrCreateWidget<T>(factoryId, options);
unresolvedWidget.then(async (widget) => {
const sketch = await this.sketchesServiceClient.currentSketch();
this.maybeSetWidgetUncloseable(sketch, widget);
});
return unresolvedWidget;
}
private maybeSetWidgetUncloseable(
sketch: CurrentSketch,
...widgets: Widget[]
): void {
const sketchFileUris =
CurrentSketch.isValid(sketch) &&
new Set([sketch.mainFileUri, ...sketch.rootFolderFileUris]);
for (const widget of widgets) {
if (widget instanceof OutputWidget) {
this.setWidgetUncloseable(widget); // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
} else if (widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
const uri = widget.editor.uri.toString();
if (!!sketchFileUris && sketchFileUris.has(uri)) {
this.setWidgetUncloseable(widget);
}
}
}
}
private setWidgetUncloseable(widget: Widget): void {
const { title } = widget;
if (title.closable) {
title.closable = false;
}
// Show the dirty indicator on uncloseable widgets when hovering over the title. Instead of showing the `X` for close.
const uncloseableClass = 'a-mod-uncloseable';
if (!title.className.includes(uncloseableClass)) {
title.className += title.className + ` ${uncloseableClass}`;
}
}
/**
* Customized to find any existing widget based on `options` deepEquals instead of string equals.
* See https://github.com/eclipse-theia/theia/issues/11309.

View File

@@ -0,0 +1,12 @@
import { MenuModelRegistry } from '@theia/core';
import { CommonCommands } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
@injectable()
export class EditorMenuContribution extends TheiaEditorMenuContribution {
override registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
registry.unregisterMenuAction(CommonCommands.CLOSE_MAIN_TAB.id);
}
}

View File

@@ -1,9 +1,10 @@
import { injectable } from '@theia/core/shared/inversify';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import {
import type {
Message,
ProgressMessage,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
import { injectable } from '@theia/core/shared/inversify';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
@injectable()
@@ -34,7 +35,9 @@ export class NotificationManager extends TheiaNotificationManager {
this.fireUpdatedEvent();
}
protected override toPlainProgress(update: ProgressUpdate): number | undefined {
protected override toPlainProgress(
update: ProgressUpdate
): number | undefined {
if (!update.work) {
return undefined;
}
@@ -43,4 +46,11 @@ export class NotificationManager extends TheiaNotificationManager {
}
return Math.min((update.work.done / update.work.total) * 100, 100);
}
/**
* For `public` visibility.
*/
override getMessageId(message: Message): string {
return super.getMessageId(message);
}
}

View File

@@ -0,0 +1,24 @@
import { injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { OutputEditorFactory as TheiaOutputEditorFactory } from '@theia/output/lib/browser/output-editor-factory';
@injectable()
export class OutputEditorFactory extends TheiaOutputEditorFactory {
protected override createOptions(
model: MonacoEditorModel,
defaultOptions: MonacoEditor.IOptions
): MonacoEditor.IOptions {
const options = super.createOptions(model, defaultOptions);
return {
...options,
// Taken from https://github.com/microsoft/vscode/blob/35b971c92d210face8c446a1c6f1e470ad2bcb54/src/vs/workbench/contrib/output/browser/outputView.ts#L211-L214
// To fix https://github.com/arduino/arduino-ide/issues/1210
unicodeHighlight: {
nonBasicASCII: false,
invisibleCharacters: false,
ambiguousCharacters: false,
},
};
}
}

View File

@@ -1,13 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import { Message, Widget } from '@theia/core/lib/browser';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
// Patched after https://github.com/eclipse-theia/theia/issues/8361
// Remove this module after ATL-222 and the Theia update.
@injectable()
export class OutputWidget extends TheiaOutputWidget {
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.onResize(Widget.ResizeMessage.UnknownSize);
}
}

View File

@@ -0,0 +1,19 @@
import { injectable } from '@theia/core/shared/inversify';
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
@injectable()
export class PreferencesEditorWidget extends TheiaPreferencesEditorWidget {
protected override resetScroll(
nodeIDToScrollTo?: string,
filterWasCleared = false
): void {
if (this.scrollBar) {
// Absent on widget creation
this.doResetScroll(nodeIDToScrollTo, filterWasCleared);
} else {
// NOOP
// Unlike Theia, IDE2 does not start multiple tasks to check if the scrollbar is ready to reset it.
// If the "scroll reset" request arrived before the existence of the scrollbar, what to reset?
}
}
}

View File

@@ -1,8 +0,0 @@
import * as monaco from '@theia/monaco-editor-core';
export function fullRange(model: monaco.editor.ITextModel): monaco.Range {
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
const end = new monaco.Position(lastLine, lastLineMaxColumn);
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
}

View File

@@ -14,11 +14,38 @@ export class ComponentListItem<
)[0];
this.state = {
selectedVersion: version,
focus: false,
};
}
}
protected async install(item: T): Promise<void> {
override componentDidUpdate(
prevProps: ComponentListItem.Props<T>,
prevState: ComponentListItem.State
): void {
if (this.state.focus !== prevState.focus) {
this.props.onFocusDidChange();
}
}
override render(): React.ReactNode {
const { item, itemRenderer } = this.props;
return (
<div
onMouseEnter={() => this.setState({ focus: true })}
onMouseLeave={() => this.setState({ focus: false })}
>
{itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
)}
</div>
);
}
private async install(item: T): Promise<void> {
const toInstall = this.state.selectedVersion;
const version = this.props.item.availableVersions.filter(
(version) => version !== this.state.selectedVersion
@@ -35,23 +62,13 @@ export class ComponentListItem<
}
}
protected async uninstall(item: T): Promise<void> {
private async uninstall(item: T): Promise<void> {
await this.props.uninstall(item);
}
protected onVersionChange(version: Installable.Version) {
private onVersionChange(version: Installable.Version): void {
this.setState({ selectedVersion: version });
}
override render(): React.ReactNode {
const { item, itemRenderer } = this.props;
return itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
);
}
}
export namespace ComponentListItem {
@@ -60,9 +77,11 @@ export namespace ComponentListItem {
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>;
readonly onFocusDidChange: () => void;
}
export interface State {
selectedVersion?: Installable.Version;
focus: boolean;
}
}

View File

@@ -1,43 +1,147 @@
import 'react-virtualized/styles.css';
import * as React from '@theia/core/shared/react';
import { Installable } from '../../../common/protocol/installable';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import {
CellMeasurer,
CellMeasurerCache,
} from 'react-virtualized/dist/commonjs/CellMeasurer';
import type {
ListRowProps,
ListRowRenderer,
} from 'react-virtualized/dist/commonjs/List';
import List from 'react-virtualized/dist/commonjs/List';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { Installable } from '../../../common/protocol/installable';
import { ComponentListItem } from './component-list-item';
import { ListItemRenderer } from './list-item-renderer';
function sameAs<T>(
left: T[],
right: T[],
...compareProps: (keyof T)[]
): boolean {
if (left === right) {
return true;
}
const leftLength = left.length;
if (leftLength !== right.length) {
return false;
}
for (let i = 0; i < leftLength; i++) {
for (const prop of compareProps) {
const leftValue = left[i][prop];
const rightValue = right[i][prop];
if (leftValue !== rightValue) {
return false;
}
}
}
return true;
}
export class ComponentList<T extends ArduinoComponent> extends React.Component<
ComponentList.Props<T>
> {
protected container?: HTMLElement;
private readonly cache: CellMeasurerCache;
private resizeAllFlag: boolean;
private list: List | undefined;
private mostRecentWidth: number | undefined;
constructor(props: ComponentList.Props<T>) {
super(props);
this.cache = new CellMeasurerCache({
defaultHeight: 140,
fixedWidth: true,
});
}
override render(): React.ReactNode {
return (
<div className={'items-container'} ref={this.setRef}>
{this.props.items.map((item) => this.createItem(item))}
</div>
<AutoSizer>
{({ width, height }) => {
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
this.resizeAllFlag = true;
setTimeout(() => this.clearAll(), 0);
}
this.mostRecentWidth = width;
return (
<List
className={'items-container'}
rowRenderer={this.createItem}
height={height}
width={width}
rowCount={this.props.items.length}
rowHeight={this.cache.rowHeight}
deferredMeasurementCache={this.cache}
ref={this.setListRef}
estimatedRowSize={140}
// If default value, then `react-virtualized` will optimize and list item will not receive a `:hover` event.
// Hence, install and version `<select>` won't be visible even if the mouse cursor is over the `<div>`.
// See https://github.com/bvaughn/react-virtualized/blob/005be24a608add0344284053dae7633be86053b2/source/Grid/Grid.js#L38-L42
scrollingResetTimeInterval={0}
/>
);
}}
</AutoSizer>
);
}
override componentDidMount(): void {
if (this.container && this.props.resolveContainer) {
this.props.resolveContainer(this.container);
override componentDidUpdate(prevProps: ComponentList.Props<T>): void {
if (
this.resizeAllFlag ||
!sameAs(this.props.items, prevProps.items, 'name', 'installedVersion')
) {
this.clearAll(true);
}
}
protected setRef = (element: HTMLElement | null) => {
this.container = element || undefined;
private readonly setListRef = (ref: List | null): void => {
this.list = ref || undefined;
};
protected createItem(item: T): React.ReactNode {
return (
<ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall}
/>
);
private clearAll(scrollToTop = false): void {
this.resizeAllFlag = false;
this.cache.clearAll();
if (this.list) {
this.list.recomputeRowHeights();
if (scrollToTop) {
this.list.scrollToPosition(0);
}
}
}
private readonly createItem: ListRowRenderer = ({
index,
parent,
key,
style,
}: ListRowProps): React.ReactNode => {
const item = this.props.items[index];
return (
<CellMeasurer
cache={this.cache}
columnIndex={0}
key={key}
rowIndex={index}
parent={parent}
>
{({ measure, registerChild }) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<div ref={registerChild} style={style}>
<ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall}
onFocusDidChange={() => measure()}
/>
</div>
)}
</CellMeasurer>
);
};
}
export namespace ComponentList {
@@ -48,6 +152,5 @@ export namespace ComponentList {
readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly resolveContainer: (element: HTMLElement) => void;
}
}

View File

@@ -0,0 +1,121 @@
import { injectable } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import {
BoardSearch,
LibrarySearch,
Searchable,
} from '../../../common/protocol';
@injectable()
export abstract class FilterRenderer<S extends Searchable.Options> {
render(
options: S,
handlePropChange: (prop: keyof S, value: S[keyof S]) => void
): React.ReactNode {
const props = this.props();
return (
<div className="filter-bar">
{Object.entries(options)
.filter(([prop]) => props.includes(prop as keyof S))
.map(([prop, value]) => (
<div key={prop} className="filter">
<div className="filter-label">
{`${this.propertyLabel(prop as keyof S)}:`}
</div>
<select
className="theia-select"
value={value}
onChange={(event) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handlePropChange(prop as keyof S, event.target.value as any)
}
>
{this.options(prop as keyof S).map((key) => (
<option key={key} value={key}>
{this.valueLabel(prop as keyof S, key)}
</option>
))}
</select>
</div>
))}
</div>
);
}
protected abstract props(): (keyof S)[];
protected abstract options(prop: keyof S): string[];
protected abstract valueLabel(prop: keyof S, key: string): string;
protected abstract propertyLabel(prop: keyof S): string;
}
@injectable()
export class BoardsFilterRenderer extends FilterRenderer<BoardSearch> {
protected props(): (keyof BoardSearch)[] {
return ['type'];
}
protected options(prop: keyof BoardSearch): string[] {
switch (prop) {
case 'type':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return BoardSearch.TypeLiterals as any;
default:
throw new Error(`Unexpected prop: ${prop}`);
}
}
protected valueLabel(prop: keyof BoardSearch, key: string): string {
switch (prop) {
case 'type':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (BoardSearch.TypeLabels as any)[key];
default:
throw new Error(`Unexpected key: ${prop}`);
}
}
protected propertyLabel(prop: keyof BoardSearch): string {
switch (prop) {
case 'type':
return BoardSearch.PropertyLabels[prop];
default:
throw new Error(`Unexpected key: ${prop}`);
}
}
}
@injectable()
export class LibraryFilterRenderer extends FilterRenderer<LibrarySearch> {
protected props(): (keyof LibrarySearch)[] {
return ['type', 'topic'];
}
protected options(prop: keyof LibrarySearch): string[] {
switch (prop) {
case 'type':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return LibrarySearch.TypeLiterals as any;
case 'topic':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return LibrarySearch.TopicLiterals as any;
default:
throw new Error(`Unexpected prop: ${prop}`);
}
}
protected propertyLabel(prop: keyof LibrarySearch): string {
switch (prop) {
case 'type':
case 'topic':
return LibrarySearch.PropertyLabels[prop];
default:
throw new Error(`Unexpected key: ${prop}`);
}
}
protected valueLabel(prop: keyof LibrarySearch, key: string): string {
switch (prop) {
case 'type':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (LibrarySearch.TypeLabels as any)[key] as any;
case 'topic':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (LibrarySearch.TopicLabels as any)[key] as any;
default:
throw new Error(`Unexpected prop: ${prop}`);
}
}
}

View File

@@ -14,25 +14,30 @@ import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
import { ResponseServiceClient } from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';
import { FilterRenderer } from './filter-renderer';
export class FilterableListContainer<
T extends ArduinoComponent
T extends ArduinoComponent,
S extends Searchable.Options
> extends React.Component<
FilterableListContainer.Props<T>,
FilterableListContainer.State<T>
FilterableListContainer.Props<T, S>,
FilterableListContainer.State<T, S>
> {
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
super(props);
this.state = {
filterText: '',
searchOptions: props.defaultSearchOptions,
items: [],
};
}
override componentDidMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
this.search(this.state.searchOptions);
this.props.searchOptionsDidChange((newSearchOptions) => {
const { searchOptions } = this.state;
this.setSearchOptionsAndUpdate({ ...searchOptions, ...newSearchOptions });
});
}
override componentDidUpdate(): void {
@@ -44,30 +49,40 @@ export class FilterableListContainer<
override render(): React.ReactNode {
return (
<div className={'filterable-list-container'}>
{this.renderSearchFilter()}
{this.renderSearchBar()}
{this.renderComponentList()}
{this.renderSearchFilter()}
<div className="filterable-list-container">
{this.renderComponentList()}
</div>
</div>
);
}
protected renderSearchFilter(): React.ReactNode {
return undefined;
return (
<>
{this.props.filterRenderer.render(
this.state.searchOptions,
this.handlePropChange.bind(this)
)}
</>
);
}
protected renderSearchBar(): React.ReactNode {
return (
<SearchBar
resolveFocus={this.props.resolveFocus}
filterText={this.state.filterText}
onFilterTextChanged={this.handleFilterTextChange}
filterText={this.state.searchOptions.query ?? ''}
onFilterTextChanged={(query) =>
this.handlePropChange('query', query as S['query'])
}
/>
);
}
protected renderComponentList(): React.ReactNode {
const { itemLabel, itemDeprecated, resolveContainer, itemRenderer } =
this.props;
const { itemLabel, itemDeprecated, itemRenderer } = this.props;
return (
<ComponentList<T>
items={this.state.items}
@@ -76,22 +91,26 @@ export class FilterableListContainer<
itemRenderer={itemRenderer}
install={this.install.bind(this)}
uninstall={this.uninstall.bind(this)}
resolveContainer={resolveContainer}
/>
);
}
protected handleFilterTextChange = (
filterText: string = this.state.filterText
) => {
this.setState({ filterText });
this.search(filterText);
protected handlePropChange = (prop: keyof S, value: S[keyof S]): void => {
const searchOptions = {
...this.state.searchOptions,
[prop]: value,
};
this.setSearchOptionsAndUpdate(searchOptions);
};
protected search(query: string): void {
private setSearchOptionsAndUpdate(searchOptions: S) {
this.setState({ searchOptions }, () => this.search(searchOptions));
}
protected search(searchOptions: S): void {
const { searchable } = this.props;
searchable
.search({ query: query.trim() })
.search(searchOptions)
.then((items) => this.setState({ items: this.sort(items) }));
}
@@ -119,7 +138,7 @@ export class FilterableListContainer<
` ${item.name}:${version}`,
run: ({ progressId }) => install({ item, progressId, version }),
});
const items = await searchable.search({ query: this.state.filterText });
const items = await searchable.search(this.state.searchOptions);
this.setState({ items: this.sort(items) });
}
@@ -147,21 +166,25 @@ export class FilterableListContainer<
}`,
run: ({ progressId }) => uninstall({ item, progressId }),
});
const items = await searchable.search({ query: this.state.filterText });
const items = await searchable.search(this.state.searchOptions);
this.setState({ items: this.sort(items) });
}
}
export namespace FilterableListContainer {
export interface Props<T extends ArduinoComponent> {
readonly container: ListWidget<T>;
readonly searchable: Searchable<T>;
export interface Props<
T extends ArduinoComponent,
S extends Searchable.Options
> {
readonly defaultSearchOptions: S;
readonly container: ListWidget<T, S>;
readonly searchable: Searchable<T, S>;
readonly itemLabel: (item: T) => string;
readonly itemDeprecated: (item: T) => boolean;
readonly itemRenderer: ListItemRenderer<T>;
readonly resolveContainer: (element: HTMLElement) => void;
readonly filterRenderer: FilterRenderer<S>;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string | undefined>;
readonly searchOptionsDidChange: Event<Partial<S> | undefined>;
readonly messageService: MessageService;
readonly responseService: ResponseServiceClient;
readonly install: ({
@@ -183,8 +206,8 @@ export namespace FilterableListContainer {
readonly commandService: CommandService;
}
export interface State<T> {
filterText: string;
export interface State<T, S extends Searchable.Options> {
searchOptions: S;
items: T[];
}
}

View File

@@ -5,6 +5,7 @@ import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
import { nls } from '@theia/core/lib/common';
import { Unknown } from '../../../common/nls';
@injectable()
export class ListItemRenderer<T extends ArduinoComponent> {
@@ -13,7 +14,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
protected onMoreInfoClick = (
event: React.SyntheticEvent<HTMLAnchorElement, Event>
) => {
): void => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
@@ -27,7 +28,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void
): React.ReactNode {
const { item } = input;
const { item, focus } = input;
let nameAndAuthor: JSX.Element;
if (item.name && item.author) {
const name = <span className="name">{item.name}</span>;
@@ -42,11 +43,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
} else if ((item as any).id) {
nameAndAuthor = <span className="name">{(item as any).id}</span>;
} else {
nameAndAuthor = (
<span className="name">
{nls.localize('arduino/common/unknown', 'Unknown')}
</span>
);
nameAndAuthor = <span className="name">{Unknown}</span>;
}
const onClickUninstall = () => uninstall(item);
const installedVersion = !!item.installedVersion && (
@@ -123,10 +120,12 @@ export class ListItemRenderer<T extends ArduinoComponent> {
{description}
</div>
<div className="info">{moreInfo}</div>
<div className="footer">
{versions}
{installButton}
</div>
{focus && (
<div className="footer">
{versions}
{installButton}
</div>
)}
</div>
);
}

View File

@@ -3,13 +3,20 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListWidget } from './list-widget';
import { Searchable } from '../../../common/protocol';
@injectable()
export abstract class ListWidgetFrontendContribution<T extends ArduinoComponent>
extends AbstractViewContribution<ListWidget<T>>
export abstract class ListWidgetFrontendContribution<
T extends ArduinoComponent,
S extends Searchable.Options
>
extends AbstractViewContribution<ListWidget<T, S>>
implements FrontendApplicationContribution
{
async initializeLayout(): Promise<void> {}
async initializeLayout(): Promise<void> {
// TS requires at least one method from `FrontendApplicationContribution`.
// Expected to be empty.
}
override registerMenus(): void {
// NOOP

View File

@@ -6,9 +6,7 @@ import {
} from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
@@ -21,10 +19,12 @@ import {
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
import { NotificationCenter } from '../../notification-center';
import { FilterRenderer } from './filter-renderer';
@injectable()
export abstract class ListWidget<
T extends ArduinoComponent
T extends ArduinoComponent,
S extends Searchable.Options
> extends ReactWidget {
@inject(MessageService)
protected readonly messageService: MessageService;
@@ -42,9 +42,8 @@ export abstract class ListWidget<
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
protected readonly filterTextChangeEmitter = new Emitter<
string | undefined
protected readonly searchOptionsChangeEmitter = new Emitter<
Partial<S> | undefined
>();
/**
* Instead of running an `update` from the `postConstruct` `init` method,
@@ -52,7 +51,7 @@ export abstract class ListWidget<
*/
protected firstActivate = true;
constructor(protected options: ListWidget.Options<T>) {
constructor(protected options: ListWidget.Options<T, S>) {
super();
const { id, label, iconClass } = options;
this.id = id;
@@ -62,10 +61,8 @@ export abstract class ListWidget<
this.title.closable = true;
this.addClass('arduino-list-widget');
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = {
suppressScrollX: true,
};
this.toDispose.push(this.filterTextChangeEmitter);
this.scrollOptions = undefined;
this.toDispose.push(this.searchOptionsChangeEmitter);
}
@postConstruct()
@@ -77,10 +74,6 @@ export abstract class ListWidget<
]);
}
protected override getScrollContainer(): MaybePromise<HTMLElement> {
return this.deferredContainer.promise;
}
protected override onAfterShow(message: Message): void {
this.maybeUpdateOnFirstRender();
super.onAfterShow(message);
@@ -109,7 +102,7 @@ export abstract class ListWidget<
this.updateScrollBar();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
protected onFocusResolved = (element: HTMLElement | undefined): void => {
this.focusNode = element;
};
@@ -137,9 +130,9 @@ export abstract class ListWidget<
render(): React.ReactNode {
return (
<FilterableListContainer<T>
<FilterableListContainer<T, S>
defaultSearchOptions={this.options.defaultSearchOptions}
container={this}
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
searchable={this.options.searchable}
install={this.install.bind(this)}
@@ -147,7 +140,8 @@ export abstract class ListWidget<
itemLabel={this.options.itemLabel}
itemDeprecated={this.options.itemDeprecated}
itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}
filterRenderer={this.options.filterRenderer}
searchOptionsDidChange={this.searchOptionsChangeEmitter.event}
messageService={this.messageService}
commandService={this.commandService}
responseService={this.responseService}
@@ -159,10 +153,8 @@ export abstract class ListWidget<
* If `filterText` is defined, sets the filter text to the argument.
* If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
*/
refresh(filterText: string | undefined): void {
this.deferredContainer.promise.then(() =>
this.filterTextChangeEmitter.fire(filterText)
);
refresh(searchOptions: Partial<S> | undefined): void {
this.searchOptionsChangeEmitter.fire(searchOptions);
}
updateScrollBar(): void {
@@ -173,14 +165,19 @@ export abstract class ListWidget<
}
export namespace ListWidget {
export interface Options<T extends ArduinoComponent> {
export interface Options<
T extends ArduinoComponent,
S extends Searchable.Options
> {
readonly id: string;
readonly label: string;
readonly iconClass: string;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly searchable: Searchable<T, S>;
readonly itemLabel: (item: T) => string;
readonly itemDeprecated: (item: T) => boolean;
readonly itemRenderer: ListItemRenderer<T>;
readonly filterRenderer: FilterRenderer<S>;
readonly defaultSearchOptions: S;
}
}

View File

@@ -0,0 +1,21 @@
import { nls } from '@theia/core/lib/common/nls';
export const Unknown = nls.localize('arduino/common/unknown', 'Unknown');
export const Later = nls.localize('arduino/common/later', 'Later');
export const Updatable = nls.localize('arduino/common/updateable', 'Updatable');
export const All = nls.localize('arduino/common/all', 'All');
export const Type = nls.localize('arduino/common/type', 'Type');
export const Partner = nls.localize('arduino/common/partner', 'Partner');
export const Contributed = nls.localize(
'arduino/common/contributed',
'Contributed'
);
export const Recommended = nls.localize(
'arduino/common/recommended',
'Recommended'
);
export const Retired = nls.localize('arduino/common/retired', 'Retired');
export const InstallManually = nls.localize(
'arduino/common/installManually',
'Install Manually'
);

View File

@@ -7,11 +7,13 @@ export interface ArduinoComponent {
readonly summary: string;
readonly description: string;
readonly moreInfoLink?: string;
readonly availableVersions: Installable.Version[];
readonly installable: boolean;
readonly installedVersion?: Installable.Version;
/**
* This is the `Type` in IDE (1.x) UI.
*/
readonly types: string[];
}
export namespace ArduinoComponent {
export function is(arg: any): arg is ArduinoComponent {

View File

@@ -2,10 +2,12 @@ import { naturalCompare } from './../utils';
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
import { nls } from '@theia/core/lib/common/nls';
import { All, Contributed, Partner, Type, Updatable } from '../nls';
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
export namespace AvailablePorts {
export function byProtocol(
export function groupByProtocol(
availablePorts: AvailablePorts
): Map<string, AvailablePorts> {
const grouped = new Map<string, AvailablePorts>();
@@ -20,11 +22,27 @@ export namespace AvailablePorts {
}
return grouped;
}
export function split(
state: AvailablePorts
): Readonly<{ boards: Board[]; ports: Port[] }> {
const availablePorts: Port[] = [];
const attachedBoards: Board[] = [];
for (const key of Object.keys(state)) {
const [port, boards] = state[key];
availablePorts.push(port);
attachedBoards.push(...boards);
}
return {
boards: attachedBoards,
ports: availablePorts,
};
}
}
export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[]; ports: Port[] }>;
readonly newState: Readonly<{ boards: Board[]; ports: Port[] }>;
readonly uploadInProgress: boolean;
}
export namespace AttachedBoardsChangeEvent {
export function isEmpty(event: AttachedBoardsChangeEvent): boolean {
@@ -115,17 +133,7 @@ export const BoardsServicePath = '/services/boards-service';
export const BoardsService = Symbol('BoardsService');
export interface BoardsService
extends Installable<BoardsPackage>,
Searchable<BoardsPackage> {
/**
* Deprecated. `getState` should be used to correctly map a board with a port.
* @deprecated
*/
getAttachedBoards(): Promise<Board[]>;
/**
* Deprecated. `getState` should be used to correctly map a board with a port.
* @deprecated
*/
getAvailablePorts(): Promise<Port[]>;
Searchable<BoardsPackage, BoardSearch> {
getState(): Promise<AvailablePorts>;
getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined>;
getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined>;
@@ -139,29 +147,90 @@ export interface BoardsService
}): Promise<BoardUserField[]>;
}
export interface BoardSearch extends Searchable.Options {
readonly type?: BoardSearch.Type;
}
export namespace BoardSearch {
export const TypeLiterals = [
'All',
'Updatable',
'Arduino',
'Contributed',
'Arduino Certified',
'Partner',
'Arduino@Heart',
] as const;
export type Type = typeof TypeLiterals[number];
export const TypeLabels: Record<Type, string> = {
All: All,
Updatable: Updatable,
Arduino: 'Arduino',
Contributed: Contributed,
'Arduino Certified': nls.localize(
'arduino/boardsType/arduinoCertified',
'Arduino Certified'
),
Partner: Partner,
'Arduino@Heart': 'Arduino@Heart',
};
export const PropertyLabels: Record<
keyof Omit<BoardSearch, 'query'>,
string
> = {
type: Type,
};
}
export interface Port {
// id is the combination of address and protocol
// formatted like "<address>|<protocol>" used
// to univocally recognize a port
readonly id: string;
readonly address: string;
readonly addressLabel: string;
readonly protocol: string;
readonly protocolLabel: string;
readonly properties?: Record<string, string>;
}
export namespace Port {
export function is(arg: any): arg is Port {
return (
!!arg &&
'address' in arg &&
typeof arg['address'] === 'string' &&
'protocol' in arg &&
typeof arg['protocol'] === 'string'
);
export type Properties = Record<string, string>;
export namespace Properties {
export function create(
properties: [string, string][] | undefined
): Properties {
if (!properties) {
return {};
}
return properties.reduce((acc, curr) => {
const [key, value] = curr;
acc[key] = value;
return acc;
}, {} as Record<string, string>);
}
}
export function is(arg: unknown): arg is Port {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'address' in object &&
typeof object['address'] === 'string' &&
'addressLabel' in object &&
typeof object['addressLabel'] === 'string' &&
'protocol' in object &&
typeof object['protocol'] === 'string' &&
'protocolLabel' in object &&
typeof object['protocolLabel'] === 'string'
);
}
return false;
}
export function toString(port: Port): string {
return `${port.addressLabel} ${port.protocolLabel}`;
/**
* Key is the combination of address and protocol formatted like `'${address}|${protocol}'` used to uniquely identify a port.
*/
export function keyOf({ address, protocol }: Port): string {
return `${address}|${protocol}`;
}
export function toString({ addressLabel, protocolLabel }: Port): string {
return `${addressLabel} ${protocolLabel}`;
}
export function compare(left: Port, right: Port): number {
@@ -190,6 +259,32 @@ export namespace Port {
}
return false;
}
// See https://github.com/arduino/arduino-ide/commit/79ea0fa9a6ad2b01eaac22cef2f494d3b68284e6#diff-fb37f20bea00881acee3aafddb1ecefcecf41ce59845ca1510da79e918ee0837L338-L348
// See https://github.com/arduino/arduino-ide/commit/79ea0fa9a6ad2b01eaac22cef2f494d3b68284e6#diff-e42c82bb67e277cfa4598239952afd65db44dba55dc7d68df619dfccfa648279L441-L455
// See https://github.com/arduino/arduino-ide/commit/74bfdc4c56d7a1577a4e800a378c21b82c1da5f8#diff-e42c82bb67e277cfa4598239952afd65db44dba55dc7d68df619dfccfa648279L405-R424
/**
* All ports with `'serial'` or `'network'` `protocol`, or any other port `protocol` that has at least one recognized board connected to.
*/
export function visiblePorts(
boardsHaystack: ReadonlyArray<Board>
): (port: Port) => boolean {
return (port: Port) => {
if (port.protocol === 'serial' || port.protocol === 'network') {
// Allow all `serial` and `network` boards.
// IDE2 must support better label for unrecognized `network` boards: https://github.com/arduino/arduino-ide/issues/1331
return true;
}
// All other ports with different protocol are
// only shown if there is a recognized board
// connected
for (const board of boardsHaystack) {
if (board.port?.address === port.address) {
return true;
}
}
return false;
};
}
}
export interface BoardsPackage extends ArduinoComponent {

View File

@@ -1,5 +1,9 @@
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
import type {
Location,
Range,
Position,
} from '@theia/core/shared/vscode-languageserver-protocol';
import type {
BoardUserField,
Port,
@@ -15,11 +19,41 @@ export const CompilerWarningLiterals = [
] as const;
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
export namespace CoreError {
export interface ErrorLocation {
export interface ErrorLocationRef {
readonly message: string;
readonly location: Location;
readonly details?: string;
}
export namespace ErrorLocationRef {
export function equals(
left: ErrorLocationRef,
right: ErrorLocationRef
): boolean {
return (
left.message === right.message &&
left.details === right.details &&
equalsLocation(left.location, right.location)
);
}
function equalsLocation(left: Location, right: Location): boolean {
return left.uri === right.uri && equalsRange(left.range, right.range);
}
function equalsRange(left: Range, right: Range): boolean {
return (
equalsPosition(left.start, right.start) &&
equalsPosition(left.end, right.end)
);
}
function equalsPosition(left: Position, right: Position): boolean {
return left.character === right.character && left.line === right.line;
}
}
export interface ErrorLocation extends ErrorLocationRef {
/**
* The range of the error location source from the CLI output.
*/
readonly rangesInOutput: Range[]; // The same error might show up multiple times in the CLI output: https://github.com/arduino/arduino-cli/issues/1761
}
export const Codes = {
Verify: 4001,
Upload: 4002,

View File

@@ -56,16 +56,16 @@ export interface IDEUpdater extends JsonRpcServer<IDEUpdaterClient> {
export const IDEUpdaterClient = Symbol('IDEUpdaterClient');
export interface IDEUpdaterClient {
onError: Event<Error>;
onCheckingForUpdate: Event<void>;
onUpdateAvailable: Event<UpdateInfo>;
onUpdateNotAvailable: Event<UpdateInfo>;
onDownloadProgressChanged: Event<ProgressInfo>;
onDownloadFinished: Event<UpdateInfo>;
notifyError(message: Error): void;
notifyCheckingForUpdate(message: void): void;
notifyUpdateAvailable(message: UpdateInfo): void;
notifyUpdateNotAvailable(message: UpdateInfo): void;
onUpdaterDidFail: Event<Error>;
onUpdaterDidCheckForUpdate: Event<void>;
onUpdaterDidFindUpdateAvailable: Event<UpdateInfo>;
onUpdaterDidNotFindUpdateAvailable: Event<UpdateInfo>;
onDownloadProgressDidChange: Event<ProgressInfo>;
onDownloadDidFinish: Event<UpdateInfo>;
notifyUpdaterFailed(message: Error): void;
notifyCheckedForUpdate(message: void): void;
notifyUpdateAvailableFound(message: UpdateInfo): void;
notifyUpdateAvailableNotFound(message: UpdateInfo): void;
notifyDownloadProgressChanged(message: ProgressInfo): void;
notifyDownloadFinished(message: UpdateInfo): void;
}

View File

@@ -27,15 +27,56 @@ export namespace Installable {
export namespace Version {
/**
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
*
* If `coerce` is `true` tries to convert any invalid semver strings to a valid semver based on [these](https://github.com/npm/node-semver#coercion) rules.
*/
export const COMPARATOR = (left: Version, right: Version): number => {
if (semver.valid(left) && semver.valid(right)) {
return semver.compare(left, right);
export const COMPARATOR = (
left: Version,
right: Version,
coerce = false
): number => {
const validLeft = semver.parse(left);
const validRight = semver.parse(right);
if (validLeft && validRight) {
return semver.compare(validLeft, validRight);
}
if (coerce) {
const coercedLeft = validLeft ?? semver.coerce(left);
const coercedRight = validRight ?? semver.coerce(right);
if (coercedLeft && coercedRight) {
return semver.compare(coercedLeft, coercedRight);
}
}
return naturalCompare(left, right);
};
}
export const Installed = <T extends ArduinoComponent>({
installedVersion,
}: T): boolean => {
return !!installedVersion;
};
export const Updateable = <T extends ArduinoComponent>(item: T): boolean => {
const { installedVersion } = item;
if (!installedVersion) {
return false;
}
const latestVersion = item.availableVersions[0];
if (!latestVersion) {
console.warn(
`Installed version ${installedVersion} is available for ${item.name}, but no available versions were available. Skipping.`
);
return false;
}
const result = Installable.Version.COMPARATOR(
latestVersion,
installedVersion,
true
);
return result > 0;
};
export async function installWithProgress<
T extends ArduinoComponent
>(options: {
@@ -44,6 +85,7 @@ export namespace Installable {
responseService: ResponseServiceClient;
item: T;
version: Installable.Version;
keepOutput?: boolean;
}): Promise<void> {
const { item, version } = options;
return ExecuteWithProgress.doWithProgress({
@@ -65,6 +107,7 @@ export namespace Installable {
messageService: MessageService;
responseService: ResponseServiceClient;
item: T;
keepOutput?: boolean;
}): Promise<void> {
const { item } = options;
return ExecuteWithProgress.doWithProgress({

View File

@@ -1,13 +1,24 @@
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
import { nls } from '@theia/core/lib/common/nls';
import {
All,
Contributed,
Partner,
Recommended,
Retired,
Type,
Updatable,
} from '../nls';
export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService');
export interface LibraryService
extends Installable<LibraryPackage>,
Searchable<LibraryPackage> {
Searchable<LibraryPackage, LibrarySearch> {
list(options: LibraryService.List.Options): Promise<LibraryPackage[]>;
search(options: LibrarySearch): Promise<LibraryPackage[]>;
/**
* When `installDependencies` is not set, it is `true` by default. If you want to skip the installation of required dependencies, set it to `false`.
*/
@@ -17,6 +28,7 @@ export interface LibraryService
version?: Installable.Version;
installDependencies?: boolean;
noOverwrite?: boolean;
installLocation?: LibraryLocation.BUILTIN | LibraryLocation.USER;
}): Promise<void>;
installZip(options: {
zipUri: string;
@@ -38,6 +50,86 @@ export interface LibraryService
}): Promise<LibraryDependency[]>;
}
export interface LibrarySearch extends Searchable.Options {
readonly type?: LibrarySearch.Type;
readonly topic?: LibrarySearch.Topic;
}
export namespace LibrarySearch {
export const TypeLiterals = [
'All',
'Updatable',
'Installed',
'Arduino',
'Partner',
'Recommended',
'Contributed',
'Retired',
] as const;
export type Type = typeof TypeLiterals[number];
export const TypeLabels: Record<Type, string> = {
All: All,
Updatable: Updatable,
Installed: nls.localize('arduino/libraryType/installed', 'Installed'),
Arduino: 'Arduino',
Partner: Partner,
Recommended: Recommended,
Contributed: Contributed,
Retired: Retired,
};
export const TopicLiterals = [
'All',
'Communication',
'Data Processing',
'Data Storage',
'Device Control',
'Display',
'Other',
'Sensors',
'Signal Input/Output',
'Timing',
'Uncategorized',
] as const;
export type Topic = typeof TopicLiterals[number];
export const TopicLabels: Record<Topic, string> = {
All: All,
Communication: nls.localize(
'arduino/libraryTopic/communication',
'Communication'
),
'Data Processing': nls.localize(
'arduino/libraryTopic/dataProcessing',
'Data Processing'
),
'Data Storage': nls.localize(
'arduino/libraryTopic/dataStorage',
'Data Storage'
),
'Device Control': nls.localize(
'arduino/libraryTopic/deviceControl',
'Device Control'
),
Display: nls.localize('arduino/libraryTopic/display', 'Display'),
Other: nls.localize('arduino/libraryTopic/other', 'Other'),
Sensors: nls.localize('arduino/libraryTopic/sensors', 'Sensors'),
'Signal Input/Output': nls.localize(
'arduino/libraryTopic/signalInputOutput',
'Signal Input/Output'
),
Timing: nls.localize('arduino/libraryTopic/timing', 'Timing'),
Uncategorized: nls.localize(
'arduino/libraryTopic/uncategorized',
'Uncategorized'
),
};
export const PropertyLabels: Record<
keyof Omit<LibrarySearch, 'query'>,
string
> = {
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
type: Type,
};
}
export namespace LibraryService {
export namespace List {
export interface Options {
@@ -50,7 +142,7 @@ export enum LibraryLocation {
/**
* In the `libraries` subdirectory of the Arduino IDE installation.
*/
IDE_BUILTIN = 0,
BUILTIN = 0,
/**
* In the `libraries` subdirectory of the user directory (sketchbook).
@@ -85,6 +177,10 @@ export interface LibraryPackage extends ArduinoComponent {
readonly exampleUris: string[];
readonly location: LibraryLocation;
readonly installDirUri?: string;
/**
* This is the `Topic` in the IDE (1.x) UI.
*/
readonly category: string;
}
export namespace LibraryPackage {
export function is(arg: any): arg is LibraryPackage {

View File

@@ -39,7 +39,7 @@ export namespace ExecuteWithProgress {
);
}
async function withProgress<T>(
export async function withProgress<T>(
text: string,
messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<T>

View File

@@ -1,5 +1,5 @@
export interface Searchable<T> {
search(options: Searchable.Options): Promise<T[]>;
export interface Searchable<T, O extends Searchable.Options> {
search(options: O): Promise<T[]>;
}
export namespace Searchable {
export interface Options {

View File

@@ -10,7 +10,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { Sketch, SketchesService } from '../../common/protocol';
import { ConfigService } from './config-service';
import { SketchContainer, SketchRef } from './sketches-service';
import { SketchContainer, SketchesError, SketchRef } from './sketches-service';
import {
ARDUINO_CLOUD_FOLDER,
REMOTE_SKETCHBOOK_FOLDER,
@@ -79,6 +79,7 @@ export class SketchesServiceClientImpl
this.sketches.set(sketch.uri, sketch);
}
this.toDispose.push(
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
this.fileService.watch(new URI(sketchDirUri), {
recursive: true,
excludes: [],
@@ -87,6 +88,45 @@ export class SketchesServiceClientImpl
this.toDispose.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
// The file change events have higher precedence in the current sketch over the sketchbook.
if (
CurrentSketch.isValid(this._currentSketch) &&
new URI(this._currentSketch.uri).isEqualOrParent(resource)
) {
// https://github.com/arduino/arduino-ide/pull/1351#pullrequestreview-1086666656
// On a sketch file rename, the FS watcher will contain two changes:
// - Deletion of the original file,
// - Update of the new file,
// Hence, `UPDATE` events must be processed but only and if only there is a `DELETED` change in the same event.
// Otherwise, IDE2 would ask CLI to reload the sketch content on every save event in IDE2.
if (
type === FileChangeType.UPDATED &&
event.changes.length === 1
) {
// If the event contains only one `UPDATE` change, it cannot be a rename.
return;
}
let reloadedSketch: Sketch | undefined = undefined;
try {
reloadedSketch = await this.sketchService.loadSketch(
this._currentSketch.uri
);
} catch (err) {
if (!SketchesError.NotFound.is(err)) {
throw err;
}
}
if (!reloadedSketch) {
return;
}
if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) {
this.useCurrentSketch(reloadedSketch, true);
}
return;
}
// We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file.
if (sketchbookUri.isEqualOrParent(resource)) {
if (Sketch.isSketchFile(resource)) {
@@ -97,7 +137,7 @@ export class SketchesServiceClientImpl
);
if (!this.sketches.has(toAdd.uri)) {
console.log(
`New sketch '${toAdd.name}' was crated in sketchbook '${sketchDirUri}'.`
`New sketch '${toAdd.name}' was created in sketchbook '${sketchDirUri}'.`
);
this.sketches.set(toAdd.uri, toAdd);
this.fireSoon(toAdd, 'created');
@@ -125,12 +165,31 @@ export class SketchesServiceClientImpl
.reachedState('started_contributions')
.then(async () => {
const currentSketch = await this.loadCurrentSketch();
this._currentSketch = currentSketch;
this.currentSketchDidChangeEmitter.fire(this._currentSketch);
this.currentSketchLoaded.resolve(this._currentSketch);
if (CurrentSketch.isValid(currentSketch)) {
this.toDispose.pushAll([
// Watch the file changes of the current sketch
this.fileService.watch(new URI(currentSketch.uri), {
recursive: true,
excludes: [],
}),
]);
}
this.useCurrentSketch(currentSketch);
});
}
private useCurrentSketch(
currentSketch: CurrentSketch,
reassignPromise = false
) {
this._currentSketch = currentSketch;
if (reassignPromise) {
this.currentSketchLoaded = new Deferred();
}
this.currentSketchLoaded.resolve(this._currentSketch);
this.currentSketchDidChangeEmitter.fire(this._currentSketch);
}
onStop(): void {
this.toDispose.dispose();
}

View File

@@ -95,6 +95,11 @@ export interface SketchesService {
* Based on https://github.com/arduino/arduino-cli/blob/550179eefd2d2bca299d50a4af9e9bfcfebec649/arduino/builder/builder.go#L30-L38
*/
getIdeTempFolderUri(sketch: Sketch): Promise<string>;
/**
* Notifies the backend to recursively delete the sketch folder with all its content.
*/
notifyDeleteSketch(sketch: Sketch): void;
}
export interface SketchRef {
@@ -157,6 +162,74 @@ export namespace Sketch {
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
}
const primitiveProps: Array<keyof Sketch> = ['name', 'uri', 'mainFileUri'];
const arrayProps: Array<keyof Sketch> = [
'additionalFileUris',
'otherSketchFileUris',
'rootFolderFileUris',
];
export function sameAs(left: Sketch, right: Sketch): boolean {
for (const prop of primitiveProps) {
const leftValue = left[prop];
const rightValue = right[prop];
assertIsNotArray(leftValue, prop, left);
assertIsNotArray(rightValue, prop, right);
if (leftValue !== rightValue) {
return false;
}
}
for (const prop of arrayProps) {
const leftValue = left[prop];
const rightValue = right[prop];
assertIsArray(leftValue, prop, left);
assertIsArray(rightValue, prop, right);
if (leftValue.length !== rightValue.length) {
return false;
}
}
for (const prop of arrayProps) {
const leftValue = left[prop];
const rightValue = right[prop];
assertIsArray(leftValue, prop, left);
assertIsArray(rightValue, prop, right);
if (
toSortedString(leftValue as string[]) !==
toSortedString(rightValue as string[])
) {
return false;
}
}
return true;
}
function toSortedString(array: string[]): string {
return array.slice().sort().join(',');
}
function assertIsNotArray(
toTest: unknown,
prop: keyof Sketch,
object: Sketch
): void {
if (Array.isArray(toTest)) {
throw new Error(
`Expected a non-array type. Got: ${toTest}. Property was: ${prop}. Object was: ${JSON.stringify(
object
)}`
);
}
}
function assertIsArray(
toTest: unknown,
prop: keyof Sketch,
object: Sketch
): void {
if (!Array.isArray(toTest)) {
throw new Error(
`Expected an array type. Got: ${toTest}. Property was: ${prop}. Object was: ${JSON.stringify(
object
)}`
);
}
}
}
export interface SketchContainer {

View File

@@ -4,31 +4,6 @@ import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@thei
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ElectronMenuContribution } from './electron-menu-contribution';
import { nls } from '@theia/core/lib/common/nls';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dialogs from '@theia/core/lib/browser/dialogs';
Object.assign(dialogs, {
confirmExit: async () => {
const messageBoxResult = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message: nls.localize(
'theia/core/quitMessage',
'Any unsaved changes will not be saved.'
),
title: nls.localize(
'theia/core/quitTitle',
'Are you sure you want to quit?'
),
type: 'question',
buttons: [dialogs.Dialog.CANCEL, dialogs.Dialog.YES],
}
);
return messageBoxResult.response === 1;
},
});
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronMenuContribution).toSelf().inSingletonScope();

View File

@@ -16,6 +16,7 @@ import {
ElectronMainWindowServiceExt,
electronMainWindowServiceExtPath,
} from '../electron-common/electron-main-window-service-ext';
import { IsTempSketch } from '../node/is-temp-sketch';
import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl';
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
import { ElectronMainApplication } from './theia/electron-main-application';
@@ -62,4 +63,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope();
});

View File

@@ -19,13 +19,13 @@ export class IDEUpdaterImpl implements IDEUpdater {
constructor() {
this.updater.on('checking-for-update', (e) => {
this.clients.forEach((c) => c.notifyCheckingForUpdate(e));
this.clients.forEach((c) => c.notifyCheckedForUpdate(e));
});
this.updater.on('update-available', (e) => {
this.clients.forEach((c) => c.notifyUpdateAvailable(e));
this.clients.forEach((c) => c.notifyUpdateAvailableFound(e));
});
this.updater.on('update-not-available', (e) => {
this.clients.forEach((c) => c.notifyUpdateNotAvailable(e));
this.clients.forEach((c) => c.notifyUpdateAvailableNotFound(e));
});
this.updater.on('download-progress', (e) => {
this.clients.forEach((c) => c.notifyDownloadProgressChanged(e));
@@ -34,7 +34,7 @@ export class IDEUpdaterImpl implements IDEUpdater {
this.clients.forEach((c) => c.notifyDownloadFinished(e));
});
this.updater.on('error', (e) => {
this.clients.forEach((c) => c.notifyError(e));
this.clients.forEach((c) => c.notifyUpdaterFailed(e));
});
}
@@ -58,10 +58,8 @@ export class IDEUpdaterImpl implements IDEUpdater {
this.isAlreadyChecked = true;
}
const {
updateInfo,
cancellationToken,
} = await this.updater.checkForUpdates();
const { updateInfo, cancellationToken } =
await this.updater.checkForUpdates();
this.cancellationToken = cancellationToken;
if (this.updater.currentVersion.compare(updateInfo.version) === -1) {
@@ -104,7 +102,7 @@ export class IDEUpdaterImpl implements IDEUpdater {
await this.updater.downloadUpdate(this.cancellationToken);
} catch (e) {
if (e.message === 'cancelled') return;
this.clients.forEach((c) => c.notifyError(e));
this.clients.forEach((c) => c.notifyUpdaterFailed(e));
}
}

View File

@@ -1,4 +1,4 @@
import { injectable } from '@theia/core/shared/inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
app,
BrowserWindow,
@@ -22,6 +22,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util';
import * as os from '@theia/core/lib/common/os';
import { Restart } from '@theia/core/lib/electron-common/messaging/electron-messages';
import { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window';
import { IsTempSketch } from '../../node/is-temp-sketch';
app.commandLine.appendSwitch('disable-http-cache');
@@ -54,6 +55,8 @@ const APP_STARTED_WITH_CONTENT_TRACE =
@injectable()
export class ElectronMainApplication extends TheiaElectronMainApplication {
@inject(IsTempSketch)
private readonly isTempSketch: IsTempSketch;
private startup = false;
private _firstWindowId: number | undefined;
private openFilePromise = new Deferred();
@@ -176,6 +179,12 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
);
for (const workspace of workspaces) {
if (await this.isValidSketchPath(workspace.file)) {
if (this.isTempSketch.is(workspace.file)) {
console.info(
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.`
);
continue;
}
useDefault = false;
await this.openSketch(workspace);
}
@@ -405,6 +414,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
const workspaceUri = URI.file(workspace);
const bounds = window.getNormalBounds();
const now = Date.now();
// Do not try to reopen the sketch if it was temp.
// Unfortunately, IDE2 has two different logic of restoring recent sketches: the Theia default `recentworkspace.json` and there is the `recent-sketches.json`.
const file = workspaceUri.fsPath;
if (this.isTempSketch.is(file)) {
console.info(
`Ignored marking workspace as a closed sketch. The sketch was detected as temporary. Workspace URI: ${workspaceUri.toString()}.`
);
return;
}
console.log(
`Marking workspace as a closed sketch. Workspace URI: ${workspaceUri.toString()}. Date: ${now}.`
);

View File

@@ -109,6 +109,7 @@ import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { IsTempSketch } from './is-temp-sketch';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@@ -419,4 +420,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope();
});

View File

@@ -3,6 +3,7 @@ import { sha256 } from 'hash.js';
import { randomBytes } from 'crypto';
import btoa = require('btoa'); // TODO: check why we cannot
import { AuthenticationSession } from './types';
import { Unknown } from '../../common/nls';
export interface IToken {
accessToken: string; // When unable to refresh due to network problems, the access token becomes undefined
@@ -62,10 +63,10 @@ export function token2IToken(token: Token): IToken {
sessionId: parsedIdToken.sub,
scope: token.scope,
account: {
id: parsedIdToken.sub || 'unknown',
email: parsedIdToken.email || 'unknown',
nickname: parsedIdToken.nickname || 'unknown',
picture: parsedIdToken.picture || 'unknown',
id: parsedIdToken.sub || Unknown,
email: parsedIdToken.email || Unknown,
nickname: parsedIdToken.nickname || Unknown,
picture: parsedIdToken.picture || Unknown,
},
};
}

View File

@@ -1,27 +1,30 @@
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ClientDuplexStream } from '@grpc/grpc-js';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { CoreClientAware } from './core-client-provider';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { BackendApplicationContribution } from '@theia/core/lib/node';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { v4 } from 'uuid';
import { Unknown } from '../common/nls';
import {
AttachedBoardsChangeEvent,
AvailablePorts,
Board,
NotificationServiceServer,
Port,
} from '../common/protocol';
import {
BoardListWatchRequest,
BoardListWatchResponse,
DetectedPort as RpcDetectedPort,
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import {
Board,
Port,
NotificationServiceServer,
AvailablePorts,
AttachedBoardsChangeEvent,
} from '../common/protocol';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { v4 } from 'uuid';
import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { CoreClientAware } from './core-client-provider';
import { ServiceError } from './service-error';
import { BackendApplicationContribution } from '@theia/core/lib/node';
import { Deferred } from '@theia/core/lib/common/promise-util';
type Duplex = ClientDuplexStream<BoardListWatchRequest, BoardListWatchResponse>;
interface StreamWrapper extends Disposable {
@@ -54,6 +57,8 @@ export class BoardDiscovery
private readonly onStreamDidCancelEmitter = new Emitter<void>(); // when the watcher is canceled by the IDE2
private readonly toDisposeOnStopWatch = new DisposableCollection();
private uploadInProgress = false;
/**
* Keys are the `address` of the ports.
*
@@ -123,6 +128,10 @@ export class BoardDiscovery
});
}
setUploadInProgress(uploadInProgress: boolean): void {
this.uploadInProgress = uploadInProgress;
}
private createTimeout(
after: number,
onTimeout: (error: Error) => void
@@ -228,102 +237,7 @@ export class BoardDiscovery
this.logger.info('start new deferred');
const { client, instance } = await this.coreClient;
const wrapper = await this.createWrapper(client);
wrapper.stream.on('data', async (resp: BoardListWatchResponse) => {
this.logger.info('onData', this.toJson(resp));
if (resp.getEventType() === 'quit') {
this.logger.info('quit received');
this.stop();
return;
}
const detectedPort = resp.getPort();
if (detectedPort) {
let eventType: 'add' | 'remove' | 'unknown' = 'unknown';
if (resp.getEventType() === 'add') {
eventType = 'add';
} else if (resp.getEventType() === 'remove') {
eventType = 'remove';
} else {
eventType = 'unknown';
}
if (eventType === 'unknown') {
throw new Error(`Unexpected event type: '${resp.getEventType()}'`);
}
const oldState = deepClone(this._availablePorts);
const newState = deepClone(this._availablePorts);
const address = (detectedPort as any).getPort().getAddress();
const protocol = (detectedPort as any).getPort().getProtocol();
// Different discoveries can detect the same port with different
// protocols, so we consider the combination of address and protocol
// to be the id of a certain port to distinguish it from others.
// If we'd use only the address of a port to store it in a map
// we can have conflicts the same port is found with multiple
// protocols.
const portID = `${address}|${protocol}`;
const label = (detectedPort as any).getPort().getLabel();
const protocolLabel = (detectedPort as any)
.getPort()
.getProtocolLabel();
const port = {
id: portID,
address,
addressLabel: label,
protocol,
protocolLabel,
};
const boards: Board[] = [];
for (const item of detectedPort.getMatchingBoardsList()) {
boards.push({
fqbn: item.getFqbn(),
name: item.getName() || 'unknown',
port,
});
}
if (eventType === 'add') {
if (newState[portID]) {
const [, knownBoards] = newState[portID];
this.logger.warn(
`Port '${Port.toString(
port
)}' was already available. Known boards before override: ${JSON.stringify(
knownBoards
)}`
);
}
newState[portID] = [port, boards];
} else if (eventType === 'remove') {
if (!newState[portID]) {
this.logger.warn(
`Port '${Port.toString(port)}' was not available. Skipping`
);
return;
}
delete newState[portID];
}
const oldAvailablePorts = this.getAvailablePorts(oldState);
const oldAttachedBoards = this.getAttachedBoards(oldState);
const newAvailablePorts = this.getAvailablePorts(newState);
const newAttachedBoards = this.getAttachedBoards(newState);
const event: AttachedBoardsChangeEvent = {
oldState: {
ports: oldAvailablePorts,
boards: oldAttachedBoards,
},
newState: {
ports: newAvailablePorts,
boards: newAttachedBoards,
},
};
this._availablePorts = newState;
this.notificationService.notifyAttachedBoardsDidChange(event);
}
});
wrapper.stream.on('data', (resp) => this.onBoardListWatchResponse(resp));
this.logger.info('start request start watch');
await this.requestStartWatch(
new BoardListWatchRequest().setInstance(instance),
@@ -334,21 +248,124 @@ export class BoardDiscovery
this.logger.info('start resolved watching');
}
getAttachedBoards(state: AvailablePorts = this.availablePorts): Board[] {
const attachedBoards: Board[] = [];
for (const portID of Object.keys(state)) {
const [, boards] = state[portID];
attachedBoards.push(...boards);
// XXX: make this `protected` and override for tests if IDE2 wants to mock events from the CLI.
private onBoardListWatchResponse(resp: BoardListWatchResponse): void {
this.logger.info(this.toJson(resp));
const eventType = EventType.parse(resp.getEventType());
if (eventType === EventType.Quit) {
this.logger.info('quit received');
this.stop();
return;
}
const detectedPort = resp.getPort();
if (detectedPort) {
const { port, boards } = this.fromRpc(detectedPort);
if (!port) {
if (!!boards.length) {
console.warn(
`Could not detect the port, but unexpectedly received discovered boards. This is most likely a bug! Response was: ${this.toJson(
resp
)}`
);
}
return;
}
const oldState = deepClone(this._availablePorts);
const newState = deepClone(this._availablePorts);
const key = Port.keyOf(port);
if (eventType === EventType.Add) {
if (newState[key]) {
const [, knownBoards] = newState[key];
this.logger.warn(
`Port '${Port.toString(
port
)}' was already available. Known boards before override: ${JSON.stringify(
knownBoards
)}`
);
}
newState[key] = [port, boards];
} else if (eventType === EventType.Remove) {
if (!newState[key]) {
this.logger.warn(
`Port '${Port.toString(port)}' was not available. Skipping`
);
return;
}
delete newState[key];
}
const event: AttachedBoardsChangeEvent = {
oldState: {
...AvailablePorts.split(oldState),
},
newState: {
...AvailablePorts.split(newState),
},
uploadInProgress: this.uploadInProgress,
};
this._availablePorts = newState;
this.notificationService.notifyAttachedBoardsDidChange(event);
}
return attachedBoards;
}
getAvailablePorts(state: AvailablePorts = this.availablePorts): Port[] {
const availablePorts: Port[] = [];
for (const portID of Object.keys(state)) {
const [port] = state[portID];
availablePorts.push(port);
}
return availablePorts;
private fromRpc(detectedPort: RpcDetectedPort): DetectedPort {
const rpcPort = detectedPort.getPort();
const port = rpcPort && this.fromRpcPort(rpcPort);
const boards = detectedPort.getMatchingBoardsList().map(
(board) =>
({
fqbn: board.getFqbn(),
name: board.getName() || Unknown,
port,
} as Board)
);
return {
boards,
port,
};
}
private fromRpcPort(rpcPort: RpcPort): Port {
const port = {
address: rpcPort.getAddress(),
addressLabel: rpcPort.getLabel(),
protocol: rpcPort.getProtocol(),
protocolLabel: rpcPort.getProtocolLabel(),
properties: Port.Properties.create(rpcPort.getPropertiesMap().toObject()),
};
return port;
}
}
enum EventType {
Add,
Remove,
Quit,
}
namespace EventType {
export function parse(type: string): EventType {
const normalizedType = type.toLowerCase();
switch (normalizedType) {
case 'add':
return EventType.Add;
case 'remove':
return EventType.Remove;
case 'quit':
return EventType.Quit;
default:
throw new Error(
`Unexpected 'BoardListWatchResponse' event type: '${type}.'`
);
}
}
}
interface DetectedPort {
port: Port | undefined;
boards: Board[];
}

View File

@@ -6,7 +6,6 @@ import {
Installable,
BoardsPackage,
Board,
Port,
BoardDetails,
Tool,
ConfigOption,
@@ -17,6 +16,7 @@ import {
AvailablePorts,
BoardWithPackage,
BoardUserField,
BoardSearch,
} from '../common/protocol';
import {
PlatformInstallRequest,
@@ -41,6 +41,8 @@ import {
SupportedUserFieldsResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ExecuteWithProgress } from './grpc-progressible';
import { ServiceError } from './service-error';
import { nls } from '@theia/core/lib/common';
@injectable()
export class BoardsServiceImpl
@@ -63,14 +65,6 @@ export class BoardsServiceImpl
return this.boardDiscovery.availablePorts;
}
async getAttachedBoards(): Promise<Board[]> {
return this.boardDiscovery.getAttachedBoards();
}
async getAvailablePorts(): Promise<Port[]> {
return this.boardDiscovery.getAvailablePorts();
}
async getBoardDetails(options: {
fqbn: string;
}): Promise<BoardDetails | undefined> {
@@ -84,19 +78,7 @@ export class BoardsServiceImpl
(resolve, reject) =>
client.boardDetails(detailsReq, (err, resp) => {
if (err) {
// Required cores are not installed manually: https://github.com/arduino/arduino-cli/issues/954
if (
(err.message.indexOf('missing platform release') !== -1 &&
err.message.indexOf('referenced by board') !== -1) ||
// Platform is not installed.
(err.message.indexOf('platform') !== -1 &&
err.message.indexOf('not installed') !== -1)
) {
resolve(undefined);
return;
}
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (err.message.indexOf('unknown package') !== -1) {
if (isMissingPlatformError(err)) {
resolve(undefined);
return;
}
@@ -249,29 +231,41 @@ export class BoardsServiceImpl
const coreClient = await this.coreClient;
const { client, instance } = coreClient;
const supportedUserFieldsReq = new SupportedUserFieldsRequest();
supportedUserFieldsReq.setInstance(instance);
supportedUserFieldsReq.setFqbn(options.fqbn);
supportedUserFieldsReq.setProtocol(options.protocol);
const req = new SupportedUserFieldsRequest();
req.setInstance(instance);
req.setFqbn(options.fqbn);
req.setProtocol(options.protocol);
const supportedUserFieldsResp =
await new Promise<SupportedUserFieldsResponse>((resolve, reject) => {
client.supportedUserFields(supportedUserFieldsReq, (err, resp) => {
!!err ? reject(err) : resolve(resp);
const resp = await new Promise<SupportedUserFieldsResponse | undefined>(
(resolve, reject) => {
client.supportedUserFields(req, (err, resp) => {
if (err) {
if (isMissingPlatformError(err)) {
resolve(undefined);
return;
}
reject(err);
return;
}
resolve(resp);
});
});
return supportedUserFieldsResp.getUserFieldsList().map((e) => {
return {
toolId: e.getToolId(),
name: e.getName(),
label: e.getLabel(),
secret: e.getSecret(),
value: '',
};
});
}
);
if (!resp) {
return [];
}
return resp.getUserFieldsList().map((e) => ({
toolId: e.getToolId(),
name: e.getName(),
label: e.getLabel(),
secret: e.getSecret(),
value: '',
}));
}
async search(options: { query?: string }): Promise<BoardsPackage[]> {
async search(options: BoardSearch): Promise<BoardsPackage[]> {
const coreClient = await this.coreClient;
const { client, instance } = coreClient;
@@ -317,8 +311,12 @@ export class BoardsServiceImpl
.map((b) => b.getName())
.join(', '),
installable: true,
types: platform.getTypeList(),
deprecated: platform.getDeprecated(),
summary: 'Boards included in this package:',
summary: nls.localize(
'arduino/component/boardsIncluded',
'Boards included in this package:'
),
installedVersion,
boards: platform
.getBoardsList()
@@ -384,7 +382,29 @@ export class BoardsServiceImpl
}
}
return [...packages.values()];
const filter = this.typePredicate(options);
return [...packages.values()].filter(filter);
}
private typePredicate(
options: BoardSearch
): (item: BoardsPackage) => boolean {
const { type } = options;
if (!type || type === 'All') {
return () => true;
}
switch (options.type) {
case 'Updatable':
return Installable.Updateable;
case 'Arduino':
case 'Partner':
case 'Arduino@Heart':
case 'Contributed':
case 'Arduino Certified':
return ({ types }: BoardsPackage) => !!types && types?.includes(type);
default:
throw new Error(`Unhandled type: ${options.type}`);
}
}
async install(options: {
@@ -486,3 +506,30 @@ export class BoardsServiceImpl
console.info('<<< Boards package uninstallation done.', item);
}
}
function isMissingPlatformError(error: unknown): boolean {
if (ServiceError.is(error)) {
const message = error.details;
// TODO: check gRPC status code? `9 FAILED_PRECONDITION` (https://grpc.github.io/grpc/core/md_doc_statuscodes.html)
// When installing a 3rd party core that depends on a missing Arduino core.
// https://github.com/arduino/arduino-cli/issues/954
if (
message.includes('missing platform release') &&
message.includes('referenced by board')
) {
return true;
}
// When the platform is not installed.
if (message.includes('platform') && message.includes('not installed')) {
return true;
}
// It's a hack to handle https://github.com/arduino/arduino-cli/issues/1262 gracefully.
if (message.includes('unknown package')) {
return true;
}
}
return false;
}

View File

@@ -147,189 +147,10 @@ function styleJson({
UseTab,
}: ClangFormatOptions): Record<string, unknown> {
// Source: https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration
const defaultConfig = require('../../src/node/default-formatter-config.json');
return {
AccessModifierOffset: -2,
AlignAfterOpenBracket: 'Align',
AlignArrayOfStructures: 'None',
AlignConsecutiveAssignments: 'None',
AlignConsecutiveBitFields: 'None',
AlignConsecutiveDeclarations: 'None',
AlignConsecutiveMacros: 'None',
AlignEscapedNewlines: 'DontAlign',
AlignOperands: 'Align',
AlignTrailingComments: true,
AllowAllArgumentsOnNextLine: true,
AllowAllConstructorInitializersOnNextLine: true,
AllowAllParametersOfDeclarationOnNextLine: true,
AllowShortBlocksOnASingleLine: 'Always',
AllowShortCaseLabelsOnASingleLine: true,
AllowShortEnumsOnASingleLine: true,
AllowShortFunctionsOnASingleLine: 'Empty',
AllowShortIfStatementsOnASingleLine: 'AllIfsAndElse',
AllowShortLambdasOnASingleLine: 'Empty',
AllowShortLoopsOnASingleLine: true,
AlwaysBreakAfterDefinitionReturnType: 'None',
AlwaysBreakAfterReturnType: 'None',
AlwaysBreakBeforeMultilineStrings: false,
AlwaysBreakTemplateDeclarations: 'No',
AttributeMacros: ['__capability'],
BasedOnStyle: 'LLVM',
BinPackArguments: true,
BinPackParameters: true,
BitFieldColonSpacing: 'Both',
BraceWrapping: {
AfterCaseLabel: false,
AfterClass: false,
AfterControlStatement: 'Never',
AfterEnum: false,
AfterFunction: false,
AfterNamespace: false,
AfterObjCDeclaration: false,
AfterStruct: false,
AfterUnion: false,
AfterExternBlock: false,
BeforeCatch: false,
BeforeElse: false,
BeforeLambdaBody: false,
BeforeWhile: false,
IndentBraces: false,
SplitEmptyFunction: true,
SplitEmptyRecord: true,
SplitEmptyNamespace: true,
},
BreakAfterJavaFieldAnnotations: false,
BreakBeforeBinaryOperators: 'NonAssignment',
BreakBeforeBraces: 'Attach',
BreakBeforeConceptDeclarations: false,
BreakBeforeInheritanceComma: false,
BreakBeforeTernaryOperators: true,
BreakConstructorInitializers: 'BeforeColon',
BreakConstructorInitializersBeforeComma: false,
BreakInheritanceList: 'BeforeColon',
BreakStringLiterals: false,
ColumnLimit: 0,
CommentPragmas: '',
CompactNamespaces: false,
ConstructorInitializerAllOnOneLineOrOnePerLine: false,
ConstructorInitializerIndentWidth: 2,
ContinuationIndentWidth: 2,
Cpp11BracedListStyle: false,
DeriveLineEnding: true,
DerivePointerAlignment: true,
DisableFormat: false,
EmptyLineAfterAccessModifier: 'Leave',
EmptyLineBeforeAccessModifier: 'Leave',
ExperimentalAutoDetectBinPacking: false,
FixNamespaceComments: false,
ForEachMacros: ['foreach', 'Q_FOREACH', 'BOOST_FOREACH'],
IfMacros: ['KJ_IF_MAYBE'],
IncludeBlocks: 'Preserve',
IncludeCategories: [
{
Regex: '^"(llvm|llvm-c|clang|clang-c)/',
Priority: 2,
SortPriority: 0,
CaseSensitive: false,
},
{
Regex: '^(<|"(gtest|gmock|isl|json)/)',
Priority: 3,
SortPriority: 0,
CaseSensitive: false,
},
{ Regex: '.*', Priority: 1, SortPriority: 0, CaseSensitive: false },
],
IncludeIsMainRegex: '',
IncludeIsMainSourceRegex: '',
IndentAccessModifiers: false,
IndentCaseBlocks: true,
IndentCaseLabels: true,
IndentExternBlock: 'Indent',
IndentGotoLabels: false,
IndentPPDirectives: 'None',
IndentRequires: true,
IndentWidth: 2,
IndentWrappedFunctionNames: false,
InsertTrailingCommas: 'None',
JavaScriptQuotes: 'Leave',
JavaScriptWrapImports: true,
KeepEmptyLinesAtTheStartOfBlocks: true,
LambdaBodyIndentation: 'Signature',
Language: 'Cpp',
MacroBlockBegin: '',
MacroBlockEnd: '',
MaxEmptyLinesToKeep: 100000,
NamespaceIndentation: 'None',
ObjCBinPackProtocolList: 'Auto',
ObjCBlockIndentWidth: 2,
ObjCBreakBeforeNestedBlockParam: true,
ObjCSpaceAfterProperty: false,
ObjCSpaceBeforeProtocolList: true,
PPIndentWidth: -1,
PackConstructorInitializers: 'BinPack',
PenaltyBreakAssignment: 1,
PenaltyBreakBeforeFirstCallParameter: 1,
PenaltyBreakComment: 1,
PenaltyBreakFirstLessLess: 1,
PenaltyBreakOpenParenthesis: 1,
PenaltyBreakString: 1,
PenaltyBreakTemplateDeclaration: 1,
PenaltyExcessCharacter: 1,
PenaltyIndentedWhitespace: 1,
PenaltyReturnTypeOnItsOwnLine: 1,
PointerAlignment: 'Right',
QualifierAlignment: 'Leave',
ReferenceAlignment: 'Pointer',
ReflowComments: false,
RemoveBracesLLVM: false,
SeparateDefinitionBlocks: 'Leave',
ShortNamespaceLines: 0,
SortIncludes: 'Never',
SortJavaStaticImport: 'Before',
SortUsingDeclarations: false,
SpaceAfterCStyleCast: false,
SpaceAfterLogicalNot: false,
SpaceAfterTemplateKeyword: false,
SpaceAroundPointerQualifiers: 'Default',
SpaceBeforeAssignmentOperators: true,
SpaceBeforeCaseColon: false,
SpaceBeforeCpp11BracedList: false,
SpaceBeforeCtorInitializerColon: true,
SpaceBeforeInheritanceColon: true,
SpaceBeforeParens: 'ControlStatements',
SpaceBeforeParensOptions: {
AfterControlStatements: true,
AfterForeachMacros: true,
AfterFunctionDefinitionName: false,
AfterFunctionDeclarationName: false,
AfterIfMacros: true,
AfterOverloadedOperator: false,
BeforeNonEmptyParentheses: false,
},
SpaceBeforeRangeBasedForLoopColon: true,
SpaceBeforeSquareBrackets: false,
SpaceInEmptyBlock: false,
SpaceInEmptyParentheses: false,
SpacesBeforeTrailingComments: 2,
SpacesInAngles: 'Leave',
SpacesInCStyleCastParentheses: false,
SpacesInConditionalStatement: false,
SpacesInContainerLiterals: false,
SpacesInLineCommentPrefix: { Minimum: 0, Maximum: -1 },
SpacesInParentheses: false,
SpacesInSquareBrackets: false,
Standard: 'Auto',
StatementAttributeLikeMacros: ['Q_EMIT'],
StatementMacros: ['Q_UNUSED', 'QT_REQUIRE_VERSION'],
...defaultConfig,
TabWidth,
UseCRLF: false,
UseTab,
WhitespaceSensitiveMacros: [
'STRINGIZE',
'PP_STRINGIZE',
'BOOST_PP_STRINGIZE',
'NS_SWIFT_NAME',
'CF_SWIFT_NAME',
],
};
}

View File

@@ -5,25 +5,41 @@ import {
Range,
Position,
} from '@theia/core/shared/vscode-languageserver-protocol';
import type { CoreError } from '../common/protocol';
import { CoreError } from '../common/protocol';
import { Sketch } from '../common/protocol/sketches-service';
export interface ErrorSource {
export interface OutputSource {
readonly content: string | ReadonlyArray<Uint8Array>;
readonly sketch?: Sketch;
}
export function tryParseError(source: ErrorSource): CoreError.ErrorLocation[] {
const { content, sketch } = source;
const err =
typeof content === 'string'
export namespace OutputSource {
export function content(source: OutputSource): string {
const { content } = source;
return typeof content === 'string'
? content
: Buffer.concat(content).toString('utf8');
}
}
export function tryParseError(source: OutputSource): CoreError.ErrorLocation[] {
const { sketch } = source;
const content = OutputSource.content(source);
if (sketch) {
return tryParse(err)
return tryParse(content)
.map(remapErrorMessages)
.filter(isLocationInSketch(sketch))
.map(toErrorInfo);
.map(toErrorInfo)
.reduce((acc, curr) => {
const existingRef = acc.find((candidate) =>
CoreError.ErrorLocationRef.equals(candidate, curr)
);
if (existingRef) {
existingRef.rangesInOutput.push(...curr.rangesInOutput);
} else {
acc.push(curr);
}
return acc;
}, [] as CoreError.ErrorLocation[]);
}
return [];
}
@@ -35,6 +51,7 @@ interface ParseResult {
readonly errorPrefix: string;
readonly error: string;
readonly message?: string;
readonly rangeInOutput?: Range | undefined;
}
namespace ParseResult {
export function keyOf(result: ParseResult): string {
@@ -64,6 +81,7 @@ function toErrorInfo({
path,
line,
column,
rangeInOutput,
}: ParseResult): CoreError.ErrorLocation {
return {
message: error,
@@ -72,6 +90,7 @@ function toErrorInfo({
uri: FileUri.create(path).toString(),
range: range(line, column),
},
rangesInOutput: rangeInOutput ? [rangeInOutput] : [],
};
}
@@ -86,48 +105,50 @@ function range(line: number, column?: number): Range {
};
}
export function tryParse(raw: string): ParseResult[] {
function tryParse(content: string): ParseResult[] {
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
const re = new RegExp(
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
'gm'
);
return [
...new Map(
Array.from(raw.matchAll(re) ?? [])
.map((match) => {
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
(match) => (match ? match.trim() : match)
return Array.from(content.matchAll(re) ?? [])
.map((match) => {
const { index: start } = match;
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
(match) => (match ? match.trim() : match)
);
const line = Number.parseInt(rawLine, 10);
if (!Number.isInteger(line)) {
console.warn(
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
);
return undefined;
}
let column: number | undefined = undefined;
if (rawColumn) {
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
column = Number.parseInt(normalizedRawColumn, 10);
if (!Number.isInteger(column)) {
console.warn(
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
);
const line = Number.parseInt(rawLine, 10);
if (!Number.isInteger(line)) {
console.warn(
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
);
return undefined;
}
let column: number | undefined = undefined;
if (rawColumn) {
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
column = Number.parseInt(normalizedRawColumn, 10);
if (!Number.isInteger(column)) {
console.warn(
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
);
}
}
return {
path,
line,
column,
errorPrefix,
error,
};
})
.filter(notEmpty)
.map((result) => [ParseResult.keyOf(result), result])
).values(),
];
}
}
const rangeInOutput = findRangeInOutput(
start,
{ path, rawLine, rawColumn },
content
);
return {
path,
line,
column,
errorPrefix,
error,
rangeInOutput,
};
})
.filter(notEmpty);
}
/**
@@ -161,3 +182,47 @@ const KnownErrors: Record<string, { error: string; message?: string }> = {
),
},
};
function findRangeInOutput(
startIndex: number | undefined,
groups: { path: string; rawLine: string; rawColumn: string | null },
content: string // TODO? lines: string[]? can this code break line on `\n`? const lines = content.split(/\r?\n/) ?? [];
): Range | undefined {
if (startIndex === undefined) {
return undefined;
}
// /path/to/location/Sketch/Sketch.ino:36:42
const offset =
groups.path.length +
':'.length +
groups.rawLine.length +
(groups.rawColumn ? groups.rawColumn.length : 0);
const start = toPosition(startIndex, content);
if (!start) {
return undefined;
}
const end = toPosition(startIndex + offset, content);
if (!end) {
return undefined;
}
return { start, end };
}
function toPosition(offset: number, content: string): Position | undefined {
let line = 0;
let character = 0;
const length = content.length;
for (let i = 0; i < length; i++) {
const c = content.charAt(i);
if (i === offset) {
return { line, character };
}
if (c === '\n') {
line++;
character = 0;
} else {
character++;
}
}
return undefined;
}

View File

@@ -49,6 +49,7 @@ interface IArduinoCoreServiceService extends grpc.ServiceDefinition<grpc.Untyped
platformList: IArduinoCoreServiceService_IPlatformList;
libraryDownload: IArduinoCoreServiceService_ILibraryDownload;
libraryInstall: IArduinoCoreServiceService_ILibraryInstall;
libraryUpgrade: IArduinoCoreServiceService_ILibraryUpgrade;
zipLibraryInstall: IArduinoCoreServiceService_IZipLibraryInstall;
gitLibraryInstall: IArduinoCoreServiceService_IGitLibraryInstall;
libraryUninstall: IArduinoCoreServiceService_ILibraryUninstall;
@@ -348,6 +349,15 @@ interface IArduinoCoreServiceService_ILibraryInstall extends grpc.MethodDefiniti
responseSerialize: grpc.serialize<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
responseDeserialize: grpc.deserialize<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
}
interface IArduinoCoreServiceService_ILibraryUpgrade extends grpc.MethodDefinition<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest, cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse> {
path: "/cc.arduino.cli.commands.v1.ArduinoCoreService/LibraryUpgrade";
requestStream: false;
responseStream: true;
requestSerialize: grpc.serialize<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest>;
requestDeserialize: grpc.deserialize<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest>;
responseSerialize: grpc.serialize<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
responseDeserialize: grpc.deserialize<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
}
interface IArduinoCoreServiceService_IZipLibraryInstall extends grpc.MethodDefinition<cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallRequest, cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallResponse> {
path: "/cc.arduino.cli.commands.v1.ArduinoCoreService/ZipLibraryInstall";
requestStream: false;
@@ -465,6 +475,7 @@ export interface IArduinoCoreServiceServer {
platformList: grpc.handleUnaryCall<cc_arduino_cli_commands_v1_core_pb.PlatformListRequest, cc_arduino_cli_commands_v1_core_pb.PlatformListResponse>;
libraryDownload: grpc.handleServerStreamingCall<cc_arduino_cli_commands_v1_lib_pb.LibraryDownloadRequest, cc_arduino_cli_commands_v1_lib_pb.LibraryDownloadResponse>;
libraryInstall: grpc.handleServerStreamingCall<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallRequest, cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
libraryUpgrade: grpc.handleServerStreamingCall<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest, cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
zipLibraryInstall: grpc.handleServerStreamingCall<cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallRequest, cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallResponse>;
gitLibraryInstall: grpc.handleServerStreamingCall<cc_arduino_cli_commands_v1_lib_pb.GitLibraryInstallRequest, cc_arduino_cli_commands_v1_lib_pb.GitLibraryInstallResponse>;
libraryUninstall: grpc.handleServerStreamingCall<cc_arduino_cli_commands_v1_lib_pb.LibraryUninstallRequest, cc_arduino_cli_commands_v1_lib_pb.LibraryUninstallResponse>;
@@ -557,6 +568,8 @@ export interface IArduinoCoreServiceClient {
libraryDownload(request: cc_arduino_cli_commands_v1_lib_pb.LibraryDownloadRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryDownloadResponse>;
libraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.LibraryInstallRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
libraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.LibraryInstallRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
libraryUpgrade(request: cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
libraryUpgrade(request: cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
zipLibraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallResponse>;
zipLibraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallResponse>;
gitLibraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.GitLibraryInstallRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.GitLibraryInstallResponse>;
@@ -663,6 +676,8 @@ export class ArduinoCoreServiceClient extends grpc.Client implements IArduinoCor
public libraryDownload(request: cc_arduino_cli_commands_v1_lib_pb.LibraryDownloadRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryDownloadResponse>;
public libraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.LibraryInstallRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
public libraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.LibraryInstallRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryInstallResponse>;
public libraryUpgrade(request: cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
public libraryUpgrade(request: cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse>;
public zipLibraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallResponse>;
public zipLibraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallRequest, metadata?: grpc.Metadata, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.ZipLibraryInstallResponse>;
public gitLibraryInstall(request: cc_arduino_cli_commands_v1_lib_pb.GitLibraryInstallRequest, options?: Partial<grpc.CallOptions>): grpc.ClientReadableStream<cc_arduino_cli_commands_v1_lib_pb.GitLibraryInstallResponse>;

View File

@@ -489,6 +489,28 @@ function deserialize_cc_arduino_cli_commands_v1_LibraryUpgradeAllResponse(buffer
return cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeAllResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_cc_arduino_cli_commands_v1_LibraryUpgradeRequest(arg) {
if (!(arg instanceof cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest)) {
throw new Error('Expected argument of type cc.arduino.cli.commands.v1.LibraryUpgradeRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_cc_arduino_cli_commands_v1_LibraryUpgradeRequest(buffer_arg) {
return cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_cc_arduino_cli_commands_v1_LibraryUpgradeResponse(arg) {
if (!(arg instanceof cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse)) {
throw new Error('Expected argument of type cc.arduino.cli.commands.v1.LibraryUpgradeResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_cc_arduino_cli_commands_v1_LibraryUpgradeResponse(buffer_arg) {
return cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_cc_arduino_cli_commands_v1_ListProgrammersAvailableForUploadRequest(arg) {
if (!(arg instanceof cc_arduino_cli_commands_v1_upload_pb.ListProgrammersAvailableForUploadRequest)) {
throw new Error('Expected argument of type cc.arduino.cli.commands.v1.ListProgrammersAvailableForUploadRequest');
@@ -1325,6 +1347,18 @@ libraryInstall: {
responseSerialize: serialize_cc_arduino_cli_commands_v1_LibraryInstallResponse,
responseDeserialize: deserialize_cc_arduino_cli_commands_v1_LibraryInstallResponse,
},
// Upgrade a library to the newest version available.
libraryUpgrade: {
path: '/cc.arduino.cli.commands.v1.ArduinoCoreService/LibraryUpgrade',
requestStream: false,
responseStream: true,
requestType: cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeRequest,
responseType: cc_arduino_cli_commands_v1_lib_pb.LibraryUpgradeResponse,
requestSerialize: serialize_cc_arduino_cli_commands_v1_LibraryUpgradeRequest,
requestDeserialize: deserialize_cc_arduino_cli_commands_v1_LibraryUpgradeRequest,
responseSerialize: serialize_cc_arduino_cli_commands_v1_LibraryUpgradeResponse,
responseDeserialize: deserialize_cc_arduino_cli_commands_v1_LibraryUpgradeResponse,
},
// Install a library from a Zip File
zipLibraryInstall: {
path: '/cc.arduino.cli.commands.v1.ArduinoCoreService/ZipLibraryInstall',

View File

@@ -159,6 +159,11 @@ export class Platform extends jspb.Message {
getDeprecated(): boolean;
setDeprecated(value: boolean): Platform;
clearTypeList(): void;
getTypeList(): Array<string>;
setTypeList(value: Array<string>): Platform;
addType(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Platform.AsObject;
@@ -182,6 +187,7 @@ export namespace Platform {
boardsList: Array<Board.AsObject>,
manuallyInstalled: boolean,
deprecated: boolean,
typeList: Array<string>,
}
}

View File

@@ -987,7 +987,7 @@ proto.cc.arduino.cli.commands.v1.Programmer.prototype.setName = function(value)
* @private {!Array<number>}
* @const
*/
proto.cc.arduino.cli.commands.v1.Platform.repeatedFields_ = [8];
proto.cc.arduino.cli.commands.v1.Platform.repeatedFields_ = [8,11];
@@ -1030,7 +1030,8 @@ proto.cc.arduino.cli.commands.v1.Platform.toObject = function(includeInstance, m
boardsList: jspb.Message.toObjectList(msg.getBoardsList(),
proto.cc.arduino.cli.commands.v1.Board.toObject, includeInstance),
manuallyInstalled: jspb.Message.getBooleanFieldWithDefault(msg, 9, false),
deprecated: jspb.Message.getBooleanFieldWithDefault(msg, 10, false)
deprecated: jspb.Message.getBooleanFieldWithDefault(msg, 10, false),
typeList: (f = jspb.Message.getRepeatedField(msg, 11)) == null ? undefined : f
};
if (includeInstance) {
@@ -1108,6 +1109,10 @@ proto.cc.arduino.cli.commands.v1.Platform.deserializeBinaryFromReader = function
var value = /** @type {boolean} */ (reader.readBool());
msg.setDeprecated(value);
break;
case 11:
var value = /** @type {string} */ (reader.readString());
msg.addType(value);
break;
default:
reader.skipField();
break;
@@ -1208,6 +1213,13 @@ proto.cc.arduino.cli.commands.v1.Platform.serializeBinaryToWriter = function(mes
f
);
}
f = message.getTypeList();
if (f.length > 0) {
writer.writeRepeatedString(
11,
f
);
}
};
@@ -1411,6 +1423,43 @@ proto.cc.arduino.cli.commands.v1.Platform.prototype.setDeprecated = function(val
};
/**
* repeated string type = 11;
* @return {!Array<string>}
*/
proto.cc.arduino.cli.commands.v1.Platform.prototype.getTypeList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 11));
};
/**
* @param {!Array<string>} value
* @return {!proto.cc.arduino.cli.commands.v1.Platform} returns this
*/
proto.cc.arduino.cli.commands.v1.Platform.prototype.setTypeList = function(value) {
return jspb.Message.setField(this, 11, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
* @return {!proto.cc.arduino.cli.commands.v1.Platform} returns this
*/
proto.cc.arduino.cli.commands.v1.Platform.prototype.addType = function(value, opt_index) {
return jspb.Message.addToRepeatedField(this, 11, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.cc.arduino.cli.commands.v1.Platform} returns this
*/
proto.cc.arduino.cli.commands.v1.Platform.prototype.clearTypeList = function() {
return this.setTypeList([]);
};

View File

@@ -82,6 +82,9 @@ export class LibraryInstallRequest extends jspb.Message {
getNoOverwrite(): boolean;
setNoOverwrite(value: boolean): LibraryInstallRequest;
getInstallLocation(): LibraryInstallLocation;
setInstallLocation(value: LibraryInstallLocation): LibraryInstallRequest;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): LibraryInstallRequest.AsObject;
@@ -100,6 +103,7 @@ export namespace LibraryInstallRequest {
version: string,
noDeps: boolean,
noOverwrite: boolean,
installLocation: LibraryInstallLocation,
}
}
@@ -134,6 +138,69 @@ export namespace LibraryInstallResponse {
}
}
export class LibraryUpgradeRequest extends jspb.Message {
hasInstance(): boolean;
clearInstance(): void;
getInstance(): cc_arduino_cli_commands_v1_common_pb.Instance | undefined;
setInstance(value?: cc_arduino_cli_commands_v1_common_pb.Instance): LibraryUpgradeRequest;
getName(): string;
setName(value: string): LibraryUpgradeRequest;
getNoDeps(): boolean;
setNoDeps(value: boolean): LibraryUpgradeRequest;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): LibraryUpgradeRequest.AsObject;
static toObject(includeInstance: boolean, msg: LibraryUpgradeRequest): LibraryUpgradeRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: LibraryUpgradeRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): LibraryUpgradeRequest;
static deserializeBinaryFromReader(message: LibraryUpgradeRequest, reader: jspb.BinaryReader): LibraryUpgradeRequest;
}
export namespace LibraryUpgradeRequest {
export type AsObject = {
instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject,
name: string,
noDeps: boolean,
}
}
export class LibraryUpgradeResponse extends jspb.Message {
hasProgress(): boolean;
clearProgress(): void;
getProgress(): cc_arduino_cli_commands_v1_common_pb.DownloadProgress | undefined;
setProgress(value?: cc_arduino_cli_commands_v1_common_pb.DownloadProgress): LibraryUpgradeResponse;
hasTaskProgress(): boolean;
clearTaskProgress(): void;
getTaskProgress(): cc_arduino_cli_commands_v1_common_pb.TaskProgress | undefined;
setTaskProgress(value?: cc_arduino_cli_commands_v1_common_pb.TaskProgress): LibraryUpgradeResponse;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): LibraryUpgradeResponse.AsObject;
static toObject(includeInstance: boolean, msg: LibraryUpgradeResponse): LibraryUpgradeResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: LibraryUpgradeResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): LibraryUpgradeResponse;
static deserializeBinaryFromReader(message: LibraryUpgradeResponse, reader: jspb.BinaryReader): LibraryUpgradeResponse;
}
export namespace LibraryUpgradeResponse {
export type AsObject = {
progress?: cc_arduino_cli_commands_v1_common_pb.DownloadProgress.AsObject,
taskProgress?: cc_arduino_cli_commands_v1_common_pb.TaskProgress.AsObject,
}
}
export class LibraryUninstallRequest extends jspb.Message {
hasInstance(): boolean;
@@ -899,6 +966,11 @@ export namespace GitLibraryInstallResponse {
}
}
export enum LibraryInstallLocation {
LIBRARY_INSTALL_LOCATION_USER = 0,
LIBRARY_INSTALL_LOCATION_BUILTIN = 1,
}
export enum LibrarySearchStatus {
LIBRARY_SEARCH_STATUS_FAILED = 0,
LIBRARY_SEARCH_STATUS_SUCCESS = 1,
@@ -910,7 +982,7 @@ export enum LibraryLayout {
}
export enum LibraryLocation {
LIBRARY_LOCATION_IDE_BUILTIN = 0,
LIBRARY_LOCATION_BUILTIN = 0,
LIBRARY_LOCATION_USER = 1,
LIBRARY_LOCATION_PLATFORM_BUILTIN = 2,
LIBRARY_LOCATION_REFERENCED_PLATFORM_BUILTIN = 3,

View File

@@ -26,6 +26,7 @@ goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryDependency', null, gl
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryDependencyStatus', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryDownloadRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryDownloadResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryInstallLocation', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryInstallRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryInstallResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryLayout', null, global);
@@ -42,6 +43,8 @@ goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryUninstallRequest', nu
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryUninstallResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryUpgradeAllRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryUpgradeAllResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.SearchedLibrary', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.ZipLibraryInstallRequest', null, global);
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.ZipLibraryInstallResponse', null, global);
@@ -129,6 +132,48 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.cc.arduino.cli.commands.v1.LibraryInstallResponse.displayName = 'proto.cc.arduino.cli.commands.v1.LibraryInstallResponse';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.displayName = 'proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.displayName = 'proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -968,7 +1013,8 @@ proto.cc.arduino.cli.commands.v1.LibraryInstallRequest.toObject = function(inclu
name: jspb.Message.getFieldWithDefault(msg, 2, ""),
version: jspb.Message.getFieldWithDefault(msg, 3, ""),
noDeps: jspb.Message.getBooleanFieldWithDefault(msg, 4, false),
noOverwrite: jspb.Message.getBooleanFieldWithDefault(msg, 5, false)
noOverwrite: jspb.Message.getBooleanFieldWithDefault(msg, 5, false),
installLocation: jspb.Message.getFieldWithDefault(msg, 6, 0)
};
if (includeInstance) {
@@ -1026,6 +1072,10 @@ proto.cc.arduino.cli.commands.v1.LibraryInstallRequest.deserializeBinaryFromRead
var value = /** @type {boolean} */ (reader.readBool());
msg.setNoOverwrite(value);
break;
case 6:
var value = /** @type {!proto.cc.arduino.cli.commands.v1.LibraryInstallLocation} */ (reader.readEnum());
msg.setInstallLocation(value);
break;
default:
reader.skipField();
break;
@@ -1091,6 +1141,13 @@ proto.cc.arduino.cli.commands.v1.LibraryInstallRequest.serializeBinaryToWriter =
f
);
}
f = message.getInstallLocation();
if (f !== 0.0) {
writer.writeEnum(
6,
f
);
}
};
@@ -1203,6 +1260,24 @@ proto.cc.arduino.cli.commands.v1.LibraryInstallRequest.prototype.setNoOverwrite
};
/**
* optional LibraryInstallLocation install_location = 6;
* @return {!proto.cc.arduino.cli.commands.v1.LibraryInstallLocation}
*/
proto.cc.arduino.cli.commands.v1.LibraryInstallRequest.prototype.getInstallLocation = function() {
return /** @type {!proto.cc.arduino.cli.commands.v1.LibraryInstallLocation} */ (jspb.Message.getFieldWithDefault(this, 6, 0));
};
/**
* @param {!proto.cc.arduino.cli.commands.v1.LibraryInstallLocation} value
* @return {!proto.cc.arduino.cli.commands.v1.LibraryInstallRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryInstallRequest.prototype.setInstallLocation = function(value) {
return jspb.Message.setProto3EnumField(this, 6, value);
};
@@ -1408,6 +1483,419 @@ proto.cc.arduino.cli.commands.v1.LibraryInstallResponse.prototype.hasTaskProgres
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.toObject = function(includeInstance, msg) {
var f, obj = {
instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f),
name: jspb.Message.getFieldWithDefault(msg, 2, ""),
noDeps: jspb.Message.getBooleanFieldWithDefault(msg, 3, false)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest;
return proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = new cc_arduino_cli_commands_v1_common_pb.Instance;
reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.Instance.deserializeBinaryFromReader);
msg.setInstance(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setName(value);
break;
case 3:
var value = /** @type {boolean} */ (reader.readBool());
msg.setNoDeps(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getInstance();
if (f != null) {
writer.writeMessage(
1,
f,
cc_arduino_cli_commands_v1_common_pb.Instance.serializeBinaryToWriter
);
}
f = message.getName();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
f = message.getNoDeps();
if (f) {
writer.writeBool(
3,
f
);
}
};
/**
* optional Instance instance = 1;
* @return {?proto.cc.arduino.cli.commands.v1.Instance}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.getInstance = function() {
return /** @type{?proto.cc.arduino.cli.commands.v1.Instance} */ (
jspb.Message.getWrapperField(this, cc_arduino_cli_commands_v1_common_pb.Instance, 1));
};
/**
* @param {?proto.cc.arduino.cli.commands.v1.Instance|undefined} value
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.setInstance = function(value) {
return jspb.Message.setWrapperField(this, 1, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.clearInstance = function() {
return this.setInstance(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.hasInstance = function() {
return jspb.Message.getField(this, 1) != null;
};
/**
* optional string name = 2;
* @return {string}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.getName = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.setName = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
/**
* optional bool no_deps = 3;
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.getNoDeps = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 3, false));
};
/**
* @param {boolean} value
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeRequest.prototype.setNoDeps = function(value) {
return jspb.Message.setProto3BooleanField(this, 3, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.toObject = function(opt_includeInstance) {
return proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.toObject = function(includeInstance, msg) {
var f, obj = {
progress: (f = msg.getProgress()) && cc_arduino_cli_commands_v1_common_pb.DownloadProgress.toObject(includeInstance, f),
taskProgress: (f = msg.getTaskProgress()) && cc_arduino_cli_commands_v1_common_pb.TaskProgress.toObject(includeInstance, f)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse;
return proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = new cc_arduino_cli_commands_v1_common_pb.DownloadProgress;
reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.DownloadProgress.deserializeBinaryFromReader);
msg.setProgress(value);
break;
case 2:
var value = new cc_arduino_cli_commands_v1_common_pb.TaskProgress;
reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.TaskProgress.deserializeBinaryFromReader);
msg.setTaskProgress(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getProgress();
if (f != null) {
writer.writeMessage(
1,
f,
cc_arduino_cli_commands_v1_common_pb.DownloadProgress.serializeBinaryToWriter
);
}
f = message.getTaskProgress();
if (f != null) {
writer.writeMessage(
2,
f,
cc_arduino_cli_commands_v1_common_pb.TaskProgress.serializeBinaryToWriter
);
}
};
/**
* optional DownloadProgress progress = 1;
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgress}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.getProgress = function() {
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgress} */ (
jspb.Message.getWrapperField(this, cc_arduino_cli_commands_v1_common_pb.DownloadProgress, 1));
};
/**
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgress|undefined} value
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.setProgress = function(value) {
return jspb.Message.setWrapperField(this, 1, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.clearProgress = function() {
return this.setProgress(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.hasProgress = function() {
return jspb.Message.getField(this, 1) != null;
};
/**
* optional TaskProgress task_progress = 2;
* @return {?proto.cc.arduino.cli.commands.v1.TaskProgress}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.getTaskProgress = function() {
return /** @type{?proto.cc.arduino.cli.commands.v1.TaskProgress} */ (
jspb.Message.getWrapperField(this, cc_arduino_cli_commands_v1_common_pb.TaskProgress, 2));
};
/**
* @param {?proto.cc.arduino.cli.commands.v1.TaskProgress|undefined} value
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.setTaskProgress = function(value) {
return jspb.Message.setWrapperField(this, 2, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse} returns this
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.clearTaskProgress = function() {
return this.setTaskProgress(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.v1.LibraryUpgradeResponse.prototype.hasTaskProgress = function() {
return jspb.Message.getField(this, 2) != null;
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
@@ -6600,6 +7088,14 @@ proto.cc.arduino.cli.commands.v1.GitLibraryInstallResponse.prototype.hasTaskProg
};
/**
* @enum {number}
*/
proto.cc.arduino.cli.commands.v1.LibraryInstallLocation = {
LIBRARY_INSTALL_LOCATION_USER: 0,
LIBRARY_INSTALL_LOCATION_BUILTIN: 1
};
/**
* @enum {number}
*/
@@ -6620,7 +7116,7 @@ proto.cc.arduino.cli.commands.v1.LibraryLayout = {
* @enum {number}
*/
proto.cc.arduino.cli.commands.v1.LibraryLocation = {
LIBRARY_LOCATION_IDE_BUILTIN: 0,
LIBRARY_LOCATION_BUILTIN: 0,
LIBRARY_LOCATION_USER: 1,
LIBRARY_LOCATION_PLATFORM_BUILTIN: 2,
LIBRARY_LOCATION_REFERENCED_PLATFORM_BUILTIN: 3,

View File

@@ -144,7 +144,7 @@ export class CoreClientProvider {
await this.updateIndexes(client); // TODO: this should run without the 3rd party URLs
await this.initInstance(client);
console.info(
`Downloaded the primary packages indexes, and successfully initialized the core gRPC client.`
`Downloaded the primary package indexes, and successfully initialized the core gRPC client.`
);
} else {
console.error(

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