Compare commits

...

37 Commits

Author SHA1 Message Date
Alberto Iannaccone
439cdfbbff 2.0.0-rc8 (#1105) 2022-06-23 11:33:27 +02:00
Alberto Iannaccone
672fd4e4b0 bump arduino-cli version to 0.24.0 (#1103) 2022-06-23 10:59:07 +02:00
David Simpson
0f1d379e58 reference cli rc1 0.24 in package.json (#1098) 2022-06-22 18:54:30 +02:00
Francesco Spissu
a79c9b4449 Sketchbook tree indentation (#1097) 2022-06-22 18:23:14 +02:00
Akos Kitta
0f8a29a493 Disabled MenuItem roles on macOS.
Closes #969
Upstream-ref: eclipse-theia/theia#11217

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-22 12:17:36 +02:00
David Simpson
a54d7c8f45 #1032 failing upload flag for monitor mgr (#1040)
* 1032 failing upload flag for monitor mgr

* move upload failure fix logic to frontend

* misc corrections

* avoid starting monitor when upload is in progress

* avoid starting monitor when upload is in progress

* prevent monitor side effects on upload (WIP)

* send upload req after notifying mgr

* dispose instead of pause on upld (code not final)

* Revert "dispose instead of pause on upld (code not final)"

This reverts commit 2d5dff2a2d.

* force wait before upload (test)

* always start queued services after uplaod finishes

* test cli with monitor close delay

* clean up unnecessary await(s)

* remove unused dependency

* revert CLI to 0.23

* use master cli for testing, await in upload finish

* remove upload port from pending monitor requests

* fix startQueuedServices

* refinements queued monitors

* clean up monitor mgr state

* fix typo from prev cleanup

* avoid dupl queued monitor services

* variable name changes

* reference latest cli commit in package.json

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2022-06-22 10:39:14 +02:00
Akos Kitta
84109e416a Fixed widget lookup to eliminate duplicate tabs.
- Removed `@theia/editor-preview`,
 - Patched opener options when repairing layout on start, and
 - Compare widget keys with deepEquals instead of string equal.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-22 10:11:11 +02:00
Francesco Spissu
083337de1c IDE updater dialog colors fix (#1092) 2022-06-21 18:04:28 +02:00
Francesco Spissu
bd6bc135fd Remote sketchbook tooltips (#1088)
* rename Cloud Sketchbook in Remote Sketchbook

* add tooltips for Sync and Account buttons
2022-06-21 18:03:39 +02:00
Akos Kitta
4611381a38 Merged in #1074.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-21 13:09:35 +02:00
Akos Kitta
d6f4096cd0 Reveal the error location after on failed verify.
Closes #608
Closes #229

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-21 13:09:35 +02:00
David Simpson
a715da3d18 flush on clear output buffer (#1074) 2022-06-20 09:32:10 +02:00
Akos Kitta
94ceefd960 Can enable debug logging of the gRPC calls.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-17 14:14:47 +02:00
Akos Kitta
27dd120e5d Cleaned up File menu.
Removed:
 - `New File`,
 - `New Window`.

Closes #1014.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-17 13:11:08 +02:00
Francesco Spissu
f5cee97fef Implement survey notification (#1035) 2022-06-17 10:17:42 +02:00
Akos Kitta
a9aac0dbb0 Bound the original handler to this.
Closes #977

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-14 14:58:48 +02:00
David Simpson
4c6243176c Output panel optimisation (#1058)
* test interval for output panel

* create buffer provider

* output panel buffer corrections

* output buffer cleanup

* code cleanup
2022-06-14 13:00:20 +02:00
Akos Kitta
a8047660a6 Restored the Settings UI. Deferred model loading.
Closes #1031

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-13 17:08:35 +02:00
Akos Kitta
7c2843f7fd Relaxed the error handling of the core client init
For example, `malformed custom board options` was incorrectly detected
as loading JSON index file error.

Closes #1036

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-13 17:06:58 +02:00
Alberto Iannaccone
fd5154ae93 2.0.0-rc7 (#1027) 2022-06-09 10:14:56 +02:00
Alberto Iannaccone
726628e20c Fix monitor service id creation (#1025) 2022-06-08 17:21:26 +02:00
Akos Kitta
585a82b51a Added logging when restoring the layout data.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-08 17:09:57 +02:00
Akos Kitta
5edccb9c35 Avoid opening duplicate editor tabs.
Customized the shell layout restorer:
 - If a resource is about to open in code editor and preview,
do not open the preview.
 - If a resource is about to open in preview only, open a code
editor instead.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-08 17:09:57 +02:00
Akos Kitta
555da878f4 Editor manager should be singleton.
Added some logging when filtering the layout data.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-08 17:09:57 +02:00
Alberto Iannaccone
df8658eff9 Pluggable monitor (#982)
* backend structure WIP

* Scaffold interfaces and classes for pluggable monitors

* Implement MonitorService to handle pluggable monitor lifetime

* Rename WebSocketService to WebSocketProvider and uninjected it

* Moved some interfaces

* Changed upload settings

* Enhance MonitorManager APIs

* Fixed WebSocketChange event signature

* Add monitor proxy functions for the frontend

* Moved settings to MonitorService

* Remove several unnecessary serial monitor classes

* Changed how connection is handled on upload

* Proxied more monitor methods to frontend

* WebSocketProvider is not injectable anymore

* Add generic monitor settings storaging

* More serial classes removal

* Remove unused file

* Changed plotter contribution to use new manager proxy

* Changed MonitorWidget and children to use new monitor proxy

* Updated MonitorWidget to use new monitor proxy

* Fix backend logger bindings

* Delete unnecessary Symbol

* coreClientProvider is now set when constructing MonitorService

* Add missing binding

* Fix `MonitorManagerProxy` DI issue

* fix monitor connection

* delete duplex when connection is closed

* update arduino-cli to 0.22.0

* fix upload when monitor is open

* add MonitorSettingsProvider interface

* monitor settings provider stub

* updated pseudo code

* refactor monitor settings interfaces

* monitor service provider singleton

* add unit tests

* change MonitorService providers to injectable deps

* fix monitor settings client communication

* refactor monitor commands protocol

* use monitor settings provider properly

* add settings to monitor model

* add settings to monitor model

* reset serial monitor when port changes

* fix serial plotter opening

* refine monitor connection settings

* fix hanging web socket connections

* add serial plotter reset command

* send port to web socket clients

* monitor service wait for success serial port open

* fix reset loop

* update serial plotter version

* update arduino-cli version to 0.23.0-rc1 and regenerate grpc protocol

* remove useless plotter protocol file

* localize web socket errors

* clean-up code

* update translation file

* Fix duplicated editor tabs (#1012)

* Save dialog for closing temporary sketch and unsaved files (#893)

* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

* Fixed FS path vs encoded URL comparision when handling stop request.

Ref: https://github.com/eclipse-theia/theia/issues/11226
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations again.

Removed `electron` from the `nls-extract`. It does not contain app code.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Aligned the stop handler code to Theia.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

* fix serial monitor send line ending

* refactor monitor-service poll for test/readability

* localize web socket errors

* update translation file

* Fix duplicated editor tabs (#1012)

* i18n:check rerun

* Speed up IDE startup time.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* override coreClientProvider in monitor-service

* cleanup merged code

Co-authored-by: Francesco Stasi <f.stasi@me.com>
Co-authored-by: Silvano Cerza <silvanocerza@gmail.com>
Co-authored-by: Mark Sujew <mark.sujew@typefox.io>
Co-authored-by: David Simpson <45690499+davegarthsimpson@users.noreply.github.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 15:51:12 +02:00
Akos Kitta
4c55807392 Speed up IDE startup time.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 13:46:29 +02:00
github-actions[bot]
cb50d3a70d Updated translation files (#974)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-07 12:19:36 +02:00
David Simpson
eaf14aa1eb Follow up 944: authentication sessions are not persistent (#1003)
* #944: Fixed auth. sessions not persistent

* 944: Prevent race conditions setting authOptions

* typo correction, duplicate identifier

* prevent block of auth client service on setOptions

* consider windows cred. mgr. password len limit
2022-06-07 11:46:28 +02:00
Akos Kitta
a59e0da2af Use clang-format as the default sketch formatter.
- Bumped `clangd` to `14.0.0`,
 - Can use `.clang-format` from:
   - current sketch folder,
   - `~/.arduinoIDE/.clang-format`,
   - `directories#data/.clang-format`, or
   - falls back to default formatter styles.

Closes #1009
Closes #566

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 10:51:45 +02:00
Francesco Spissu
3a3ac6da4e Dark theme implementation (#991) 2022-06-07 10:48:45 +02:00
Akos Kitta
d7809616a4 Fixed LS stops working after OS sleep/wakeup cycle
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-07 10:31:05 +02:00
Mark Sujew
5b486b1480 Save dialog for closing temporary sketch and unsaved files (#893)
* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

* Fixed FS path vs encoded URL comparision when handling stop request.

Ref: https://github.com/eclipse-theia/theia/issues/11226
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations again.

Removed `electron` from the `nls-extract`. It does not contain app code.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Aligned the stop handler code to Theia.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
2022-06-01 10:55:08 +02:00
Mark Sujew
5fc30bd33e Fix duplicated editor tabs (#1012) 2022-05-31 11:33:07 +02:00
Akos Kitta
522a5c6e01 Relaxed the Node version: ^14.x
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-05-25 10:05:20 +02:00
Mark Sujew
1ae60ec9bc Updated Theia to 1.25.0
Co-authored-by: Mark Sujew <mark.sujew@typefox.io>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-05-25 10:05:20 +02:00
David Simpson
b8c718ce9e #944: Fixed auth. sessions not persistent (#992) 2022-05-23 09:52:44 +02:00
Akos Kitta
b407d0aee0 #985: Restored the missing inject decorator.
Closes #985.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-05-13 09:20:00 +02:00
313 changed files with 24191 additions and 17067 deletions

View File

@@ -78,6 +78,7 @@ jobs:
fi
fi
npx node-gyp install
yarn --cwd ./electron/packager/
yarn --cwd ./electron/packager/ package

View File

@@ -0,0 +1,49 @@
name: themes-weekly-pull
on:
schedule:
# run every friday at 5AM
- cron: '0 5 * * 5'
workflow_dispatch:
env:
NODE_VERSION: 14.x
jobs:
pull-from-jsonbin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: yarn
- name: Run themes:pull script
run: yarn run themes:pull
env:
JSONBIN_MASTER_KEY: ${{ secrets.JSONBIN_MASTER_KEY }}
JSONBIN_ID: ${{ secrets.JSONBIN_ID }}
- name: Generate dark tokens
run: npx token-transformer scripts/themes/tokens/arduino-tokens.json scripts/themes/tokens/dark.json core,ide-default,ide-dark,theia core,ide-default,ide-dark
- name: Generate default tokens
run: npx token-transformer scripts/themes/tokens/arduino-tokens.json scripts/themes/tokens/default.json core,ide-default,theia core,ide-default
- name: Run themes:generate script
run: yarn run themes:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
commit-message: Updated themes
title: Update themes
branch: themes/themes-update
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

6
.gitignore vendored
View File

@@ -17,3 +17,9 @@ yarn*.log
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes
scripts/themes/tokens
# environment variables
.env
# content trace files for electron
electron-app/traces

View File

@@ -2,5 +2,6 @@
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 80
"printWidth": 80,
"endOfLine": "auto"
}

40
.vscode/launch.json vendored
View File

@@ -1,6 +1,44 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "App (Electron) [Dev]",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"args": [
".",
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--app-project-path=${workspaceRoot}/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--nosplash",
"--content-trace",
"--open-devtools"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
"${workspaceRoot}/node_modules/@theia/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
@@ -10,7 +48,6 @@
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
},
"cwd": "${workspaceFolder}/electron-app",
"protocol": "inspector",
"args": [
".",
"--log-level=debug",
@@ -78,7 +115,6 @@
{
"type": "node",
"request": "launch",
"protocol": "inspector",
"name": "Run Test [current]",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": [

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-rc6",
"version": "2.0.0-rc8",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -21,31 +21,30 @@
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"@grpc/grpc-js": "^1.3.7",
"@theia/application-package": "1.22.1",
"@theia/core": "1.22.1",
"@theia/editor": "1.22.1",
"@theia/editor-preview": "1.22.1",
"@theia/electron": "1.22.1",
"@theia/filesystem": "1.22.1",
"@theia/git": "1.22.1",
"@theia/keymaps": "1.22.1",
"@theia/markers": "1.22.1",
"@theia/monaco": "1.22.1",
"@theia/navigator": "1.22.1",
"@theia/outline-view": "1.22.1",
"@theia/output": "1.22.1",
"@theia/preferences": "1.22.1",
"@theia/search-in-workspace": "1.22.1",
"@theia/terminal": "1.22.1",
"@theia/workspace": "1.22.1",
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/markers": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/outline-view": "1.25.0",
"@theia/output": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/search-in-workspace": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^5.0.35",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
@@ -58,19 +57,18 @@
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.0.17",
"arduino-serial-plotter-webapp": "0.1.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"css-element-queries": "^1.2.0",
"dateformat": "^3.0.3",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fuzzy": "^0.1.3",
"fast-safe-stringify": "^2.1.1",
"glob": "^7.1.6",
"google-protobuf": "^3.11.4",
"grpc": "^1.24.11",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
@@ -91,6 +89,7 @@
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
"temp-dir": "^2.0.0",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"url": "^0.11.0",
@@ -157,13 +156,13 @@
],
"arduino": {
"cli": {
"version": "0.21.0"
"version": "0.24.0"
},
"fwuploader": {
"version": "2.0.0"
"version": "2.2.0"
},
"clangd": {
"version": "13.0.0"
"version": "14.0.0"
},
"languageServer": {
"version": "0.6.0"

View File

@@ -4,30 +4,93 @@
const version = '1.9.1';
(async () => {
const os = require('os');
const { promises: fs } = require('fs');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
process.exit(1);
if (
shell.exec(
`git clone https://github.com/arduino/arduino-examples.git ${repository}`
).code !== 0
) {
shell.exit(1);
}
if (
shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`)
.code !== 0
) {
shell.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
const isSketch = async (pathLike) => {
try {
const names = await fs.readdir(pathLike);
const dirName = path.basename(pathLike);
return names.indexOf(`${dirName}.ino`) !== -1;
} catch (e) {
if (e.code === 'ENOTDIR') {
return false;
}
throw e;
}
if (shell.exec(`git clone https://github.com/arduino/arduino-examples.git ${repository}`).code !== 0) {
shell.exit(1);
process.exit(1);
};
const examples = [];
const categories = await fs.readdir(destination);
const visit = async (pathLike, container) => {
const stat = await fs.lstat(pathLike);
if (stat.isDirectory()) {
if (await isSketch(pathLike)) {
container.sketches.push({
name: path.basename(pathLike),
relativePath: path.relative(destination, pathLike),
});
} else {
const names = await fs.readdir(pathLike);
for (const name of names) {
const childPath = path.join(pathLike, name);
if (await isSketch(childPath)) {
container.sketches.push({
name,
relativePath: path.relative(destination, childPath),
});
} else {
const child = {
label: name,
children: [],
sketches: [],
};
container.children.push(child);
await visit(childPath, child);
}
}
}
}
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
process.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
};
for (const category of categories) {
const example = {
label: category,
children: [],
sketches: [],
};
await visit(path.join(destination, category), example);
examples.push(example);
}
await fs.writeFile(
path.join(destination, 'examples.json'),
JSON.stringify(examples, null, 2),
{ encoding: 'utf8' }
);
shell.echo(`Generated output to ${path.join(destination, 'examples.json')}`);
})();

View File

@@ -66,21 +66,24 @@
build,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, lsSuffix, clangdSuffix;
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'win32-x64':
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
@@ -103,4 +106,15 @@
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
build,
clangFormatExecutablePath,
force,
{
strip: 1,
}
);
})();

View File

@@ -1,5 +1,9 @@
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import {
BoardsService,
@@ -7,6 +11,7 @@ import {
ExecutableService,
Sketch,
LibraryService,
ArduinoDaemon,
} from '../common/protocol';
import { Mutex } from 'async-mutex';
import {
@@ -17,9 +22,11 @@ import {
DisposableCollection,
} from '@theia/core';
import {
Dialog,
FrontendApplication,
FrontendApplicationContribution,
LocalStorageService,
OnWillStopAction,
SaveableWidget,
StatusBar,
StatusBarAlignment,
@@ -44,19 +51,12 @@ import {
EditorManager,
EditorOpenerOptions,
} from '@theia/editor/lib/browser';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorCommands, FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ConfigService } from '../common/protocol/config-service';
import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
@@ -67,12 +67,15 @@ import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { ArduinoPreferences } from './arduino-preferences';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from './contributions/save-as-sketch';
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog';
import { IDEUpdater } from '../common/protocol/ide-updater';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { HostedPluginEvents } from './hosted-plugin-events';
const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
export const SKIP_IDE_VERSION = 'skipIDEVersion';
@@ -84,92 +87,73 @@ export class ArduinoFrontendContribution
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution {
ColorContribution
{
@inject(ILogger)
protected logger: ILogger;
private readonly logger: ILogger;
@inject(MessageService)
protected readonly messageService: MessageService;
private readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
private readonly boardsService: BoardsService;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
private readonly libraryService: LibraryService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
private readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
private readonly editorManager: EditorManager;
@inject(FileService)
protected readonly fileService: FileService;
private readonly fileService: FileService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
private readonly sketchService: SketchesService;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
private readonly boardsConfigDialog: BoardsConfigDialog;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SketchbookWidgetContribution)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
private readonly statusBar: StatusBar;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
private readonly editorMode: EditorMode;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport;
@inject(HostedPluginEvents)
private readonly hostedPluginEvents: HostedPluginEvents;
@inject(ExecutableService)
protected executableService: ExecutableService;
private readonly executableService: ExecutableService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
private readonly sketchServiceClient: SketchesServiceClientImpl;
protected readonly appStateService: FrontendApplicationStateService;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
private readonly localStorageService: LocalStorageService;
@inject(FileSystemFrontendContribution)
protected readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
@inject(IDEUpdater)
protected readonly updater: IDEUpdater;
private readonly updater: IDEUpdater;
@inject(IDEUpdaterDialog)
protected readonly updaterDialog: IDEUpdaterDialog;
private readonly updaterDialog: IDEUpdaterDialog;
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
protected invalidConfigPopup:
| Promise<void | 'No' | 'Yes' | undefined>
@@ -240,7 +224,10 @@ export class ArduinoFrontendContribution
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && !(await this.sketchService.isTemp(sketch))) {
if (
CurrentSketch.isValid(sketch) &&
!(await this.sketchService.isTemp(sketch))
) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
@@ -266,21 +253,6 @@ export class ArduinoFrontendContribution
}
async onStart(app: FrontendApplication): Promise<void> {
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution,
this.sketchbookWidgetContribution,
] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
}
this.updater
.init(
this.arduinoPreferences.get('arduino.ide.updateChannel'),
@@ -314,6 +286,12 @@ export class ArduinoFrontendContribution
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.hostedPluginEvents.onPluginsDidStart(() =>
start(this.boardsServiceClientImpl.boardsConfig)
);
this.hostedPluginEvents.onPluginsWillUnload(
() => (this.languageServerFqbn = undefined)
);
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
@@ -344,16 +322,18 @@ export class ArduinoFrontendContribution
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
this.fileSystemFrontendContribution.onDidChangeEditorFile(e => {
if (e.type === FileChangeType.DELETED) {
const editorWidget = e.editor;
if (SaveableWidget.is(editorWidget)) {
editorWidget.closeWithoutSaving();
} else {
editorWidget.close();
this.fileSystemFrontendContribution.onDidChangeEditorFile(
({ type, editor }) => {
if (type === FileChangeType.DELETED) {
const editorWidget = editor;
if (SaveableWidget.is(editorWidget)) {
editorWidget.closeWithoutSaving();
} else {
editorWidget.close();
}
}
}
});
);
}
onStop(): void {
@@ -366,9 +346,13 @@ export class ArduinoFrontendContribution
fqbn: string,
name: string | undefined
): Promise<void> {
const port = await this.daemon.tryGetPort();
if (!port) {
return;
}
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
await this.hostedPluginEvents.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
@@ -403,7 +387,7 @@ export class ArduinoFrontendContribution
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
if (CurrentSketch.isValid(currentSketch)) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
@@ -415,8 +399,6 @@ export class ArduinoFrontendContribution
this.fileService.fsPath(new URI(lsUri)),
]);
const config = await this.configService.getConfiguration();
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
@@ -428,7 +410,7 @@ export class ArduinoFrontendContribution
'arduino.languageserver.start',
{
lsPath,
cliDaemonAddr: `localhost:${config.daemon.port}`, // TODO: verify if this port is coming from the BE
cliDaemonAddr: `localhost:${port}`,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
@@ -494,13 +476,13 @@ export class ArduinoFrontendContribution
EditorCommands.SPLIT_EDITOR_UP,
EditorCommands.SPLIT_EDITOR_VERTICAL,
EditorCommands.SPLIT_EDITOR_HORIZONTAL,
FileNavigatorCommands.REVEAL_IN_NAVIGATOR
FileNavigatorCommands.REVEAL_IN_NAVIGATOR,
]) {
registry.unregisterCommand(command);
}
}
registerMenus(registry: MenuModelRegistry) {
registerMenus(registry: MenuModelRegistry): void {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
@@ -569,12 +551,19 @@ export class ArduinoFrontendContribution
uri: string,
forceOpen = false,
options?: EditorOpenerOptions | undefined
): Promise<any> {
): Promise<unknown> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
return this.editorManager.open(
new URI(uri),
options ?? {
mode: 'reveal',
preview: false,
counter: 0,
}
);
}
}
@@ -658,4 +647,57 @@ export class ArduinoFrontendContribution
}
);
}
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

@@ -1,5 +1,5 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from 'inversify';
import { ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -42,15 +42,14 @@ import { FileNavigatorContribution as TheiaFileNavigatorContribution } from '@th
import { KeymapsFrontendContribution } from './theia/keymaps/keymaps-frontend-contribution';
import { KeymapsFrontendContribution as TheiaKeymapsFrontendContribution } from '@theia/keymaps/lib/browser/keymaps-frontend-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { EditorPreviewContribution as TheiaEditorPreviewContribution } from '@theia/editor-preview/lib/browser/editor-preview-contribution';
import { EditorPreviewContribution } from './theia/editor/editor-contribution';
import { EditorContribution as TheiaEditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from './theia/editor/editor-contribution';
import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-contribution';
import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
@@ -69,20 +68,12 @@ import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { SerialServiceClientImpl } from './serial/serial-service-client-impl';
import {
SerialServicePath,
SerialService,
SerialServiceClient,
} from '../common/protocol/serial-service';
import {
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import { MonitorWidget } from './serial/monitor/monitor-widget';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { SerialConnectionManager } from './serial/serial-connection-manager';
import { SerialModel } from './serial/serial-model';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
@@ -130,6 +121,7 @@ import { SaveAsSketch } from './contributions/save-as-sketch';
import { SaveSketch } from './contributions/save-sketch';
import { VerifySketch } from './contributions/verify-sketch';
import { UploadSketch } from './contributions/upload-sketch';
import { SurveyNotification } from './contributions/survey-notification';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
import { OpenSketchExternal } from './contributions/open-sketch-external';
@@ -138,7 +130,6 @@ import { PreferencesContribution } from './theia/preferences/preferences-contrib
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { Settings } from './contributions/settings';
import { KeybindingRegistry } from './theia/core/keybindings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
@@ -160,7 +151,14 @@ import {
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
OutputChannelRegistryMainImpl,
} from './theia/plugin-ext/output-channel-registry-main';
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
import {
ExecutableService,
ExecutableServicePath,
MonitorManagerProxy,
MonitorManagerProxyClient,
MonitorManagerProxyFactory,
MonitorManagerProxyPath,
} from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { ResponseServiceImpl } from './response-service-impl';
@@ -275,20 +273,51 @@ import {
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
const ElementQueries = require('css-element-queries/src/ElementQueries');
import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorManager } from './theia/editor/editor-manager';
import { HostedPluginEvents } from './hosted-plugin-events';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { Formatter, FormatterPath } from '../common/protocol/formatter';
import { Format } from './contributions/format';
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
import { DefaultJsonSchemaContribution } from './theia/core/json-schema-store';
import { DefaultJsonSchemaContribution as TheiaDefaultJsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
import { EditorNavigationContribution } from './theia/editor/editor-navigation-contribution';
import { EditorNavigationContribution as TheiaEditorNavigationContribution } from '@theia/editor/lib/browser/editor-navigation-contribution';
import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-generator';
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
import { AboutDialog } from './theia/core/about-dialog';
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import {
SurveyNotificationService,
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { WindowContribution } from './theia/core/window-contribution';
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
MonacoThemingService.register({
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/arduino.color-theme.json'),
json: require('../../src/browser/data/default.color-theme.json'),
});
MonacoThemingService.register({
id: 'arduino-theme-dark',
label: 'Dark (Arduino)',
uiTheme: 'vs-dark',
json: require('../../src/browser/data/dark.color-theme.json'),
});
export default new ContainerModule((bind, unbind, isBound, rebind) => {
ElementQueries.listen();
ElementQueries.init();
// Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
@@ -405,31 +434,47 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
bind(CoreErrorHandler).toSelf().inSingletonScope();
// Serial monitor
bind(SerialModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SerialModel);
bind(MonitorWidget).toSelf();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorModel).toSelf().inSingletonScope();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget),
}));
// Frontend binding for the serial service
bind(SerialService)
.toDynamicValue((context) => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get<SerialServiceClient>(
SerialServiceClient
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
return connection.createProxy(SerialServicePath, client);
})
.inSingletonScope();
bind(SerialConnectionManager).toSelf().inSingletonScope();
},
}));
// Serial service client to receive and delegate notifications from the backend.
bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
bind(MonitorManagerProxyFactory).toFactory(
(context) => () =>
context.container.get<MonitorManagerProxy>(MonitorManagerProxy)
);
bind(MonitorManagerProxy)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
MonitorManagerProxyPath,
context.container.get(MonitorManagerProxyClient)
)
)
.inSingletonScope();
// Monitor manager proxy client to receive and delegate pluggable monitors
// notifications from the backend
bind(MonitorManagerProxyClient)
.to(MonitorManagerProxyClientImpl)
.inSingletonScope();
bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);
@@ -442,6 +487,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
// Survey notification
bind(SurveyNotification).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SurveyNotification);
bind(SurveyNotificationService)
.toDynamicValue((context) => {
return ElectronIpcConnectionProvider.createProxy(
context.container,
SurveyNotificationServicePath
);
})
.inSingletonScope();
// Layout and shell customizations.
rebind(TheiaOutlineViewContribution)
.to(OutlineViewContribution)
@@ -453,9 +511,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaKeymapsFrontendContribution)
.to(KeymapsFrontendContribution)
.inSingletonScope();
rebind(TheiaEditorPreviewContribution)
.to(EditorPreviewContribution)
.inSingletonScope();
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
rebind(TheiaMonacoStatusBarContribution)
.to(MonacoStatusBarContribution)
.inSingletonScope();
@@ -477,7 +533,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaPreferencesContribution)
.to(PreferencesContribution)
.inSingletonScope();
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
rebind(TheiaWorkspaceCommandContribution)
.to(WorkspaceCommandContribution)
.inSingletonScope();
@@ -486,11 +541,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) => () => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
@@ -508,6 +564,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SearchInWorkspaceWidget).toSelf();
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).to(EditorManager);
// replace search icon
rebind(TheiaSearchInWorkspaceFactory)
.to(SearchInWorkspaceFactory)
@@ -550,6 +610,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(OutputToolbarContribution).toSelf().inSingletonScope();
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
// To remove `New Window` from the `File` menu
bind(WindowContribution).toSelf().inSingletonScope();
rebind(TheiaWindowContribution).toService(WindowContribution);
bind(ArduinoDaemon)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -559,6 +623,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
.inSingletonScope();
bind(Formatter)
.toDynamicValue(({ container }) =>
WebSocketConnectionProvider.createProxy(container, FormatterPath)
)
.inSingletonScope();
bind(ArduinoFirmwareUploader)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
@@ -626,6 +696,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ArchiveSketch);
Contribution.configure(bind, AddZipLibrary);
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);
Contribution.configure(bind, CompilerErrors);
// 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.
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoFormattingConflictsContribution).toService(
MonacoFormattingConflictsContribution
);
bind(ResponseServiceImpl)
.toSelf()
@@ -655,15 +734,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Enable the dirty indicator on uncloseable widgets.
rebind(TabBarRendererFactory).toFactory((context) => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(
ContextMenuRenderer
);
const contextMenuRenderer =
context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(
TabBarDecoratorService
);
const iconThemeService = context.container.get<IconThemeService>(
IconThemeService
);
const iconThemeService =
context.container.get<IconThemeService>(IconThemeService);
return new TabBarRenderer(
contextMenuRenderer,
decoratorService,
@@ -673,6 +750,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
bind(EditorCommandContribution).toSelf().inSingletonScope();
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
@@ -680,6 +759,26 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
// Do not fetch the `catalog.json` from Azure on FE load.
bind(DefaultJsonSchemaContribution).toSelf().inSingletonScope();
rebind(TheiaDefaultJsonSchemaContribution).toService(
DefaultJsonSchemaContribution
);
// Do not block the app startup when initializing the editor navigation history.
bind(EditorNavigationContribution).toSelf().inSingletonScope();
rebind(TheiaEditorNavigationContribution).toService(
EditorNavigationContribution
);
// IDE2 does not use the Theia preferences widget, no need to create and sync the underlying tree model.
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
rebind(TheiaPreferenceTreeGenerator).toService(PreferenceTreeGenerator);
// IDE2 has a custom about dialog, so there is no need to load the Theia extensions on FE load
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
@@ -692,6 +791,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager);
// Preferences
bindArduinoPreferences(bind);
@@ -799,4 +902,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
})
.inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
});

View File

@@ -1,4 +1,4 @@
import { interfaces } from 'inversify';
import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
@@ -13,6 +13,32 @@ export enum UpdateChannel {
Stable = 'stable',
Nightly = 'nightly',
}
export const ErrorRevealStrategyLiterals = [
/**
* Scroll vertically as necessary and reveal a line.
*/
'auto',
/**
* Scroll vertically as necessary and reveal a line centered vertically.
*/
'center',
/**
* Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition.
*/
'top',
/**
* Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport.
*/
'centerIfOutsideViewport',
] as const;
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
export namespace ErrorRevealStrategy {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export function is(arg: any): arg is ErrorRevealStrategy {
return !!arg && ErrorRevealStrategyLiterals.includes(arg);
}
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
}
export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object',
@@ -33,6 +59,23 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.compile.experimental': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/compile.experimental',
'True if the IDE should handle multiple compiler errors. False by default'
),
default: false,
},
'arduino.compile.revealRange': {
enum: [...ErrorRevealStrategyLiterals],
description: nls.localize(
'arduino/preferences/compile.revealRange',
"Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.",
ErrorRevealStrategy.Default
),
default: ErrorRevealStrategy.Default,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description: nls.localize(
@@ -83,7 +126,7 @@ export const ArduinoConfigSchema: PreferenceSchema = {
default: 'https://downloads.arduino.cc/arduino-ide',
description: nls.localize(
'arduino/preferences/ide.updateBaseUrl',
`The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'`
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
),
},
'arduino.board.certificates': {
@@ -174,12 +217,30 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: 'https://auth.arduino.cc/login#/register',
},
'arduino.survey.notification': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/survey.notification',
'True if users should be notified if a survey is available. True by default.'
),
default: true,
},
'arduino.cli.daemon.debug': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/cli.daemonDebug',
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
),
default: false,
},
},
};
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy;
'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
@@ -198,6 +259,8 @@ export interface ArduinoConfiguration {
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@@ -43,13 +43,15 @@ export class AuthenticationClientService
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
onStart(): void {
async onStart(): Promise<void> {
this.toDispose.push(this.onSessionDidChangeEmitter);
this.service.setClient(this);
this.service
.session()
.then((session) => this.notifySessionDidChange(session));
this.setOptions();
this.setOptions().then(() => this.service.initAuthSession());
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.preferenceName.startsWith('arduino.auth.')) {
this.setOptions();
@@ -57,8 +59,8 @@ export class AuthenticationClientService
});
}
setOptions(): void {
this.service.setOptions({
setOptions(): Promise<void> {
return this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`,
responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'],

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { injectable, inject } from 'inversify';
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
@@ -55,12 +55,13 @@ export class BoardsConfigDialogWidget extends ReactWidget {
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
/>
</div>
);
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();

View File

@@ -1,5 +1,5 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
import { BoardsConfig } from './boards-config';
@@ -26,7 +26,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
constructor(
@inject(BoardsConfigDialogProps)
protected readonly props: BoardsConfigDialogProps
protected override readonly props: BoardsConfigDialogProps
) {
super(props);
@@ -52,7 +52,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
async open(
override async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
@@ -95,7 +95,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
return head;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -110,23 +110,23 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
this.update();
}
protected onUpdateRequest(msg: Message) {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
protected override isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return nls.localize(

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
@@ -16,6 +16,7 @@ import {
} from './boards-service-provider';
import { naturalCompare } from '../../common/utils';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
export namespace BoardsConfig {
export interface Config {
@@ -29,6 +30,7 @@ export namespace BoardsConfig {
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>;
readonly onAppStateDidChange: Event<FrontendApplicationState>;
}
export interface State extends Config {
@@ -47,7 +49,7 @@ export abstract class Item<T> extends React.Component<{
missing?: boolean;
details?: string;
}> {
render(): React.ReactNode {
override render(): React.ReactNode {
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
@@ -99,14 +101,18 @@ export class BoardsConfig extends React.Component<
};
}
componentDidMount() {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
override componentDidMount(): void {
this.toDispose.pushAll([
this.props.onAppStateDidChange((state) => {
if (state === 'ready') {
this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
}
}),
this.props.notificationCenter.onAttachedBoardsChanged((event) =>
this.updatePorts(
event.newState.ports,
@@ -141,11 +147,11 @@ export class BoardsConfig extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
protected fireConfigChanged(): void {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
@@ -250,7 +256,7 @@ export class BoardsConfig extends React.Component<
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<div className="body">
{this.renderContainer('boards', this.renderBoards.bind(this))}

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
@@ -13,6 +13,7 @@ import { BoardsDataStore } from './boards-data-store';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@@ -31,11 +32,20 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.appStateService
.reachedState('ready')
.then(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard
)
);
this.boardsDataStore.onChanged(() =>
this.updateMenuActions(
this.boardsServiceClient.boardsConfig.selectedBoard

View File

@@ -1,4 +1,4 @@
import { injectable, inject, named } from 'inversify';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { Event, Emitter } from '@theia/core/lib/common/event';

View File

@@ -1,4 +1,4 @@
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import {
BoardsPackage,
BoardsService,
@@ -30,7 +30,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
}
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onPlatformInstalled(() =>
@@ -42,7 +42,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
]);
}
protected async install({
protected override async install({
item,
progressId,
version,
@@ -63,7 +63,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
);
}
protected async uninstall({
protected override async uninstall({
item,
progressId,
}: {

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandService } from '@theia/core/lib/common/command';

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
@@ -41,7 +41,7 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
}
}
render(): React.ReactNode {
override render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
@@ -130,13 +130,13 @@ export class BoardsToolBarItem extends React.Component<
});
}
componentDidMount() {
override componentDidMount(): void {
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
@@ -161,7 +161,7 @@ export class BoardsToolBarItem extends React.Component<
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
override render(): React.ReactNode {
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
const title = BoardsConfig.Config.toString(boardsConfig, {

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { BoardsListWidget } from './boards-list-widget';
import { BoardsPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
@@ -18,7 +18,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
});
}
async initializeLayout(): Promise<void> {
override async initializeLayout(): Promise<void> {
this.openView();
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as moment from 'moment';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
@@ -22,13 +22,13 @@ export class About extends Contribution {
@inject(ConfigService)
protected readonly configService: ConfigService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, {
execute: () => this.showAbout(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id,
label: nls.localize(

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
@@ -10,19 +10,20 @@ import {
} from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id,
label: nls.localize('arduino/contributions/addFile', 'Add File') + '...',
@@ -32,7 +33,7 @@ export class AddFile extends SketchContribution {
protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
@@ -28,13 +28,13 @@ export class AddZipLibrary extends SketchContribution {
@inject(LibraryService)
protected readonly libraryService: LibraryService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
execute: () => this.addZipLibrary(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri';
@@ -10,16 +10,17 @@ import {
MenuModelRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: nls.localize('arduino/sketch/archiveSketch', 'Archive Sketch'),
@@ -32,7 +33,7 @@ export class ArchiveSketch extends SketchContribution {
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const archiveBasename = `${sketch.name}-${dateFormat(

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
@@ -47,7 +47,7 @@ export class BoardSelection extends SketchContribution {
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const { selectedBoard, selectedPort } =
@@ -100,19 +100,20 @@ PID: ${PID}`;
});
}
onStart(): void {
override onStart(): void {
this.notificationCenter.onPlatformInstalled(() => this.updateMenus());
this.notificationCenter.onPlatformUninstalled(() => this.updateMenus());
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
this.updateMenus()
);
this.boardsServiceProvider.onAvailablePortsChanged(() =>
this.updateMenus()
);
}
override async onReady(): Promise<void> {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailablePortsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> {

View File

@@ -1,12 +1,9 @@
import { inject, injectable } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { CoreService } from '../../common/protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -14,29 +11,20 @@ import {
import { nls } from '@theia/core/lib/common';
@injectable()
export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
export class BurnBootloader extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
label: nls.localize(
@@ -60,9 +48,15 @@ export class BurnBootloader extends SketchContribution {
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
};
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
fqbn,
board,
programmer,
port,
verify,
@@ -78,15 +72,7 @@ export class BurnBootloader extends SketchContribution {
}
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
} finally {
await this.serialConnection.reconnectAfterUpload();
this.handleError(e);
}
}
}

View File

@@ -1,12 +1,10 @@
import { inject, injectable } from 'inversify';
import { toArray } from '@phosphor/algorithm';
import { inject, injectable } from '@theia/core/shared/inversify';
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 { 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 { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
Command,
@@ -23,90 +21,21 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class Close extends SketchContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
protected shell: ApplicationShell;
onStart(app: FrontendApplication): void {
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Close.Commands.CLOSE, {
execute: async () => {
// Close current editor if closeable.
const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) {
currentEditor.close();
return;
}
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: [
nls.localize(
'vscode/abstractTaskService/saveBeforeRun.dontSave',
"Don't Save"
),
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize(
'vscode/abstractTaskService/saveBeforeRun.save',
'Save'
),
],
message: nls.localize(
'arduino/common/saveChangesToSketch',
'Do you want to save changes to this sketch before closing?'
),
detail: nls.localize(
'arduino/common/loseChanges',
"If you don't save, your changes will be lost."
),
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
window.close();
},
execute: () => remote.getCurrentWindow().close()
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: nls.localize('vscode/editor.contribution/close', 'Close'),
@@ -114,7 +43,7 @@ export class Close extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',

View File

@@ -0,0 +1,656 @@
import {
Command,
CommandRegistry,
Disposable,
DisposableCollection,
Emitter,
MaybePromise,
nls,
notEmpty,
} from '@theia/core';
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Location,
Range,
} from '@theia/core/shared/vscode-languageserver-protocol';
import {
EditorWidget,
TextDocumentChangeEvent,
} from '@theia/editor/lib/browser';
import {
EditorDecoration,
TrackedRangeStickiness,
} from '@theia/editor/lib/browser/decorations/editor-decoration';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
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 { CoreError } from '../../common/protocol/core-service';
import {
ArduinoPreferences,
ErrorRevealStrategy,
} from '../arduino-preferences';
import { InoSelector } from '../ino-selectors';
import { fullRange } from '../utils/monaco';
import { Contribution } from './contribution';
import { CoreErrorHandler } from './core-error-handler';
interface ErrorDecoration {
/**
* This is the unique ID of the decoration given by `monaco`.
*/
readonly id: string;
/**
* The resource this decoration belongs to.
*/
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;
}
return editorProvider(uri).then((editor) => {
if (editor) {
return rangeOf({ id, uri }, editor);
}
return undefined;
});
}
// 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
): boolean {
return left.id === right.id && left.uri === right.uri;
}
}
@injectable()
export class CompilerErrors
extends Contribution
implements monaco.languages.CodeLensProvider
{
@inject(EditorManager)
private readonly editorManager: EditorManager;
@inject(ProtocolToMonacoConverter)
private readonly p2m: ProtocolToMonacoConverter;
@inject(MonacoToProtocolConverter)
private readonly mp2: MonacoToProtocolConverter;
@inject(CoreErrorHandler)
private readonly coreErrorHandler: CoreErrorHandler;
@inject(ArduinoPreferences)
private readonly preferences: ArduinoPreferences;
private readonly errors: ErrorDecoration[] = [];
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
private readonly onCurrentErrorDidChange =
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;
if (!current) {
return -1;
}
return this.errors.findIndex((error) =>
ErrorDecoration.sameAs(error, current)
);
}
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
monaco.languages.registerCodeLensProvider(InoSelector, this);
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
);
this.onCurrentErrorDidChange(async (error) => {
const range = await ErrorDecoration.rangeOf(error, (uri) =>
this.monacoEditor(uri)
);
if (!range) {
console.warn(
'compiler-errors',
`Could not find range of decoration: ${error.id}`
);
return;
}
const editor = await this.revealLocationInEditor({
uri: error.uri,
range: this.mp2.asRange(range),
});
if (!editor) {
console.warn(
'compiler-errors',
`Failed to mark error ${error.id} as the current one.`
);
}
});
this.preferences.ready.then(() => {
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.compile.revealRange') {
this.revealStrategy = ErrorRevealStrategy.is(newValue)
? newValue
: ErrorRevealStrategy.Default;
}
});
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to next error. Unknown current error.`
);
return;
}
const nextError =
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
this.markAsCurrentError(nextError);
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
});
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
execute: () => {
const index = this.currentErrorIndex;
if (index < 0) {
console.warn(
'compiler-errors',
`Could not advance to previous error. Unknown current error.`
);
return;
}
const previousError =
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
this.markAsCurrentError(previousError);
},
isEnabled: () => !!this.currentError && this.errors.length > 1,
});
}
get onDidChange(): monaco.IEvent<this> {
return this.onDidChangeEmitter.event;
}
async provideCodeLenses(
model: monaco.editor.ITextModel,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.CodeLensList> {
const lenses: monaco.languages.CodeLens[] = [];
if (
this.currentError &&
this.currentError.uri === model.uri.toString() &&
this.errors.length > 1
) {
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
this.monacoEditor(uri)
);
if (range) {
lenses.push(
{
range,
command: {
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
title: nls.localize(
'arduino/editor/previousError',
'Previous Error'
),
arguments: [this.currentError],
},
},
{
range,
command: {
id: CompilerErrors.Commands.NEXT_ERROR.id,
title: nls.localize('arduino/editor/nextError', 'Next Error'),
arguments: [this.currentError],
},
}
);
}
}
return {
lenses,
dispose: () => {
/* NOOP */
},
};
}
private async handleCompilerErrorsDidChange(
errors: CoreError.Compiler[]
): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose();
const compilerErrorsPerResource = this.groupByResource(
await this.filter(errors)
);
const decorations = await this.decorateEditors(compilerErrorsPerResource);
this.errors.push(...decorations.errors);
this.toDisposeOnCompilerErrorDidChange.pushAll([
Disposable.create(() => (this.errors.length = 0)),
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
...(await Promise.all([
decorations.dispose,
this.trackEditors(
compilerErrorsPerResource,
(editor) =>
editor.editor.onSelectionChanged((selection) =>
this.handleSelectionChange(editor, selection)
),
(editor) =>
editor.onDidDispose(() =>
this.handleEditorDidDispose(editor.editor.uri.toString())
),
(editor) =>
editor.editor.onDocumentContentChanged((event) =>
this.handleDocumentContentChange(editor, event)
)
),
])),
]);
const currentError = this.errors[0];
if (currentError) {
await this.markAsCurrentError(currentError);
}
}
private async filter(
errors: CoreError.Compiler[]
): Promise<CoreError.Compiler[]> {
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.Compiler[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const composite = await Promise.all(
[...errors.entries()].map(([uri, errors]) =>
this.decorateEditor(uri, errors)
)
);
return {
dispose: new DisposableCollection(
...composite.map(({ dispose }) => dispose)
),
errors: composite.reduce(
(acc, { errors }) => acc.concat(errors),
[] as ErrorDecoration[]
),
};
}
private async decorateEditor(
uri: string,
errors: CoreError.Compiler[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) {
return { dispose: Disposable.NULL, errors: [] };
}
const oldDecorations = editor.editor.deltaDecorations({
oldDecorations: [],
newDecorations: errors.map((error) =>
this.compilerErrorDecoration(error.location.range)
),
});
return {
dispose: Disposable.create(() => {
if (editor) {
editor.editor.deltaDecorations({
oldDecorations,
newDecorations: [],
});
}
}),
errors: oldDecorations.map((id) => ({ id, uri })),
};
}
private compilerErrorDecoration(range: Range): EditorDecoration {
return {
range,
options: {
isWholeLine: true,
className: 'compiler-error',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
},
};
}
/**
* 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;
}
const uri = monacoEditor.uri.toString();
const monacoSelection = this.p2m.asRange(selection);
console.log(
'compiler-errors',
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
);
const calculatePriority = (
candidateErrorRange: monaco.Range,
currentSelection: monaco.Range
) => {
console.trace(
'compiler-errors',
`Candidate error range: ${candidateErrorRange.toJSON()}`
);
console.trace(
'compiler-errors',
`Current selection range: ${currentSelection.toJSON()}`
);
if (candidateErrorRange.intersectRanges(currentSelection)) {
console.trace('Intersects.');
return { score: 2 };
}
if (
candidateErrorRange.startLineNumber <=
currentSelection.startLineNumber &&
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
) {
console.trace('Same line.');
return { score: 1 };
}
console.trace('No match');
return undefined;
};
const error = this.errors
.filter((error) => error.uri === uri)
.map((error) => ({
error,
range: ErrorDecoration.rangeOf(error, monacoEditor),
}))
.map(({ error, range }) => {
if (range) {
const priority = calculatePriority(range, monacoSelection);
if (priority) {
return { ...priority, error };
}
}
return undefined;
})
.filter(notEmpty)
.sort((left, right) => right.score - left.score) // highest first
.map(({ error }) => error)
.shift();
if (error) {
this.markAsCurrentError(error);
} else {
console.info(
'compiler-errors',
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
);
}
}
/**
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
* If editor closes, delete the decorators.
*/
private handleEditorDidDispose(uri: string): void {
let i = this.errors.length;
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
while (i--) {
const error = this.errors[i];
if (error.uri === uri) {
this.errors.splice(i, 1);
}
}
this.onDidChangeEmitter.fire(this);
}
/**
* If a document change "destroys" the range of the decoration, the decoration must be removed.
*/
private handleDocumentContentChange(
editor: EditorWidget,
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
);
if (!relevantChanges.length) {
return;
}
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
.map(({ range }) => this.p2m.asRange(range))
.map((changeRange) =>
resolvedMarkers.filter(({ range: decorationRange }) =>
changeRange.containsRange(decorationRange)
)
)
.reduce((acc, curr) => acc.concat(curr), [])
.map(({ error, index }) => {
this.errors.splice(index, 1);
return error.id;
});
if (!decorationIdsToRemove.length) {
return;
}
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
this.onDidChangeEmitter.fire(this);
}
private async trackEditors(
errors: Map<string, CoreError.Compiler[]>,
...track: ((editor: EditorWidget) => 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));
if (!editor) {
return Disposable.NULL;
}
return new DisposableCollection(...track.map((t) => t(editor)));
})
))
);
}
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
const index = this.errors.findIndex((candidate) =>
ErrorDecoration.sameAs(candidate, error)
);
if (index < 0) {
console.warn(
'compiler-errors',
`Failed to mark error ${
error.id
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
({ id }) => id
)}`
);
return;
}
const newError = this.errors[index];
if (
!this.currentError ||
!ErrorDecoration.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);
this.onDidChangeEmitter.fire(this);
}
}
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
private async revealLocationInEditor(
location: Location
): Promise<EditorWidget | undefined> {
const { uri, range } = location;
const editor = await this.editorManager.getByUri(new URI(uri), {
mode: 'activate',
});
if (editor && this.shell) {
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
editor.editor.revealRange(range, { at: this.revealStrategy });
const activeWidget = await this.shell.activateWidget(editor.id);
if (!activeWidget) {
console.warn(
'compiler-errors',
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
);
return editor;
}
if (editor !== activeWidget) {
console.warn(
'compiler-errors',
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
);
}
return editor;
}
console.warn(
'compiler-errors',
`could not found editor widget for URI: ${uri}`
);
return undefined;
}
private groupByResource(
errors: CoreError.Compiler[]
): Map<string, CoreError.Compiler[]> {
return errors.reduce((acc, curr) => {
const {
location: { uri },
} = curr;
let errors = acc.get(uri);
if (!errors) {
errors = [];
acc.set(uri, errors);
}
errors.push(curr);
return acc;
}, new Map<string, CoreError.Compiler[]>());
}
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
private monacoEditor(
uriOrWidget: string | EditorWidget
): MaybePromise<MonacoEditor | undefined> {
if (uriOrWidget instanceof EditorWidget) {
const editor = uriOrWidget.editor;
if (editor instanceof MonacoEditor) {
return editor;
}
return undefined;
} else {
return this.editorManager
.getByUri(new URI(uriOrWidget))
.then((editor) => {
if (editor) {
return this.monacoEditor(editor);
}
return undefined;
});
}
}
}
export namespace CompilerErrors {
export namespace Commands {
export const NEXT_ERROR: Command = {
id: 'arduino-editor-next-error',
};
export const PREVIOUS_ERROR: Command = {
id: 'arduino-editor-previous-error',
};
}
}

View File

@@ -1,4 +1,9 @@
import { inject, injectable, interfaces } from 'inversify';
import {
inject,
injectable,
interfaces,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { Saveable } from '@theia/core/lib/browser/saveable';
@@ -9,7 +14,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
MenuModelRegistry,
MenuContribution,
@@ -34,14 +39,24 @@ import {
} from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode';
import { SettingsService } from '../dialogs/settings/settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import {
SketchesService,
ConfigService,
FileSystemExt,
Sketch,
CoreService,
CoreError,
} from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { CoreErrorHandler } from './core-error-handler';
import { nls } from '@theia/core';
import { OutputChannelManager } from '../theia/output/output-channel';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
export {
Command,
@@ -84,15 +99,31 @@ export abstract class Contribution
@inject(SettingsService)
protected readonly settingsService: SettingsService;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@postConstruct()
protected init(): void {
this.appStateService.reachedState('ready').then(() => this.onReady());
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
onStart(app: FrontendApplication): MaybePromise<void> {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerCommands(registry: CommandRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerMenus(registry: MenuModelRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerKeybindings(registry: KeybindingRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
registerToolbarItems(registry: TabBarToolbarRegistry): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onReady(): MaybePromise<void> {}
}
@injectable()
@@ -127,7 +158,7 @@ export abstract class SketchContribution extends Contribution {
protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) {
if (CurrentSketch.isValid(sketch)) {
for (const editor of this.editorManager.all) {
const uri = editor.editor.uri;
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
@@ -139,8 +170,58 @@ export abstract class SketchContribution extends Contribution {
}
}
@injectable()
export class CoreServiceContribution extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(CoreErrorHandler)
protected readonly coreErrorHandler: CoreErrorHandler;
@inject(ClipboardService)
private readonly clipboardService: ClipboardService;
protected handleError(error: unknown): void {
this.coreErrorHandler.tryHandle(error);
this.tryToastErrorMessage(error);
}
private tryToastErrorMessage(error: unknown): void {
let message: undefined | string = undefined;
if (CoreError.is(error)) {
message = error.message;
} else if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
} else {
try {
message = JSON.stringify(error);
} catch {}
}
if (message) {
const copyAction = nls.localize(
'arduino/coreContribution/copyError',
'Copy error messages'
);
this.messageService.error(message, copyAction).then(async (action) => {
if (action === copyAction) {
const content = await this.outputChannelManager.contentOfChannel(
'Arduino'
);
if (content) {
this.clipboardService.writeText(content);
}
}
});
} else {
throw error;
}
}
}
export namespace Contribution {
export function configure<T>(
export function configure(
bind: interfaces.Bind,
serviceIdentifier: typeof Contribution
): void {

View File

@@ -0,0 +1,32 @@
import { Emitter, Event } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import { CoreError } from '../../common/protocol/core-service';
@injectable()
export class CoreErrorHandler {
private readonly compilerErrors: CoreError.Compiler[] = [];
private readonly compilerErrorsDidChangeEmitter = new Emitter<
CoreError.Compiler[]
>();
tryHandle(error: unknown): void {
if (CoreError.is(error)) {
this.compilerErrors.length = 0;
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
this.fireCompilerErrorsDidChange();
}
}
reset(): void {
this.compilerErrors.length = 0;
this.fireCompilerErrorsDidChange();
}
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
return this.compilerErrorsDidChangeEmitter.event;
}
private fireCompilerErrorsDidChange(): void {
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -12,7 +12,8 @@ import {
SketchContribution,
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { MaybePromise, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class Debug extends SketchContribution {
@@ -66,7 +67,7 @@ export class Debug extends SketchContribution {
onDidChange: this.onDisabledMessageDidChange as Event<void>,
};
onStart(): void {
override onStart(): void {
this.onDisabledMessageDidChange(
() =>
(this.debugToolbarItem.tooltip = `${
@@ -79,55 +80,18 @@ export class Debug extends SketchContribution {
: Debug.Commands.START_DEBUGGING.label
}`)
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
};
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
this.refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
this.notificationCenter.onPlatformInstalled(() => this.refreshState());
this.notificationCenter.onPlatformUninstalled(() => this.refreshState());
}
registerCommands(registry: CommandRegistry): void {
override onReady(): MaybePromise<void> {
this.refreshState();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
execute: () => this.startDebug(),
isVisible: (widget) =>
@@ -136,10 +100,51 @@ export class Debug extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem(this.debugToolbarItem);
}
private async refreshState(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
}
protected async startDebug(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
@@ -156,7 +161,7 @@ export class Debug extends SketchContribution {
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
@@ -12,6 +12,8 @@ import {
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
// Depends on https://github.com/eclipse-theia/theia/pull/7964
@@ -26,7 +28,7 @@ export class EditContributions extends Contribution {
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
execute: () => this.run('editor.action.gotoLine'),
});
@@ -91,7 +93,7 @@ ${value}
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.CUT.id,
order: '0',
@@ -199,7 +201,7 @@ ${value}
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
@@ -250,10 +252,10 @@ ${value}
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
protected async current(): Promise<ICodeEditor | StandaloneCodeEditor | undefined> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
this.codeEditorService.getActiveCodeEditor() || undefined
);
}

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import {
MenuPath,
@@ -21,7 +21,7 @@ import {
MenuModelRegistry,
} from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board, Sketch, SketchContainer } from '../../common/protocol';
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable()
@@ -43,8 +43,8 @@ export abstract class Examples extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
@postConstruct()
init(): void {
protected override init(): void {
super.init();
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard)
);
@@ -54,7 +54,7 @@ export abstract class Examples extends SketchContribution {
// NOOP
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
@@ -82,7 +82,7 @@ export abstract class Examples extends SketchContribution {
registerRecursively(
sketchContainerOrPlaceholder:
| SketchContainer
| (Sketch | SketchContainer)[]
| (SketchRef | SketchContainer)[]
| string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
@@ -100,7 +100,7 @@ export abstract class Examples extends SketchContribution {
)
);
} else {
const sketches: Sketch[] = [];
const sketches: SketchRef[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
@@ -161,7 +161,7 @@ export abstract class Examples extends SketchContribution {
@injectable()
export class BuiltInExamples extends Examples {
onStart(): void {
override async onReady(): Promise<void> {
this.register(); // no `await`
}
@@ -201,13 +201,16 @@ export class LibraryExamples extends Examples {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
onStart(): void {
this.register(); // no `await`
override onStart(): void {
this.notificationCenter.onLibraryInstalled(() => this.register());
this.notificationCenter.onLibraryUninstalled(() => this.register());
}
protected handleBoardChanged(board: Board | undefined): void {
override async onReady(): Promise<void> {
this.register(); // no `await`
}
protected override handleBoardChanged(board: Board | undefined): void {
this.register(board);
}

View File

@@ -0,0 +1,79 @@
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 { Contribution, URI } from './contribution';
@injectable()
export class Format
extends Contribution
implements
monaco.languages.DocumentRangeFormattingEditProvider,
monaco.languages.DocumentFormattingEditProvider
{
@inject(Formatter)
private readonly formatter: Formatter;
override onStart(): MaybePromise<void> {
monaco.languages.registerDocumentRangeFormattingEditProvider(
InoSelector,
this
);
monaco.languages.registerDocumentFormattingEditProvider(InoSelector, this);
}
async provideDocumentRangeFormattingEdits(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const text = await this.format(model, range, options);
return [{ range, text }];
}
async provideDocumentFormattingEdits(
model: monaco.editor.ITextModel,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const range = fullRange(model);
const text = await this.format(model, range, options);
return [{ range, text }];
}
/**
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
* folder locations where the `.clang-format` file could be.
*/
private formatterConfigFolderUris(model: monaco.editor.ITextModel): string[] {
const editorUri = new URI(model.uri.toString());
return this.workspaceService
.tryGetRoots()
.map(({ resource }) => resource)
.filter((workspaceUri) => workspaceUri.isEqualOrParent(editorUri))
.map((uri) => uri.toString());
}
private format(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions
): Promise<string> {
console.info(
`Formatting ${model.uri.toString()} [Range: ${JSON.stringify(
range.toJSON()
)}]`
);
const content = model.getValueInRange(range);
const formatterConfigFolderUris = this.formatterConfigFolderUris(model);
return this.formatter.format({
content,
formatterConfigFolderUris,
options,
});
}
}

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@@ -15,6 +15,7 @@ import {
import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from '../ide-updater/ide-updater-commands';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class Help extends Contribution {
@@ -27,7 +28,7 @@ export class Help extends Contribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
const open = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) =>
@@ -91,7 +92,7 @@ export class Help extends Contribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.unregisterMenuAction({
commandId: ElectronCommands.TOGGLE_DEVELOPER_TOOLS.id,
});
@@ -135,7 +136,7 @@ export class Help extends Contribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F',

View File

@@ -1,5 +1,5 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser';
@@ -16,6 +16,8 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class IncludeLibrary extends SketchContribution {
@@ -29,7 +31,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
@@ -43,8 +45,7 @@ export class IncludeLibrary extends SketchContribution {
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection();
onStart(): void {
this.updateMenuActions();
override onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(() =>
this.updateMenuActions()
);
@@ -54,7 +55,11 @@ export class IncludeLibrary extends SketchContribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.updateMenuActions();
}
override registerMenus(registry: MenuModelRegistry): void {
// `Include Library` submenu
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
@@ -77,7 +82,7 @@ export class IncludeLibrary extends SketchContribution {
});
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
@@ -168,7 +173,7 @@ export class IncludeLibrary extends SketchContribution {
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
@@ -14,7 +14,7 @@ import {
@injectable()
export class NewSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(),
});
@@ -25,7 +25,7 @@ export class NewSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: nls.localize('arduino/sketch/new', 'New'),
@@ -33,14 +33,14 @@ export class NewSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import {
Disposable,
@@ -35,18 +35,19 @@ export class OpenRecentSketch extends SketchContribution {
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void {
const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches);
this.mainMenuManager.update();
};
override onStart(): void {
this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
refreshMenu(sketches)
this.refreshMenu(sketches)
);
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.sketchService
.recentlyOpenedSketches()
.then((sketches) => this.refreshMenu(sketches));
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
nls.localize('arduino/sketch/openRecent', 'Open Recent'),
@@ -54,6 +55,11 @@ export class OpenRecentSketch extends SketchContribution {
);
}
private refreshMenu(sketches: Sketch[]): void {
this.register(sketches);
this.mainMenuManager.update();
}
protected register(sketches: Sketch[]): void {
const order = 0;
for (const sketch of sketches) {

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -13,13 +13,13 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class OpenSketchExternal extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
execute: () => this.openExternal(),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
label: nls.localize('arduino/sketch/showFolder', 'Show Sketch Folder'),
@@ -27,7 +27,7 @@ export class OpenSketchExternal extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
keybinding: 'CtrlCmd+Alt+K',

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
@@ -43,7 +43,7 @@ export class OpenSketch extends SketchContribution {
protected readonly toDispose = new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: (arg) =>
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
@@ -116,7 +116,7 @@ export class OpenSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
@@ -124,14 +124,14 @@ export class OpenSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { isOSX } from '@theia/core/lib/common/os';
import {
@@ -13,7 +13,7 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class QuitApp extends Contribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit(),
@@ -21,7 +21,7 @@ export class QuitApp extends Contribution {
}
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
@@ -32,7 +32,7 @@ export class QuitApp extends Contribution {
}
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
if (!isOSX) {
registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -14,6 +14,7 @@ import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveAsSketch extends SketchContribution {
@@ -22,18 +23,18 @@ export class SaveAsSketch extends SketchContribution {
protected readonly applicationShell: ApplicationShell;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(WindowService)
protected readonly windowService: WindowService;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: (args) => this.saveAs(args),
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
@@ -41,7 +42,7 @@ export class SaveAsSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S',
@@ -59,7 +60,7 @@ export class SaveAsSketch extends SketchContribution {
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return false;
}

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -12,10 +12,11 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(),
});
@@ -27,7 +28,7 @@ export class SaveSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: nls.localize('vscode/fileCommands/save', 'Save'),
@@ -35,14 +36,14 @@ export class SaveSketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
@@ -53,7 +54,7 @@ export class SaveSketch extends SketchContribution {
async saveSketch(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -18,7 +18,7 @@ export class Settings extends SketchContribution {
protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => {
let settings: Preferences | undefined = undefined;
@@ -39,7 +39,7 @@ export class Settings extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id,
label:
@@ -52,7 +52,7 @@ export class Settings extends SketchContribution {
registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,',

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
@@ -19,7 +19,10 @@ import {
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { nls } from '@theia/core/lib/common';
@@ -35,7 +38,7 @@ export class SketchControl extends SketchContribution {
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
protected override readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@@ -46,7 +49,7 @@ export class SketchControl extends SketchContribution {
protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection();
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
{
@@ -55,7 +58,7 @@ export class SketchControl extends SketchContribution {
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
@@ -70,25 +73,22 @@ export class SketchControl extends SketchContribution {
return;
}
const { mainFileUri, rootFolderFileUris } =
await this.sketchService.loadSketch(sketch.uri);
const { mainFileUri, rootFolderFileUris } = sketch;
const uris = [mainFileUri, ...rootFolderFileUris];
const currentSketch =
await this.sketchesServiceClient.currentSketch();
const parentsketchUri = this.editorManager.currentEditor
const parentSketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentsketch = await this.sketchService.getSketchFolder(
parentsketchUri || ''
const parentSketch = await this.sketchService.getSketchFolder(
parentSketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
this.allowRename(parentsketch.uri)
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowRename(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
@@ -122,10 +122,10 @@ export class SketchControl extends SketchContribution {
}
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
this.allowDelete(parentsketch.uri)
sketch &&
parentSketch &&
parentSketch.uri === sketch.uri &&
this.allowDelete(parentSketch.uri)
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
@@ -200,7 +200,7 @@ export class SketchControl extends SketchContribution {
);
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
@@ -228,7 +228,7 @@ export class SketchControl extends SketchContribution {
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
@@ -243,7 +243,7 @@ export class SketchControl extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import { CommandRegistry, MenuModelRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -12,10 +12,10 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class Sketchbook extends Examples {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
protected override readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
protected override readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@@ -23,11 +23,7 @@ export class Sketchbook extends Examples {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
onStart(): void {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
});
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
@@ -36,7 +32,14 @@ export class Sketchbook extends Examples {
});
}
registerMenus(registry: MenuModelRegistry): void {
override async onReady(): Promise<void> {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
nls.localize('arduino/sketch/sketchbook', 'Sketchbook'),
@@ -53,7 +56,7 @@ export class Sketchbook extends Examples {
);
}
protected createHandler(uri: string): CommandHandler {
protected override createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);

View File

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

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -39,7 +39,7 @@ export class UploadCertificate extends Contribution {
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadCertificate.Commands.OPEN, {
execute: async () => {
try {
@@ -93,7 +93,7 @@ export class UploadCertificate extends Contribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadCertificate.Commands.OPEN.id,
label: UploadCertificate.Commands.OPEN.label,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Command,
MenuModelRegistry,
@@ -16,7 +16,7 @@ export class UploadFirmware extends Contribution {
protected dialogOpened = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadFirmware.Commands.OPEN, {
execute: async () => {
try {
@@ -30,7 +30,7 @@ export class UploadFirmware extends Contribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
commandId: UploadFirmware.Commands.OPEN.id,
label: UploadFirmware.Commands.OPEN.label,

View File

@@ -1,13 +1,12 @@
import { inject, injectable, postConstruct } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -16,15 +15,10 @@ import {
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class UploadSketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@@ -47,8 +41,8 @@ export class UploadSketch extends SketchContribution {
protected readonly menuActionsDisposables = new DisposableCollection();
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceClientImpl.selectedBoardUserFields();
@@ -72,7 +66,7 @@ export class UploadSketch extends SketchContribution {
return fqbn + '|' + address;
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
@@ -134,7 +128,7 @@ export class UploadSketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
@@ -177,7 +171,7 @@ export class UploadSketch extends SketchContribution {
);
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U',
@@ -188,7 +182,7 @@ export class UploadSketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
@@ -204,16 +198,17 @@ export class UploadSketch extends SketchContribution {
return;
}
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
// toggle the toolbar button and menu item state.
// uploadInProgress will be set to false whether the upload fails or not
this.uploadInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
await Promise.all([
@@ -226,8 +221,12 @@ export class UploadSketch extends SketchContribution {
this.sourceOverride(),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
};
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort;
@@ -246,8 +245,8 @@ export class UploadSketch extends SketchContribution {
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
sketch,
board,
optimizeForDebug,
programmer,
port,
@@ -258,8 +257,8 @@ export class UploadSketch extends SketchContribution {
};
} else {
options = {
sketchUri,
fqbn,
sketch,
board,
optimizeForDebug,
port,
verbose,
@@ -279,18 +278,10 @@ export class UploadSketch extends SketchContribution {
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = '';
if (typeof e === 'string') {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
}
}
}

View File

@@ -1,12 +1,11 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
CoreServiceContribution,
Command,
CommandRegistry,
MenuModelRegistry,
@@ -14,12 +13,10 @@ import {
TabBarToolbarRegistry,
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
@injectable()
export class VerifySketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
export class VerifySketch extends CoreServiceContribution {
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@@ -31,7 +28,7 @@ export class VerifySketch extends SketchContribution {
protected verifyInProgress = false;
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch(),
isEnabled: () => !this.verifyInProgress,
@@ -50,7 +47,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerMenus(registry: MenuModelRegistry): void {
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: nls.localize('arduino/sketch/verifyOrCompile', 'Verify/Compile'),
@@ -66,7 +63,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerKeybindings(registry: KeybindingRegistry): void {
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R',
@@ -77,7 +74,7 @@ export class VerifySketch extends SketchContribution {
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
@@ -95,14 +92,14 @@ export class VerifySketch extends SketchContribution {
// toggle the toolbar button and menu item state.
// verifyInProgress will be set to false whether the compilation fails or not
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
if (!CurrentSketch.isValid(sketch)) {
return;
}
try {
this.verifyInProgress = true;
this.coreErrorHandler.reset();
this.onDidChangeEmitter.fire();
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
@@ -110,12 +107,17 @@ export class VerifySketch extends SketchContribution {
),
this.sourceOverride(),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
};
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
fqbn,
sketch,
board,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
@@ -127,13 +129,7 @@ export class VerifySketch extends SketchContribution {
{ timeout: 3000 }
);
} catch (e) {
let errorMessage = "";
if (typeof e === "string") {
errorMessage = e;
} else {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
this.handleError(e);
} finally {
this.verifyInProgress = false;
this.onDidChangeEmitter.fire();

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
@@ -117,11 +117,11 @@ export class CreateApi {
headers,
})
).sketches;
if (partialSketches.length != 0) {
if (partialSketches.length !== 0) {
result.sketches = result.sketches.concat(partialSketches);
}
currentOffset = currentOffset + limit;
} while (partialSketches.length != 0);
} while (partialSketches.length !== 0);
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
return result.sketches;

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Event } from '@theia/core/lib/common/event';
import {

View File

@@ -0,0 +1,149 @@
{
"name": "Arduino dark",
"type": "dark",
"colors": {
"list.highlightForeground": "#0ca1a6",
"list.activeSelectionForeground": "#dae3e3",
"list.activeSelectionBackground": "#434f54",
"list.inactiveSelectionForeground": "#dae3e3",
"list.inactiveSelectionBackground": "#434f54",
"list.hoverBackground": "#1f272a",
"progressBar.background": "#005c5f",
"editor.background": "#1f272a",
"editor.foreground": "#dae3e3",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#f1c40f",
"editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#171e21",
"focusBorder": "#dae3e3",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",
"menu.selectionBackground": "#dae3e3",
"menu.selectionForeground": "#212121",
"editorGroupHeader.tabsBackground": "#171e21",
"button.background": "#0ca1a6",
"titleBar.activeBackground": "#171e21",
"titleBar.activeForeground": "#dae3e3",
"terminal.background": "#000000",
"terminal.foreground": "#e0e0e0",
"dropdown.border": "#7fcbcd",
"dropdown.background": "#2c353a",
"dropdown.foreground": "#dae3e3",
"activityBar.background": "#171e21",
"activityBar.foreground": "#dae3e3",
"activityBar.inactiveForeground": "#4e5b61",
"activityBar.activeBorder": "#0ca1a6",
"statusBar.background": "#171e21",
"secondaryButton.background": "#ff000000",
"secondaryButton.foreground": "#dae3e3",
"secondaryButton.hoverBackground": "#434f54",
"arduino.branding.primary": "#0ca1a6",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#ffffff",
"arduino.output.background": "#000000",
"arduino.toolbar.hoverBackground": "#dae3e3",
"sideBar.background": "#101618",
"input.background": "#000000",
"foreground": "#dae3e3",
"settings.headerForeground": "#dae3e3",
"tree.indentGuidesStroke": "#374146",
"tab.unfocusedActiveForeground": "#dae3e3",
"tab.inactiveBackground": "#171e21"
},
"tokenColors": [
{
"name": "",
"settings": {
"foreground": "#dae3e3"
}
},
{
"name": "Comments",
"scope": "comment",
"settings": {
"foreground": "#7f8c8d"
}
},
{
"name": "Keywords Attributes",
"scope": [
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"foreground": "#0ca1a6"
}
},
{
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"meta.function-call.c",
"variable.other"
],
"settings": {
"foreground": "#F39C12"
}
},
{
"name": "punctuation",
"scope": [
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"variable",
"variable.name"
],
"settings": {
"foreground": "#dae3e3"
}
},
{
"name": "function preprocessor",
"scope": [
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
],
"settings": {
"foreground": "#569CD6"
}
},
{
"name": "constants",
"scope": [
"string.quoted.double",
"string.quoted.other.lt-gt",
"constant"
],
"settings": {
"foreground": "#7fcbcd"
}
},
{
"name": "meta keywords",
"scope": [
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#C586C0"
}
},
{
"name": "numeric preprocessor",
"scope": [
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#434f54"
}
}
]
}

View File

@@ -0,0 +1,149 @@
{
"name": "Arduino default",
"type": "default",
"colors": {
"list.highlightForeground": "#008184",
"list.activeSelectionForeground": "#4e5b61",
"list.activeSelectionBackground": "#dae3e3",
"list.inactiveSelectionForeground": "#4e5b61",
"list.inactiveSelectionBackground": "#dae3e3",
"list.hoverBackground": "#ecf1f1",
"progressBar.background": "#005c5f",
"editor.background": "#ffffff",
"editor.foreground": "#4e5b61",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#f1c40f",
"editorCursor.foreground": "#434f54",
"editorWhitespace.foreground": "#bfbfbf",
"editorWidget.background": "#f7f9f9",
"focusBorder": "#7fcbcd",
"menubar.selectionBackground": "#ffffff",
"menubar.selectionForeground": "#212121",
"menu.selectionBackground": "#dae3e3",
"menu.selectionForeground": "#212121",
"editorGroupHeader.tabsBackground": "#ecf1f1",
"button.background": "#7fcbcd",
"titleBar.activeBackground": "#006d70",
"titleBar.activeForeground": "#f7f9f9",
"terminal.background": "#000000",
"terminal.foreground": "#e0e0e0",
"dropdown.border": "#f7f9f9",
"dropdown.background": "#ffffff",
"dropdown.foreground": "#4e5b61",
"activityBar.background": "#ecf1f1",
"activityBar.foreground": "#4e5b61",
"activityBar.inactiveForeground": "#bdc7c7",
"activityBar.activeBorder": "#008184",
"statusBar.background": "#006d70",
"secondaryButton.background": "#ff000000",
"secondaryButton.foreground": "#008184",
"secondaryButton.hoverBackground": "#dae3e3",
"arduino.branding.primary": "#008184",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#ffffff",
"arduino.output.background": "#000000",
"arduino.toolbar.hoverBackground": "#f7f9f9",
"sideBar.background": "#f7f9f9",
"input.background": "#ffffff",
"foreground": "#4e5b61",
"settings.headerForeground": "#4e5b61",
"tree.indentGuidesStroke": "#dae3e3",
"tab.unfocusedActiveForeground": "#4e5b61",
"tab.inactiveBackground": "#ecf1f1"
},
"tokenColors": [
{
"name": "",
"settings": {
"foreground": "#434f54"
}
},
{
"name": "Comments",
"scope": "comment",
"settings": {
"foreground": "#95a5a6cc"
}
},
{
"name": "Keywords Attributes",
"scope": [
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"foreground": "#00979D"
}
},
{
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"meta.function-call.c",
"variable.other"
],
"settings": {
"foreground": "#D35400"
}
},
{
"name": "punctuation",
"scope": [
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"variable",
"variable.name"
],
"settings": {
"foreground": "#434f54"
}
},
{
"name": "function preprocessor",
"scope": [
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
],
"settings": {
"foreground": "#9e846d"
}
},
{
"name": "constants",
"scope": [
"string.quoted.double",
"string.quoted.other.lt-gt",
"constant"
],
"settings": {
"foreground": "#005C5F"
}
},
{
"name": "meta keywords",
"scope": [
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#728E00"
}
},
{
"name": "numeric preprocessor",
"scope": [
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"foreground": "#434f54"
}
}
]
}

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import * as React from '@theia/core/shared/react';
export const CertificateAddComponent = ({
addCertificate,
@@ -8,9 +8,12 @@ export const CertificateAddComponent = ({
}): React.ReactElement => {
const [value, setValue] = React.useState('');
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
const handleChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[]
);
return (
<form

View File

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

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import Tippy from '@tippyjs/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { CertificateListComponent } from './certificate-list';

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from '@theia/core/shared/react';
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 '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
@@ -139,7 +139,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
constructor(
@inject(UploadCertificateDialogProps)
protected readonly props: UploadCertificateDialogProps
protected override readonly props: UploadCertificateDialogProps
) {
super({
title: nls.localize(
@@ -155,7 +155,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -165,21 +165,21 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
override close(): void {
if (this.busy) {
return;
}

View File

@@ -1,5 +1,5 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { AvailableBoard } from '../../boards/boards-service-provider';
import { ArduinoSelect } from '../../widgets/arduino-select';

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { clipboard } from 'electron';
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
import { AbstractDialog } from '../theia/dialogs/dialogs';
@@ -149,7 +149,7 @@ export class ShareSketchDialog extends AbstractDialog<void> {
constructor(
@inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps
protected override readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
@@ -159,7 +159,7 @@ export class ShareSketchDialog extends AbstractDialog<void> {
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -168,12 +168,12 @@ export class ShareSketchDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
ConfirmDialog,
@@ -19,7 +19,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
constructor(
@inject(DoNotAskAgainDialogProps)
protected readonly props: DoNotAskAgainDialogProps
protected override readonly props: DoNotAskAgainDialogProps
) {
super(props);
this.controlPanel.removeChild(this.errorMessageNode);
@@ -42,7 +42,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
this.doNotAskAgainCheckbox.type = 'checkbox';
}
protected async accept(): Promise<void> {
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
@@ -65,7 +65,7 @@ export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
}
}
protected setErrorMessage(error: DialogError): void {
protected override setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}

View File

@@ -1,5 +1,6 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Port } from '../../../common/protocol';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
@@ -20,7 +21,7 @@ export const FirmwareUploaderComponent = ({
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
isOpen: any;
}): React.ReactElement => {
// boolean states for buttons
@@ -81,7 +82,7 @@ export const FirmwareUploaderComponent = ({
const installStatus =
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
(await flashFirmware(firmwareToFlash, selectedBoard?.port));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {

View File

@@ -1,9 +1,13 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from '@theia/core/shared/react';
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 '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
@@ -15,6 +19,8 @@ import {
} from '../../../common/protocol/arduino-firmware-uploader';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@@ -24,6 +30,9 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
@@ -38,7 +47,8 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
@postConstruct()
protected init(): void {
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
this.appStatusService.reachedState('ready').then(async () => {
const fqbns = await this.arduinoFirmwareUploader.updatableBoards();
this.updatableFqbns = fqbns;
this.update();
});
@@ -49,14 +59,14 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
});
}
protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
onCloseRequest(msg: Message): void {
protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
@@ -88,7 +98,7 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
constructor(
@inject(UploadFirmwareDialogProps)
protected readonly props: UploadFirmwareDialogProps
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.contentNode.classList.add('firmware-uploader-dialog');
@@ -99,7 +109,7 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
return;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -109,21 +119,21 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
close(): void {
override close(): void {
if (this.busy) {
return;
}

View File

@@ -1,8 +1,8 @@
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 'react';
import * as ReactDOM from 'react-dom';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
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';
@@ -70,7 +70,7 @@ export class IDEUpdaterDialogWidget extends ReactWidget {
this.close();
}
close(): void {
override close(): void {
super.close();
this.onClose();
}
@@ -122,7 +122,7 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
constructor(
@inject(IDEUpdaterDialogProps)
protected readonly props: IDEUpdaterDialogProps
protected override readonly props: IDEUpdaterDialogProps
) {
super({
title: nls.localize(
@@ -138,7 +138,7 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
return this.widget.updateInfo;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -147,7 +147,7 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
this.update();
}
async open(
override async open(
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
@@ -156,17 +156,17 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
}
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
close(): void {
override close(): void {
this.widget.dispose();
super.close();
}

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import { Disable } from 'react-disable';
@@ -17,7 +17,10 @@ import {
import { nls } from '@theia/core/lib/common';
import { Settings, SettingsService } from './settings';
import { AdditionalUrlsDialog } from './settings-dialog';
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
import {
AsyncLocalizationProvider,
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
export class SettingsComponent extends React.Component<
SettingsComponent.Props,
@@ -29,7 +32,7 @@ export class SettingsComponent extends React.Component<
super(props);
}
componentDidUpdate(
override componentDidUpdate(
_: SettingsComponent.Props,
prevState: SettingsComponent.State
): void {
@@ -46,7 +49,7 @@ export class SettingsComponent extends React.Component<
}
}
componentDidMount(): void {
override componentDidMount(): void {
this.props.settingsService
.settings()
.then((settings) =>
@@ -64,11 +67,11 @@ export class SettingsComponent extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
this.toDispose.dispose();
}
render(): React.ReactNode {
override render(): React.ReactNode {
if (!this.state) {
return <div />;
}
@@ -213,11 +216,9 @@ export class SettingsComponent extends React.Component<
value={this.state.currentLanguage}
onChange={this.languageDidChange}
>
{this.state.languages.map((label) => (
<option key={label} value={label}>
{label}
</option>
))}
{this.state.languages.map((label) =>
this.toSelectOptions(label)
)}
</select>
<span style={{ marginLeft: '5px' }}>
(
@@ -275,7 +276,7 @@ export class SettingsComponent extends React.Component<
<label className="flex-line">
<input
type="checkbox"
checked={this.state.autoSave === 'on'}
checked={this.state.autoSave !== 'off'}
onChange={this.autoSaveDidChange}
/>
{nls.localize(
@@ -314,6 +315,24 @@ export class SettingsComponent extends React.Component<
);
}
private toSelectOptions(language: string | LanguageInfo): JSX.Element {
const plain = typeof language === 'string';
const key = plain ? language : language.languageId;
const value = plain ? language : language.languageId;
const label = plain
? language === 'en'
? 'English'
: language
: language.localizedLanguageName ||
language.languageName ||
language.languageId;
return (
<option key={key} value={value}>
{label}
</option>
);
}
protected renderNetwork(): React.ReactNode {
return (
<div className="content noselect">
@@ -549,7 +568,9 @@ export class SettingsComponent extends React.Component<
protected autoSaveDidChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ autoSave: event.target.checked ? 'on' : 'off' });
this.setState({
autoSave: event.target.checked ? Settings.AutoSave.DEFAULT_ON : 'off',
});
};
protected quickSuggestionsOtherDidChange = (

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import * as React from '@theia/core/shared/react';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { DialogError, ReactWidget } from '@theia/core/lib/browser';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser';
import { Settings, SettingsService } from './settings';
@@ -56,7 +56,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
constructor(
@inject(SettingsDialogProps)
protected readonly props: SettingsDialogProps
protected override readonly props: SettingsDialogProps
) {
super(props);
this.contentNode.classList.add('arduino-settings-dialog');
@@ -73,7 +73,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
);
}
protected async isValid(settings: Promise<Settings>): Promise<DialogError> {
protected override async isValid(settings: Promise<Settings>): Promise<DialogError> {
const result = await this.settingsService.validate(settings);
if (typeof result === 'string') {
return result;
@@ -85,7 +85,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
return this.settingsService.settings();
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -97,12 +97,12 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
// calling settingsService.reset() in order to reload the settings from the preferenceService
@@ -172,17 +172,17 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
return AdditionalUrls.parse(this.textArea.value, 'newline');
}
protected onAfterAttach(message: Message): void {
protected override onAfterAttach(message: Message): void {
super.onAfterAttach(message);
this.addUpdateListener(this.textArea, 'input');
}
protected onActivateRequest(message: Message): void {
protected override onActivateRequest(message: Message): void {
super.onActivateRequest(message);
this.textArea.focus();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLInputElement) {
return super.handleEnter(event);
}

View File

@@ -1,4 +1,8 @@
import { injectable, inject, postConstruct } from 'inversify';
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
@@ -16,12 +20,15 @@ import {
Network,
} from '../../../common/protocol';
import { CommandService, nls } from '@theia/core/lib/common';
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
import {
AsyncLocalizationProvider,
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
export const EDITOR_SETTING = 'editor';
export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`;
export const AUTO_SAVE_SETTING = `${EDITOR_SETTING}.autoSave`;
export const AUTO_SAVE_SETTING = `files.autoSave`;
export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`;
export const ARDUINO_SETTING = 'arduino';
export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
@@ -39,10 +46,10 @@ export const SHOW_ALL_FILES_SETTING = `${SKETCHBOOK_SETTING}.showAllFiles`;
export interface Settings {
editorFontSize: number; // `editor.fontSize`
themeId: string; // `workbench.colorTheme`
autoSave: 'on' | 'off'; // `editor.autoSave`
autoSave: Settings.AutoSave; // `files.autoSave`
quickSuggestions: Record<'other' | 'comments' | 'strings', boolean>; // `editor.quickSuggestions`
languages: string[]; // `languages from the plugins`
languages: (string | LanguageInfo)[]; // `languages from the plugins`
currentLanguage: string;
autoScaleInterface: boolean; // `arduino.window.autoScale`
@@ -61,6 +68,14 @@ export namespace Settings {
export function belongsToCli<K extends keyof Settings>(key: K): boolean {
return key === 'sketchbookPath' || key === 'additionalUrls';
}
export type AutoSave =
| 'off'
| 'afterDelay'
| 'onFocusChange'
| 'onWindowChange';
export namespace AutoSave {
export const DEFAULT_ON: AutoSave = 'afterDelay'; // https://github.com/eclipse-theia/theia/issues/10812
}
}
@injectable()
@@ -126,7 +141,10 @@ export class SettingsService {
'workbench.colorTheme',
'arduino-theme'
),
this.preferenceService.get<'on' | 'off'>(AUTO_SAVE_SETTING, 'on'),
this.preferenceService.get<Settings.AutoSave>(
AUTO_SAVE_SETTING,
Settings.AutoSave.DEFAULT_ON
),
this.preferenceService.get<
Record<'other' | 'comments' | 'strings', boolean>
>(QUICK_SUGGESTIONS_SETTING, {
@@ -262,7 +280,7 @@ export class SettingsService {
await this.savePreference('editor.fontSize', editorFontSize);
await this.savePreference('workbench.colorTheme', themeId);
await this.savePreference('editor.autoSave', autoSave);
await this.savePreference(AUTO_SAVE_SETTING, autoSave);
await this.savePreference('editor.quickSuggestions', quickSuggestions);
await this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface);
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { BoardUserField } from '../../../common/protocol';
import { nls } from '@theia/core/lib/common';

View File

@@ -1,12 +1,12 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
AbstractDialog,
DialogProps,
ReactWidget,
} from '@theia/core/lib/browser';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { UploadSketch } from '../../contributions/upload-sketch';
import { UserFieldsComponent } from './user-fields-component';
import { BoardUserField } from '../../../common/protocol';
@@ -61,7 +61,7 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
constructor(
@inject(UserFieldsDialogProps)
protected readonly props: UserFieldsDialogProps
protected override readonly props: UserFieldsDialogProps
) {
super({
title: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label || '',
@@ -83,7 +83,7 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
return this.widget.currentUserFields;
}
protected onAfterAttach(msg: Message): void {
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
@@ -92,17 +92,17 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
this.update();
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected async accept(): Promise<void> {
protected override async accept(): Promise<void> {
// If the user presses enter and at least
// a field is empty don't accept the input
for (const field of this.value) {
@@ -113,7 +113,7 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
return super.accept();
}
close(): void {
override close(): void {
this.widget.resetUserFieldsValue();
this.widget.close();
super.close();

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution,
FrontendApplication,

View File

@@ -0,0 +1,74 @@
import { DisposableCollection, Emitter, Event } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
/**
* Frontend contribution to watch VS Code extension start/stop events from Theia.
*
* In Theia, there are no events when a VS Code extension is loaded, started, unloaded, and stopped.
* Currently, it's possible to `@inject` the `HostedPluginSupport` service from Theia and `await`
* for the `didStart` promise to resolve. But if the OS goes to sleep, the VS Code extensions will
* be unloaded and loaded and started again when the OS awakes. Theia reloads the VS Code extensions
* after the OS awake event, but the `didStart` promise was already resolved, so IDE2 cannot restart the LS.
* This service is meant to work around the limitation of Theia and fire an event every time the VS Code extensions
* loaded and started.
*/
@injectable()
export class HostedPluginEvents implements FrontendApplicationContribution {
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
private firstStart = true;
private readonly onPluginsDidStartEmitter = new Emitter<void>();
private readonly onPluginsWillUnloadEmitter = new Emitter<void>();
private readonly toDispose = new DisposableCollection(
this.onPluginsDidStartEmitter,
this.onPluginsWillUnloadEmitter
);
onStart(): void {
this.hostedPluginSupport.onDidLoad(() => {
// Fire the first event, when `didStart` resolves.
if (!this.firstStart) {
console.debug('HostedPluginEvents', "Received 'onDidLoad' event.");
this.onPluginsDidStartEmitter.fire();
} else {
console.debug(
'HostedPluginEvents',
"Received 'onDidLoad' event before the first start. Skipping."
);
}
});
this.hostedPluginSupport.didStart.then(() => {
console.debug('HostedPluginEvents', "Hosted plugins 'didStart'.");
if (!this.firstStart) {
throw new Error(
'Unexpectedly received a `didStart` event after the first start.'
);
}
this.firstStart = false;
this.onPluginsDidStartEmitter.fire();
});
this.hostedPluginSupport.onDidCloseConnection(() => {
console.debug('HostedPluginEvents', "Received 'onDidCloseConnection'.");
this.onPluginsWillUnloadEmitter.fire();
});
}
onStop(): void {
this.toDispose.dispose();
}
get onPluginsDidStart(): Event<void> {
return this.onPluginsDidStartEmitter.event;
}
get onPluginsWillUnload(): Event<void> {
return this.onPluginsWillUnloadEmitter.event;
}
get didStart(): Promise<void> {
return this.hostedPluginSupport.didStart;
}
}

View File

@@ -5,7 +5,7 @@ import {
MessageService,
nls,
} from '@theia/core';
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { IDEUpdater, UpdateInfo } from '../../common/protocol/ide-updater';
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';

View File

@@ -0,0 +1,13 @@
import * as monaco from '@theia/monaco-editor-core';
/**
* Exclusive "ino" document selector for monaco.
*/
export const InoSelector = selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
function selectorOf(
...languageId: string[]
): monaco.languages.LanguageSelector {
return languageId.map((language) => ({
language,
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
}));
}

View File

@@ -1,5 +1,5 @@
import { injectable, postConstruct, inject } from 'inversify';
import { Message } from '@phosphor/messaging';
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';
@@ -38,7 +38,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
}
@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
@@ -48,7 +48,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
]);
}
protected async install({
protected override async install({
item,
progressId,
version,
@@ -158,7 +158,7 @@ export class LibraryListWidget extends ListWidget<LibraryPackage> {
}
}
protected async uninstall({
protected override async uninstall({
item,
progressId,
}: {
@@ -199,7 +199,7 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
});
}
protected onCloseRequest(message: Message): void {
protected override onCloseRequest(message: Message): void {
super.onCloseRequest(message);
this.accept();
}
@@ -217,7 +217,7 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
return message;
}
protected handleEnter(event: KeyboardEvent): boolean | void {
protected override handleEnter(event: KeyboardEvent): boolean | void {
this.response = 0;
super.handleEnter(event);
}

View File

@@ -1,4 +1,4 @@
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry } from '@theia/core';
@@ -28,7 +28,7 @@ export class LibraryListWidgetFrontendContribution
this.openView();
}
registerMenus(menus: MenuModelRegistry): void {
override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,

View File

@@ -1,4 +1,4 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';

View File

@@ -0,0 +1,199 @@
import {
CommandRegistry,
Disposable,
Emitter,
MessageService,
nls,
} from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Board, Port } from '../common/protocol';
import {
Monitor,
MonitorManagerProxyClient,
MonitorManagerProxyFactory,
} from '../common/protocol/monitor-service';
import {
PluggableMonitorSettings,
MonitorSettings,
} from '../node/monitor-settings/monitor-settings-provider';
import { BoardsConfig } from './boards/boards-config';
import { BoardsServiceProvider } from './boards/boards-service-provider';
@injectable()
export class MonitorManagerProxyClientImpl
implements MonitorManagerProxyClient
{
// When pluggable monitor messages are received from the backend
// this event is triggered.
// Ideally a frontend component is connected to this event
// to update the UI.
protected readonly onMessagesReceivedEmitter = new Emitter<{
messages: string[];
}>();
readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;
protected readonly onMonitorSettingsDidChangeEmitter =
new Emitter<MonitorSettings>();
readonly onMonitorSettingsDidChange =
this.onMonitorSettingsDidChangeEmitter.event;
protected readonly onMonitorShouldResetEmitter = new Emitter();
readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event;
// WebSocket used to handle pluggable monitor communication between
// frontend and backend.
private webSocket?: WebSocket;
private wsPort?: number;
private lastConnectedBoard: BoardsConfig.Config;
private onBoardsConfigChanged: Disposable | undefined;
getWebSocketPort(): number | undefined {
return this.wsPort;
}
constructor(
@inject(MessageService)
protected messageService: MessageService,
// This is necessary to call the backend methods from the frontend
@inject(MonitorManagerProxyFactory)
protected server: MonitorManagerProxyFactory,
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {}
/**
* Connects a localhost WebSocket using the specified port.
* @param addressPort port of the WebSocket
*/
async connect(addressPort: number): Promise<void> {
if (!!this.webSocket) {
if (this.wsPort === addressPort) return;
else this.disconnect();
}
try {
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
} catch {
this.messageService.error(
nls.localize(
'arduino/monitor/unableToConnectToWebSocket',
'Unable to connect to websocket'
)
);
return;
}
this.webSocket.onmessage = (message) => {
const parsedMessage = JSON.parse(message.data);
if (Array.isArray(parsedMessage))
this.onMessagesReceivedEmitter.fire({ messages: parsedMessage });
else if (
parsedMessage.command ===
Monitor.MiddlewareCommand.ON_SETTINGS_DID_CHANGE
) {
this.onMonitorSettingsDidChangeEmitter.fire(parsedMessage.data);
}
};
this.wsPort = addressPort;
}
/**
* Disconnects the WebSocket if connected.
*/
disconnect(): void {
if (!this.webSocket) return;
this.onBoardsConfigChanged?.dispose();
this.onBoardsConfigChanged = undefined;
try {
this.webSocket?.close();
this.webSocket = undefined;
} catch {
this.messageService.error(
nls.localize(
'arduino/monitor/unableToCloseWebSocket',
'Unable to close websocket'
)
);
}
}
async isWSConnected(): Promise<boolean> {
return !!this.webSocket;
}
async startMonitor(settings?: PluggableMonitorSettings): Promise<void> {
this.lastConnectedBoard = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort,
};
if (!this.onBoardsConfigChanged) {
this.onBoardsConfigChanged =
this.boardsServiceProvider.onBoardsConfigChanged(
async ({ selectedBoard, selectedPort }) => {
if (
typeof selectedBoard === 'undefined' ||
typeof selectedPort === 'undefined'
)
return;
// a board is plugged and it's different from the old connected board
if (
selectedBoard?.fqbn !==
this.lastConnectedBoard?.selectedBoard?.fqbn ||
selectedPort?.id !== this.lastConnectedBoard?.selectedPort?.id
) {
this.onMonitorShouldResetEmitter.fire(null);
this.lastConnectedBoard = {
selectedBoard: selectedBoard,
selectedPort: selectedPort,
};
} else {
// a board is plugged and it's the same as prev, rerun "this.startMonitor" to
// recreate the listener callback
this.startMonitor();
}
}
);
}
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return;
await this.server().startMonitor(selectedBoard, selectedPort, settings);
}
getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings> {
return this.server().getCurrentSettings(board, port);
}
send(message: string): void {
if (!this.webSocket) {
return;
}
this.webSocket.send(
JSON.stringify({
command: Monitor.ClientCommand.SEND_MESSAGE,
data: message,
})
);
}
changeSettings(settings: MonitorSettings): void {
if (!this.webSocket) {
return;
}
this.webSocket.send(
JSON.stringify({
command: Monitor.ClientCommand.CHANGE_SETTINGS,
data: settings,
})
);
}
}

View File

@@ -0,0 +1,278 @@
import { Emitter, Event } from '@theia/core';
import {
FrontendApplicationContribution,
LocalStorageService,
} from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonitorManagerProxyClient } from '../common/protocol';
import { isNullOrUndefined } from '../common/utils';
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
protected readonly onChangeEmitter: Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
>;
protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _lineEnding: MonitorModel.EOL;
protected _interpolate: boolean;
protected _darkTheme: boolean;
protected _wsPort: number;
protected _serialPort: string;
protected _connected: boolean;
constructor() {
this._autoscroll = true;
this._timestamp = false;
this._interpolate = false;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this._darkTheme = false;
this._wsPort = 0;
this._serialPort = '';
this._connected = true;
this.onChangeEmitter = new Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
>();
}
onStart(): void {
this.localStorageService
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.then(this.restoreState.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
}
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event;
}
protected restoreState(state: MonitorModel.State): void {
if (!state) {
return;
}
this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp;
this._lineEnding = state.lineEnding;
this._interpolate = state.interpolate;
this._serialPort = state.serialPort;
}
protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
lineEnding: this._lineEnding,
interpolate: this._interpolate,
serialPort: this._serialPort,
});
}
get autoscroll(): boolean {
return this._autoscroll;
}
set autoscroll(autoscroll: boolean) {
if (autoscroll === this._autoscroll) return;
this._autoscroll = autoscroll;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { autoscroll },
});
this.storeState().then(() => {
this.onChangeEmitter.fire({
property: 'autoscroll',
value: this._autoscroll,
});
});
}
toggleAutoscroll(): void {
this.autoscroll = !this._autoscroll;
}
get timestamp(): boolean {
return this._timestamp;
}
set timestamp(timestamp: boolean) {
if (timestamp === this._timestamp) return;
this._timestamp = timestamp;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { timestamp },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'timestamp',
value: this._timestamp,
})
);
}
toggleTimestamp(): void {
this.timestamp = !this._timestamp;
}
get lineEnding(): MonitorModel.EOL {
return this._lineEnding;
}
set lineEnding(lineEnding: MonitorModel.EOL) {
if (lineEnding === this._lineEnding) return;
this._lineEnding = lineEnding;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { lineEnding },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'lineEnding',
value: this._lineEnding,
})
);
}
get interpolate(): boolean {
return this._interpolate;
}
set interpolate(interpolate: boolean) {
if (interpolate === this._interpolate) return;
this._interpolate = interpolate;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { interpolate },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'interpolate',
value: this._interpolate,
})
);
}
get darkTheme(): boolean {
return this._darkTheme;
}
set darkTheme(darkTheme: boolean) {
if (darkTheme === this._darkTheme) return;
this._darkTheme = darkTheme;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { darkTheme },
});
this.onChangeEmitter.fire({
property: 'darkTheme',
value: this._darkTheme,
});
}
get wsPort(): number {
return this._wsPort;
}
set wsPort(wsPort: number) {
if (wsPort === this._wsPort) return;
this._wsPort = wsPort;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { wsPort },
});
this.onChangeEmitter.fire({
property: 'wsPort',
value: this._wsPort,
});
}
get serialPort(): string {
return this._serialPort;
}
set serialPort(serialPort: string) {
if (serialPort === this._serialPort) return;
this._serialPort = serialPort;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { serialPort },
});
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'serialPort',
value: this._serialPort,
})
);
}
get connected(): boolean {
return this._connected;
}
set connected(connected: boolean) {
if (connected === this._connected) return;
this._connected = connected;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { connected },
});
this.onChangeEmitter.fire({
property: 'connected',
value: this._connected,
});
}
protected onMonitorSettingsDidChange = (settings: MonitorSettings): void => {
const { monitorUISettings } = settings;
if (!monitorUISettings) return;
const {
autoscroll,
interpolate,
lineEnding,
timestamp,
darkTheme,
wsPort,
serialPort,
connected,
} = monitorUISettings;
if (!isNullOrUndefined(autoscroll)) this.autoscroll = autoscroll;
if (!isNullOrUndefined(interpolate)) this.interpolate = interpolate;
if (!isNullOrUndefined(lineEnding)) this.lineEnding = lineEnding;
if (!isNullOrUndefined(timestamp)) this.timestamp = timestamp;
if (!isNullOrUndefined(darkTheme)) this.darkTheme = darkTheme;
if (!isNullOrUndefined(wsPort)) this.wsPort = wsPort;
if (!isNullOrUndefined(serialPort)) this.serialPort = serialPort;
if (!isNullOrUndefined(connected)) this.connected = connected;
};
}
// TODO: Move this to /common
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
lineEnding: EOL;
interpolate: boolean;
darkTheme: boolean;
wsPort: number;
serialPort: string;
connected: boolean;
}
export namespace State {
export interface Change<K extends keyof State> {
readonly property: K;
readonly value: State[K];
}
}
export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL {
export const DEFAULT: EOL = '\n';
}
}

View File

@@ -1,4 +1,8 @@
import { inject, injectable, postConstruct } from 'inversify';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
@@ -14,6 +18,10 @@ import {
Config,
Sketch,
} from '../common/protocol';
import {
FrontendApplicationStateService,
FrontendApplicationState,
} from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class NotificationCenter
@@ -22,8 +30,11 @@ export class NotificationCenter
@inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly indexUpdatedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<string>();
protected readonly daemonStoppedEmitter = new Emitter<void>();
protected readonly configChangedEmitter = new Emitter<{
config: Config | undefined;
@@ -45,6 +56,8 @@ export class NotificationCenter
protected readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[];
}>();
private readonly onAppStateDidChangeEmitter =
new Emitter<FrontendApplicationState>();
protected readonly toDispose = new DisposableCollection(
this.indexUpdatedEmitter,
@@ -68,10 +81,16 @@ export class NotificationCenter
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
readonly onAppStateDidChange = this.onAppStateDidChangeEmitter.event;
@postConstruct()
protected init(): void {
this.server.setClient(this);
this.toDispose.push(
this.appStateService.onStateChanged((state) =>
this.onAppStateDidChangeEmitter.fire(state)
)
);
}
onStop(): void {
@@ -82,8 +101,8 @@ export class NotificationCenter
this.indexUpdatedEmitter.fire();
}
notifyDaemonStarted(): void {
this.daemonStartedEmitter.fire();
notifyDaemonStarted(port: string): void {
this.daemonStartedEmitter.fire(port);
}
notifyDaemonStopped(): void {

View File

@@ -1,7 +1,9 @@
import { inject, injectable } from 'inversify';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import {
OutputChannelManager,
OutputChannelSeverity,
} from '@theia/output/lib/browser/output-channel';
import {
OutputMessage,
ProgressMessage,
@@ -10,13 +12,10 @@ import {
@injectable()
export class ResponseServiceImpl implements ResponseServiceArduino {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
private readonly outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
private readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
@@ -25,13 +24,22 @@ export class ResponseServiceImpl implements ResponseServiceArduino {
}
appendToOutput(message: OutputMessage): void {
const { chunk } = message;
const { chunk, severity } = message;
const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true });
channel.append(chunk);
channel.append(chunk, mapSeverity(severity));
}
reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress);
}
}
function mapSeverity(severity?: OutputMessage.Severity): OutputChannelSeverity {
if (severity === OutputMessage.Severity.Error) {
return OutputChannelSeverity.Error;
} else if (severity === OutputMessage.Severity.Warning) {
return OutputChannelSeverity.Warning;
}
return OutputChannelSeverity.Info;
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { injectable, inject } from 'inversify';
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { AbstractViewContribution, codicon } from '@theia/core/lib/browser';
import { MonitorWidget } from './monitor-widget';
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
@@ -8,9 +8,10 @@ import {
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import { SerialModel } from '../serial-model';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { MonitorModel } from '../../monitor-model';
import { MonitorManagerProxyClient } from '../../../common/protocol';
export namespace SerialMonitor {
export namespace Commands {
@@ -47,10 +48,15 @@ export class MonitorViewContribution
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
MonitorWidget.ID + ':toggle-toolbar';
static readonly RESET_SERIAL_MONITOR = MonitorWidget.ID + ':reset';
@inject(SerialModel) protected readonly model: SerialModel;
constructor(
@inject(MonitorModel)
protected readonly model: MonitorModel,
constructor() {
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient
) {
super({
widgetId: MonitorWidget.ID,
widgetName: MonitorWidget.LABEL,
@@ -60,9 +66,10 @@ export class MonitorViewContribution
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
toggleKeybinding: 'CtrlCmd+Shift+M',
});
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
}
registerMenus(menus: MenuModelRegistry): void {
override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
@@ -95,7 +102,7 @@ export class MonitorViewContribution
});
}
registerCommands(commands: CommandRegistry): void {
override registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
@@ -118,6 +125,10 @@ export class MonitorViewContribution
}
);
}
commands.registerCommand(
{ id: MonitorViewContribution.RESET_SERIAL_MONITOR },
{ execute: () => this.reset() }
);
}
protected async toggle(): Promise<void> {
@@ -129,6 +140,14 @@ export class MonitorViewContribution
}
}
protected async reset(): Promise<void> {
const widget = this.tryGetWidget();
if (widget) {
widget.dispose();
await this.openView({ activate: true, reveal: true });
}
}
protected renderAutoScrollButton(): React.ReactNode {
return (
<React.Fragment key="autoscroll-toolbar-item">

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { postConstruct, injectable, inject } from 'inversify';
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { OptionsType } from 'react-select/src/types';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
@@ -9,14 +9,14 @@ import {
Widget,
MessageLoop,
} from '@theia/core/lib/browser/widgets';
import { SerialConfig } from '../../../common/protocol/serial-service';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SerialModel } from '../serial-model';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialMonitorSendInput } from './serial-monitor-send-input';
import { SerialMonitorOutput } from './serial-monitor-send-output';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { nls } from '@theia/core/lib/common';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorWidget extends ReactWidget {
@@ -26,14 +26,7 @@ export class MonitorWidget extends ReactWidget {
);
static readonly ID = 'serial-monitor';
@inject(SerialModel)
protected readonly serialModel: SerialModel;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected settings: MonitorSettings = {};
protected widgetHeight: number;
@@ -48,7 +41,16 @@ export class MonitorWidget extends ReactWidget {
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor() {
constructor(
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel,
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {
super();
this.id = MonitorWidget.ID;
this.title.label = MonitorWidget.LABEL;
@@ -57,17 +59,30 @@ export class MonitorWidget extends ReactWidget {
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() => this.serialConnection.closeWStoBE())
Disposable.create(() => this.monitorManagerProxy.disconnect())
);
}
@postConstruct()
protected init(): void {
protected override onBeforeAttach(msg: Message): void {
this.update();
this.toDispose.push(
this.serialConnection.onConnectionChanged(() => this.clearConsole())
this.toDispose.push(this.monitorModel.onChange(() => this.update()));
this.getCurrentSettings().then(this.onMonitorSettingsDidChange.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
this.toDispose.push(this.serialModel.onChange(() => this.update()));
this.monitorManagerProxy.startMonitor();
}
onMonitorSettingsDidChange(settings: MonitorSettings): void {
this.settings = {
...this.settings,
pluggableMonitorSettings: {
...this.settings.pluggableMonitorSettings,
...settings.pluggableMonitorSettings,
},
};
this.update();
}
clearConsole(): void {
@@ -75,21 +90,16 @@ export class MonitorWidget extends ReactWidget {
this.update();
}
dispose(): void {
override dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.serialConnection.openWSToBE();
}
onCloseRequest(msg: Message): void {
protected override onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
protected override onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
@@ -97,13 +107,13 @@ export class MonitorWidget extends ReactWidget {
}
}
protected onResize(msg: Widget.ResizeMessage): void {
protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected onActivateRequest(msg: Message): void {
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
@@ -119,7 +129,7 @@ export class MonitorWidget extends ReactWidget {
};
protected get lineEndings(): OptionsType<
SerialMonitorOutput.SelectOption<SerialModel.EOL>
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
> {
return [
{
@@ -144,32 +154,40 @@ export class MonitorWidget extends ReactWidget {
];
}
protected get baudRates(): OptionsType<
SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
> {
const baudRates: Array<SerialConfig.BaudRate> = [
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
];
return baudRates.map((baudRate) => ({
label: baudRate + ' baud',
value: baudRate,
}));
private getCurrentSettings(): Promise<MonitorSettings> {
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
if (!board || !port) {
return Promise.resolve(this.settings || {});
}
return this.monitorManagerProxy.getCurrentSettings(board, port);
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const baudrate = this.settings?.pluggableMonitorSettings
? this.settings.pluggableMonitorSettings.baudrate
: undefined;
const baudrateOptions = baudrate?.values.map((b) => ({
label: b + ' baud',
value: b,
}));
const baudrateSelectedOption = baudrateOptions?.find(
(b) => b.value === baudrate?.selectedValue
);
const lineEnding =
lineEndings.find((item) => item.value === this.serialModel.lineEnding) ||
lineEndings[1]; // Defaults to `\n`.
const baudRate =
baudRates.find((item) => item.value === this.serialModel.baudRate) ||
baudRates[4]; // Defaults to `9600`.
this.lineEndings.find(
(item) => item.value === this.monitorModel.lineEnding
) || this.lineEndings[1]; // Defaults to `\n`.
return (
<div className="serial-monitor">
<div className="head">
<div className="send">
<SerialMonitorSendInput
serialConnection={this.serialConnection}
boardsServiceProvider={this.boardsServiceProvider}
monitorModel={this.monitorModel}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>
@@ -178,26 +196,28 @@ export class MonitorWidget extends ReactWidget {
<div className="select">
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
options={this.lineEndings}
value={lineEnding}
onChange={this.onChangeLineEnding}
/>
</div>
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
value={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
{baudrateOptions && baudrateSelectedOption && (
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudrateOptions}
value={baudrateSelectedOption}
onChange={this.onChangeBaudRate}
/>
</div>
)}
</div>
</div>
<div className="body">
<SerialMonitorOutput
serialModel={this.serialModel}
serialConnection={this.serialConnection}
monitorModel={this.monitorModel}
monitorManagerProxy={this.monitorManagerProxy}
clearConsoleEvent={this.clearOutputEmitter.event}
height={Math.floor(this.widgetHeight - 50)}
/>
@@ -208,18 +228,26 @@ export class MonitorWidget extends ReactWidget {
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
this.serialConnection.send(value);
this.monitorManagerProxy.send(value);
}
protected readonly onChangeLineEnding = (
option: SerialMonitorOutput.SelectOption<SerialModel.EOL>
) => {
this.serialModel.lineEnding = option.value;
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
): void => {
this.monitorModel.lineEnding = option.value;
};
protected readonly onChangeBaudRate = (
option: SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
) => {
this.serialModel.baudRate = option.value;
protected readonly onChangeBaudRate = ({
value,
}: {
value: string;
}): void => {
this.getCurrentSettings().then(({ pluggableMonitorSettings }) => {
if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate'])
return;
const baudRateSettings = pluggableMonitorSettings['baudrate'];
baudRateSettings.selectedValue = value;
this.monitorManagerProxy.changeSettings({ pluggableMonitorSettings });
});
};
}

View File

@@ -1,14 +1,15 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { Board } from '../../../common/protocol/boards-service';
import { isOSX } from '@theia/core/lib/common/os';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from '../plotter/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
export namespace SerialMonitorSendInput {
export interface Props {
readonly serialConnection: SerialConnectionManager;
readonly boardsServiceProvider: BoardsServiceProvider;
readonly monitorModel: MonitorModel;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
@@ -26,41 +27,33 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: false };
this.state = { text: '', connected: true };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
componentDidMount(): void {
this.props.serialConnection.isBESerialConnected().then((connected) => {
this.setState({ connected });
});
this.toDisposeBeforeUnmount.pushAll([
this.props.serialConnection.onRead(({ messages }) => {
if (
messages.command ===
SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED &&
'connected' in messages.data
) {
this.setState({ connected: messages.data.connected });
}
}),
]);
override componentDidMount(): void {
this.setState({ connected: this.props.monitorModel.connected });
this.toDisposeBeforeUnmount.push(
this.props.monitorModel.onChange(({ property }) => {
if (property === 'connected')
this.setState({ connected: this.props.monitorModel.connected });
})
);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.state.connected ? '' : 'warning'}`}
className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
@@ -69,15 +62,22 @@ export class SerialMonitorSendInput extends React.Component<
);
}
protected shouldShowWarning(): boolean {
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return !this.state.connected || !board || !port;
}
protected get placeholder(): string {
const serialConfig = this.props.serialConnection.getConfig();
if (!this.state.connected || !serialConfig) {
if (this.shouldShowWarning()) {
return nls.localize(
'arduino/serial/notConnected',
'Not connected. Select a board and a port to connect automatically.'
);
}
const { board, port } = serialConfig;
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return nls.localize(
'arduino/serial/message',
"Message ({0} + Enter to send message to '{1}' on '{2}')",
@@ -102,7 +102,7 @@ export class SerialMonitorSendInput extends React.Component<
}
protected onSend(): void {
this.props.onSend(this.state.text);
this.props.onSend(this.state.text + this.props.monitorModel.lineEnding);
this.setState({ text: '' });
}

View File

@@ -1,11 +1,11 @@
import * as React from 'react';
import * as React from '@theia/core/shared/react';
import { Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { areEqual, FixedSizeList as List } from 'react-window';
import { SerialModel } from '../serial-model';
import { SerialConnectionManager } from '../serial-connection-manager';
import dateFormat = require('dateformat');
import { messagesToLines, truncateLines } from './monitor-utils';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
export type Line = { message: string; timestamp?: Date; lineLen: number };
@@ -24,12 +24,12 @@ export class SerialMonitorOutput extends React.Component<
this.listRef = React.createRef();
this.state = {
lines: [],
timestamp: this.props.serialModel.timestamp,
timestamp: this.props.monitorModel.timestamp,
charCount: 0,
};
}
render(): React.ReactNode {
override render(): React.ReactNode {
return (
<List
className="serial-monitor-messages"
@@ -51,21 +51,20 @@ export class SerialMonitorOutput extends React.Component<
);
}
shouldComponentUpdate(): boolean {
override shouldComponentUpdate(): boolean {
return true;
}
componentDidMount(): void {
override componentDidMount(): void {
this.scrollToBottom();
this.toDisposeBeforeUnmount.pushAll([
this.props.serialConnection.onRead(({ messages }) => {
this.props.monitorManagerProxy.onMessagesReceived(({ messages }) => {
const [newLines, totalCharCount] = messagesToLines(
messages,
this.state.lines,
this.state.charCount
);
const [lines, charCount] = truncateLines(newLines, totalCharCount);
this.setState({
lines,
charCount,
@@ -75,9 +74,9 @@ export class SerialMonitorOutput extends React.Component<
this.props.clearConsoleEvent(() =>
this.setState({ lines: [], charCount: 0 })
),
this.props.serialModel.onChange(({ property }) => {
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.serialModel;
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
if (property === 'autoscroll') {
@@ -87,13 +86,13 @@ export class SerialMonitorOutput extends React.Component<
]);
}
componentWillUnmount(): void {
override componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
scrollToBottom = ((): void => {
if (this.listRef.current && this.props.serialModel.autoscroll) {
if (this.listRef.current && this.props.monitorModel.autoscroll) {
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
}
}).bind(this);
@@ -128,8 +127,8 @@ const Row = React.memo(_Row, areEqual);
export namespace SerialMonitorOutput {
export interface Props {
readonly serialModel: SerialModel;
readonly serialConnection: SerialConnectionManager;
readonly monitorModel: MonitorModel;
readonly monitorManagerProxy: MonitorManagerProxyClient;
readonly clearConsoleEvent: Event<void>;
readonly height: number;
}

View File

@@ -1,20 +1,19 @@
import { ThemeService } from '@theia/core/lib/browser/theming';
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
Command,
CommandRegistry,
MaybePromise,
MenuModelRegistry,
} from '@theia/core';
import { SerialModel } from '../serial-model';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { Contribution } from '../../contributions/contribution';
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
import { ipcRenderer } from '@theia/electron/shared/electron';
import { SerialConfig } from '../../../common/protocol';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from './protocol';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
const queryString = require('query-string');
export namespace SerialPlotterContribution {
@@ -24,6 +23,11 @@ export namespace SerialPlotterContribution {
label: 'Serial Plotter',
category: 'Arduino',
};
export const RESET: Command = {
id: 'serial-plotter-reset',
label: 'Reset Serial Plotter',
category: 'Arduino',
};
}
}
@@ -33,19 +37,19 @@ export class PlotterFrontendContribution extends Contribution {
protected url: string;
protected wsPort: number;
@inject(SerialModel)
protected readonly model: SerialModel;
@inject(MonitorModel)
protected readonly model: MonitorModel;
@inject(ThemeService)
protected readonly themeService: ThemeService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
onStart(app: FrontendApplication): MaybePromise<void> {
override onStart(app: FrontendApplication): MaybePromise<void> {
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString();
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
@@ -53,16 +57,21 @@ export class PlotterFrontendContribution extends Contribution {
this.window = null;
}
});
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
return super.onStart(app);
}
registerCommands(registry: CommandRegistry): void {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SerialPlotterContribution.Commands.OPEN, {
execute: this.connect.bind(this),
execute: this.startPlotter.bind(this),
});
registry.registerCommand(SerialPlotterContribution.Commands.RESET, {
execute: () => this.reset(),
});
}
registerMenus(menus: MenuModelRegistry): void {
override registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: SerialPlotterContribution.Commands.OPEN.id,
label: SerialPlotterContribution.Commands.OPEN.label,
@@ -70,12 +79,13 @@ export class PlotterFrontendContribution extends Contribution {
});
}
async connect(): Promise<void> {
async startPlotter(): Promise<void> {
await this.monitorManagerProxy.startMonitor();
if (!!this.window) {
this.window.focus();
return;
}
const wsPort = this.serialConnection.getWsPort();
const wsPort = this.monitorManagerProxy.getWebSocketPort();
if (wsPort) {
this.open(wsPort);
} else {
@@ -84,15 +94,10 @@ export class PlotterFrontendContribution extends Contribution {
}
protected async open(wsPort: number): Promise<void> {
const initConfig: Partial<SerialPlotter.Config> = {
baudrates: SerialConfig.BaudRates.map((b) => b),
currentBaudrate: this.model.baudRate,
currentLineEnding: this.model.lineEnding,
const initConfig = {
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
wsPort,
interpolate: this.model.interpolate,
connected: await this.serialConnection.isBESerialConnected(),
serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
serialPort: this.model.serialPort,
};
const urlWithParams = queryString.stringifyUrl(
{
@@ -103,4 +108,11 @@ export class PlotterFrontendContribution extends Contribution {
);
this.window = window.open(urlWithParams, 'serialPlotter');
}
protected async reset(): Promise<void> {
if (!!this.window) {
this.window.close();
await this.startPlotter();
}
}
}

View File

@@ -1,26 +0,0 @@
export namespace SerialPlotter {
export type Config = {
currentBaudrate: number;
baudrates: number[];
currentLineEnding: string;
darkTheme: boolean;
wsPort: number;
interpolate: boolean;
serialPort: string;
connected: boolean;
generate?: boolean;
};
export namespace Protocol {
export enum Command {
PLOTTER_SET_BAUDRATE = 'PLOTTER_SET_BAUDRATE',
PLOTTER_SET_LINE_ENDING = 'PLOTTER_SET_LINE_ENDING',
PLOTTER_SET_INTERPOLATE = 'PLOTTER_SET_INTERPOLATE',
PLOTTER_SEND_MESSAGE = 'PLOTTER_SEND_MESSAGE',
MIDDLEWARE_CONFIG_CHANGED = 'MIDDLEWARE_CONFIG_CHANGED',
}
export type Message = {
command: SerialPlotter.Protocol.Command;
data?: any;
};
}
}

View File

@@ -1,360 +0,0 @@
import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import {
SerialService,
SerialConfig,
SerialError,
Status,
SerialServiceClient,
} from '../../common/protocol/serial-service';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
Board,
BoardsService,
} from '../../common/protocol/boards-service';
import { BoardsConfig } from '../boards/boards-config';
import { SerialModel } from './serial-model';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { CoreService } from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class SerialConnectionManager {
protected config: Partial<SerialConfig> = {
board: undefined,
port: undefined,
baudRate: undefined,
};
protected readonly onConnectionChangedEmitter = new Emitter<boolean>();
/**
* This emitter forwards all read events **if** the connection is established.
*/
protected readonly onReadEmitter = new Emitter<{ messages: string[] }>();
/**
* Array for storing previous serial errors received from the server, and based on the number of elements in this array,
* we adjust the reconnection delay.
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
*/
protected serialErrors: SerialError[] = [];
protected reconnectTimeout?: number;
/**
* When the websocket server is up on the backend, we save the port here, so that the client knows how to connect to it
* */
protected wsPort?: number;
protected webSocket?: WebSocket;
constructor(
@inject(SerialModel) protected readonly serialModel: SerialModel,
@inject(SerialService) protected readonly serialService: SerialService,
@inject(SerialServiceClient)
protected readonly serialServiceClient: SerialServiceClient,
@inject(BoardsService) protected readonly boardsService: BoardsService,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider,
@inject(MessageService) protected messageService: MessageService,
@inject(ThemeService) protected readonly themeService: ThemeService,
@inject(CoreService) protected readonly core: CoreService,
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider
) {
this.serialServiceClient.onWebSocketChanged(
this.handleWebSocketChanged.bind(this)
);
this.serialServiceClient.onBaudRateChanged((baudRate) => {
if (this.serialModel.baudRate !== baudRate) {
this.serialModel.baudRate = baudRate;
}
});
this.serialServiceClient.onLineEndingChanged((lineending) => {
if (this.serialModel.lineEnding !== lineending) {
this.serialModel.lineEnding = lineending;
}
});
this.serialServiceClient.onInterpolateChanged((interpolate) => {
if (this.serialModel.interpolate !== interpolate) {
this.serialModel.interpolate = interpolate;
}
});
this.serialServiceClient.onError(this.handleError.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
// Handles the `baudRate` changes by reconnecting if required.
this.serialModel.onChange(async ({ property }) => {
if (
property === 'baudRate' &&
(await this.serialService.isSerialPortOpen())
) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
// update the current values in the backend and propagate to websocket clients
this.serialService.updateWsConfigParam({
...(property === 'lineEnding' && {
currentLineEnding: this.serialModel.lineEnding,
}),
...(property === 'interpolate' && {
interpolate: this.serialModel.interpolate,
}),
});
});
this.themeService.onDidColorThemeChange((theme) => {
this.serialService.updateWsConfigParam({
darkTheme: theme.newTheme.type === 'dark',
});
});
}
/**
* Updated the config in the BE passing only the properties that has changed.
* BE will create a new connection if needed.
*
* @param newConfig the porperties of the config that has changed
*/
async setConfig(newConfig: Partial<SerialConfig>): Promise<void> {
let configHasChanged = false;
Object.keys(this.config).forEach((key: keyof SerialConfig) => {
if (newConfig[key] !== this.config[key]) {
configHasChanged = true;
this.config = { ...this.config, [key]: newConfig[key] };
}
});
if (configHasChanged) {
this.serialService.updateWsConfigParam({
currentBaudrate: this.config.baudRate,
serialPort: this.config.port?.address,
});
if (isSerialConfig(this.config)) {
this.serialService.setSerialConfig(this.config);
}
}
}
getConfig(): Partial<SerialConfig> {
return this.config;
}
getWsPort(): number | undefined {
return this.wsPort;
}
protected handleWebSocketChanged(wsPort: number): void {
this.wsPort = wsPort;
}
get serialConfig(): SerialConfig | undefined {
return isSerialConfig(this.config)
? (this.config as SerialConfig)
: undefined;
}
async isBESerialConnected(): Promise<boolean> {
return await this.serialService.isSerialPortOpen();
}
openWSToBE(): void {
if (!isSerialConfig(this.config)) {
this.messageService.error(
`Please select a board and a port to open the serial connection.`
);
}
if (!this.webSocket && this.wsPort) {
try {
this.webSocket = new WebSocket(`ws://localhost:${this.wsPort}`);
this.webSocket.onmessage = (res) => {
const messages = JSON.parse(res.data);
this.onReadEmitter.fire({ messages });
};
} catch {
this.messageService.error(`Unable to connect to websocket`);
}
}
}
closeWStoBE(): void {
if (this.webSocket) {
try {
this.webSocket.close();
this.webSocket = undefined;
} catch {
this.messageService.error(`Unable to close websocket`);
}
}
}
/**
* Handles error on the SerialServiceClient and try to reconnect, eventually
*/
async handleError(error: SerialError): Promise<void> {
if (!(await this.serialService.isSerialPortOpen())) return;
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
switch (code) {
case SerialError.ErrorCodes.CLIENT_CANCEL: {
console.debug(
`Serial connection was canceled by client: ${Serial.Config.toString(
this.config
)}.`
);
break;
}
case SerialError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(
nls.localize(
'arduino/serial/connectionBusy',
'Connection failed. Serial port is busy: {0}',
port.address
),
options
);
this.serialErrors.push(error);
break;
}
case SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
nls.localize(
'arduino/serial/disconnected',
'Disconnected {0} from {1}.',
Board.toString(board, {
useFqbn: false,
}),
port.address
),
options
);
break;
}
case undefined: {
this.messageService.error(
nls.localize(
'arduino/serial/unexpectedError',
'Unexpected error. Reconnecting {0} on port {1}.',
Board.toString(board),
port.address
),
options
);
console.error(JSON.stringify(error));
break;
}
}
if ((await this.serialService.clientsAttached()) > 0) {
if (this.serialErrors.length >= 10) {
this.messageService.warn(
nls.localize(
'arduino/serial/failedReconnect',
'Failed to reconnect {0} to serial port after 10 consecutive attempts. The {1} serial port is busy.',
Board.toString(board, {
useFqbn: false,
}),
port.address
)
);
this.serialErrors.length = 0;
} else {
const attempts = this.serialErrors.length || 1;
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(
nls.localize(
'arduino/serial/reconnect',
'Reconnecting {0} to {1} in {2} seconds...',
Board.toString(board, {
useFqbn: false,
}),
port.address,
attempts.toString()
)
);
this.reconnectTimeout = window.setTimeout(
() => this.reconnectAfterUpload(),
timeout
);
}
}
}
async reconnectAfterUpload(): Promise<void> {
try {
if (isSerialConfig(this.config)) {
await this.boardsServiceClientImpl.waitUntilAvailable(
Object.assign(this.config.board, { port: this.config.port }),
10_000
);
this.serialService.connectSerialIfRequired();
}
} catch (waitError) {
this.messageService.error(
nls.localize(
'arduino/sketch/couldNotConnectToSerial',
'Could not reconnect to serial port. {0}',
waitError.toString()
)
);
}
}
/**
* Sends the data to the connected serial port.
* The desired EOL is appended to `data`, you do not have to add it.
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!(await this.serialService.isSerialPortOpen())) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>((resolve) => {
this.serialService
.sendMessageToSerial(data + this.serialModel.lineEnding)
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<boolean> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ messages: any }> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(
boardsConfig: BoardsConfig.Config
): Promise<void> {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.serialModel;
const newConfig: Partial<SerialConfig> = { board, port, baudRate };
this.setConfig(newConfig);
}
}
export namespace Serial {
export namespace Config {
export function toString(config: Partial<SerialConfig>): string {
if (!isSerialConfig(config)) return '';
const { board, port } = config;
return `${Board.toString(board)} ${port.address}`;
}
}
}
function isSerialConfig(config: Partial<SerialConfig>): config is SerialConfig {
return !!config.board && !!config.baudRate && !!config.port;
}

View File

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

View File

@@ -1,48 +0,0 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import {
SerialServiceClient,
SerialError,
SerialConfig,
} from '../../common/protocol/serial-service';
import { SerialModel } from './serial-model';
@injectable()
export class SerialServiceClientImpl implements SerialServiceClient {
protected readonly onErrorEmitter = new Emitter<SerialError>();
readonly onError = this.onErrorEmitter.event;
protected readonly onWebSocketChangedEmitter = new Emitter<number>();
readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event;
protected readonly onBaudRateChangedEmitter =
new Emitter<SerialConfig.BaudRate>();
readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event;
protected readonly onLineEndingChangedEmitter =
new Emitter<SerialModel.EOL>();
readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event;
protected readonly onInterpolateChangedEmitter = new Emitter<boolean>();
readonly onInterpolateChanged = this.onInterpolateChangedEmitter.event;
notifyError(error: SerialError): void {
this.onErrorEmitter.fire(error);
}
notifyWebSocketChanged(message: number): void {
this.onWebSocketChangedEmitter.fire(message);
}
notifyBaudRateChanged(message: SerialConfig.BaudRate): void {
this.onBaudRateChangedEmitter.fire(message);
}
notifyLineEndingChanged(message: SerialModel.EOL): void {
this.onLineEndingChangedEmitter.fire(message);
}
notifyInterpolateChanged(message: boolean): void {
this.onInterpolateChangedEmitter.fire(message);
}
}

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
import { injectable, inject } from '@theia/core/shared/inversify';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import {
Command,

View File

@@ -8,3 +8,8 @@
.monaco-list-row.show-file-icons.focused {
background-color: #d6ebff;
}
.monaco-editor .view-overlays .compiler-error {
background-color: var(--theia-inputValidation-errorBackground);
opacity: 0.4 !important;
}

View File

@@ -27,8 +27,9 @@
}
.ide-updater-dialog .changelog-container {
background: white;
border: 1px solid #dae3e3;
color: var(--theia-dropdown-foreground);
background-color: var(--theia-dropdown-background);
border: 1px solid var(--theia-tree-indentGuidesStroke);
border-radius: 2px;
font-size: 12px;
height: 180px;

View File

@@ -108,3 +108,21 @@ button.theia-button.main {
.fa-reload {
font-size: 14px;
}
/* restore the old Theia spinner */
/* https://github.com/eclipse-theia/theia/pull/10761#issuecomment-1131476318 */
.old-theia-preload {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 50000;
background: var(--theia-editor-background);
background-image: var(--theia-preloader);
background-size: 60px 60px;
background-repeat: no-repeat;
background-attachment: fixed;
background-position: center;
transition: opacity 0.8s;
}

View File

@@ -0,0 +1,10 @@
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import { duration } from '../../../common/decorators';
export class AboutDialog extends TheiaAboutDialog {
@duration({ name: 'theia-about#init' })
protected override async init(): Promise<void> {
// NOOP
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.
}
}

View File

@@ -1,4 +1,4 @@
import { injectable, inject } from 'inversify';
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';
@@ -15,7 +15,7 @@ import {
} from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
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';
@@ -33,7 +33,7 @@ export class ApplicationShell extends TheiaApplicationShell {
@inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService;
protected track(widget: Widget): void {
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
@@ -41,7 +41,7 @@ export class ApplicationShell extends TheiaApplicationShell {
if (widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
this.sketchesServiceClient.currentSketch().then((sketch) => {
if (sketch) {
if (CurrentSketch.isValid(sketch)) {
if (!this.isSketchFile(widget.editor.uri, sketch.uri)) {
return;
}
@@ -61,7 +61,7 @@ export class ApplicationShell extends TheiaApplicationShell {
return false;
}
async addWidget(
override async addWidget(
widget: Widget,
options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
): Promise<void> {
@@ -87,19 +87,19 @@ export class ApplicationShell extends TheiaApplicationShell {
return super.addWidget(widget, { ...options, ref });
}
handleEvent(): boolean {
override handleEvent(): boolean {
// NOOP, dragging has been disabled
return false
return false;
}
// Avoid hiding top panel as we use it for arduino toolbar
protected createTopPanel(): Panel {
protected override createTopPanel(): Panel {
const topPanel = super.createTopPanel();
topPanel.show();
return topPanel;
}
async saveAll(): Promise<void> {
override async saveAll(): Promise<void> {
if (
this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE
) {
@@ -130,5 +130,5 @@ DockPanel.prototype.handleEvent = function (event) {
case 'p-drop':
return;
}
originalHandleEvent(event);
originalHandleEvent.bind(this)(event);
};

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