mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-26 19:48:35 +00:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cf019bc07 | ||
|
|
ad4928957e | ||
|
|
6f6f620ec9 | ||
|
|
b055bd9e41 | ||
|
|
a6cef7c605 | ||
|
|
d56962251e | ||
|
|
69c7383da8 | ||
|
|
f6a8dceabc | ||
|
|
3ed72de810 | ||
|
|
2d9fa5615b | ||
|
|
24bd23b669 | ||
|
|
1bc996d8d8 | ||
|
|
beb529cf15 | ||
|
|
a72533b208 | ||
|
|
acf7b6a8da | ||
|
|
0445700088 | ||
|
|
486f110c80 | ||
|
|
a427cf94f1 | ||
|
|
bd6a6382f6 | ||
|
|
887682b9c3 | ||
|
|
4c63af572e | ||
|
|
40ddd3714b | ||
|
|
2855026cec | ||
|
|
879d5c7cc9 | ||
|
|
8c3fab824f | ||
|
|
0f35821d14 | ||
|
|
fb50244a29 | ||
|
|
8a78e09c6d | ||
|
|
fe3cc1904c | ||
|
|
828379cdc9 | ||
|
|
576f96a502 | ||
|
|
2ba95947de | ||
|
|
68db44fa49 | ||
|
|
36ac97d95d | ||
|
|
a886a106f5 | ||
|
|
1441b685ee | ||
|
|
38ab95973e | ||
|
|
2ffca88c02 | ||
|
|
2b7bceada0 | ||
|
|
5baf43bf25 | ||
|
|
68ff6acb6a | ||
|
|
76f126b913 | ||
|
|
ec18cf0dc1 | ||
|
|
8aa356bd6e | ||
|
|
ea5f528ef0 | ||
|
|
3d6d2ce814 | ||
|
|
e189a8c33e | ||
|
|
2f9ef76cd2 | ||
|
|
ef74b04b0b | ||
|
|
4b859c2db9 | ||
|
|
d53d474243 | ||
|
|
403fdbfdc0 | ||
|
|
cbe603a99f | ||
|
|
1d98e79d7b | ||
|
|
41bf1ce6dc | ||
|
|
1aa944b25e | ||
|
|
b220ce4c5f | ||
|
|
15c66442cc | ||
|
|
3a56c16ab2 | ||
|
|
cea62e315a | ||
|
|
6618816330 | ||
|
|
2577451c15 | ||
|
|
a1ab42d282 | ||
|
|
8dbcb8bbb6 | ||
|
|
1be76aa264 | ||
|
|
21cd580e41 | ||
|
|
8839e318f4 | ||
|
|
bef9185c6c | ||
|
|
b8bd444def | ||
|
|
b8fdb03433 | ||
|
|
4290497edc | ||
|
|
7a89a8048f | ||
|
|
84be804df0 | ||
|
|
f07441a7fc | ||
|
|
08f127711f | ||
|
|
d1ae15a838 | ||
|
|
7ca8830a02 | ||
|
|
13b7a7e1d8 | ||
|
|
c5c9b8674b | ||
|
|
6447191bf5 | ||
|
|
e78ed85761 | ||
|
|
9bc520ccf9 | ||
|
|
cfdb00529c | ||
|
|
8ccea24452 | ||
|
|
ad563d26ba | ||
|
|
f0a628534e | ||
|
|
1b95242ad1 | ||
|
|
729588770e | ||
|
|
6b2046e090 | ||
|
|
80673ad18f | ||
|
|
2f33038695 | ||
|
|
6154d1e8d5 | ||
|
|
557ec2ae42 | ||
|
|
8c49c04359 | ||
|
|
29ebf055e6 | ||
|
|
71842abfa3 | ||
|
|
ed660ccd64 | ||
|
|
6af22ec9b8 | ||
|
|
79f588d067 | ||
|
|
e3e4a96db3 | ||
|
|
4129544738 | ||
|
|
a866bde4d1 | ||
|
|
92b6208a76 | ||
|
|
12deceef19 | ||
|
|
f635751a8c | ||
|
|
85bf50213d | ||
|
|
5aeb2d388e | ||
|
|
b6b4c75718 | ||
|
|
c4a8062df4 | ||
|
|
6e89e89738 | ||
|
|
c7242ca34f | ||
|
|
a4e5e65286 | ||
|
|
80549db289 | ||
|
|
eb7b3ad683 | ||
|
|
9efcbcf2ae | ||
|
|
d22c0b9e55 | ||
|
|
5d2f09354d | ||
|
|
fcd6c792e3 | ||
|
|
94233a1a19 | ||
|
|
7fb32766ca | ||
|
|
85cf8757c4 | ||
|
|
41c56c1126 | ||
|
|
4c503c0c5e | ||
|
|
3f180b6059 | ||
|
|
6a8a76f720 | ||
|
|
9a27252d91 | ||
|
|
4e683b237d | ||
|
|
a2a9cbb02e | ||
|
|
dd10436051 | ||
|
|
e79d42d6bd | ||
|
|
a9c9dcde7b | ||
|
|
62b18ccbed | ||
|
|
0a8b6bc41e | ||
|
|
b1388be5f9 | ||
|
|
b4848f62fa | ||
|
|
f359843635 | ||
|
|
6448b447b3 | ||
|
|
c7bb3abf19 | ||
|
|
c3e2aa4feb | ||
|
|
63cd2701b4 | ||
|
|
35ac73181b | ||
|
|
840cde872c | ||
|
|
c2008460b0 | ||
|
|
435fdcdf7f | ||
|
|
7e6343e60e | ||
|
|
fdda4a72d0 | ||
|
|
7077303a36 | ||
|
|
acd9bf1354 | ||
|
|
d92fc25769 | ||
|
|
f9a98d708e | ||
|
|
df33c5689f | ||
|
|
2dc73eb3b5 | ||
|
|
f6444b2570 | ||
|
|
186180800f | ||
|
|
20bc3c6f13 | ||
|
|
125ee70fa3 | ||
|
|
3cfb1450c0 | ||
|
|
9643dd397f | ||
|
|
7c1ebf273c |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -2,13 +2,16 @@ node_modules/
|
||||
# .node_modules is a hack for the electron builder.
|
||||
.node_modules/
|
||||
lib/
|
||||
build/
|
||||
downloads/
|
||||
build/
|
||||
!electron/build/
|
||||
src-gen/
|
||||
browser-app/webpack.config.js
|
||||
electron-app/webpack.config.js
|
||||
*webpack.config.js
|
||||
.DS_Store
|
||||
/workspace/static
|
||||
.DS_Store
|
||||
# switching from `electron` to `browser` in dev mode.
|
||||
.browser_modules
|
||||
# LS logs
|
||||
inols*.log
|
||||
yarn-error.log
|
||||
|
||||
@@ -2,8 +2,12 @@ image:
|
||||
file: Dockerfile
|
||||
|
||||
ports:
|
||||
- port: 3000
|
||||
onOpen: open-browser
|
||||
- port: 3000
|
||||
onOpen: open-preview
|
||||
- port: 5900
|
||||
onOpen: ignore
|
||||
- port: 6080
|
||||
onOpen: ignore
|
||||
|
||||
tasks:
|
||||
- init: >
|
||||
|
||||
146
.vscode/launch.json
vendored
146
.vscode/launch.json
vendored
@@ -1,39 +1,111 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Electron Packager",
|
||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||
"cwd": "${workspaceFolder}/electron/packager"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Backend",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Electron Packager",
|
||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||
"cwd": "${workspaceFolder}/electron/packager"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Electron)",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"program": "${workspaceRoot}/electron-app/src-gen/frontend/electron-main.js",
|
||||
"protocol": "inspector",
|
||||
"args": [
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Browser)",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install"
|
||||
],
|
||||
"windows": {
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Browser - Debug CLI daemon)",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install",
|
||||
"--debug-cli=true"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@@ -1,3 +1,21 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
"tslint.enable": true,
|
||||
"tslint.configFile": "./tslint.json",
|
||||
"editor.formatOnSave": true,
|
||||
"files.exclude": {
|
||||
"**/lib": false
|
||||
},
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false,
|
||||
"[typescript]": {
|
||||
"editor.tabSize": 4
|
||||
},
|
||||
"[json]": {
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"files.insertFinalNewline": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM gitpod/workspace-full
|
||||
FROM gitpod/workspace-full-vnc
|
||||
|
||||
USER root
|
||||
RUN apt-get update -q --fix-missing && \
|
||||
@@ -7,7 +7,8 @@ RUN apt-get update -q --fix-missing && \
|
||||
build-essential \
|
||||
libssl-dev \
|
||||
golang-go \
|
||||
libxkbfile-dev
|
||||
libxkbfile-dev \
|
||||
libnss3-dev
|
||||
|
||||
RUN set -ex && \
|
||||
tmpdir=$(mktemp -d) && \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Arduino IDE PoC
|
||||
# Arduino Pro IDE
|
||||
|
||||
[](https://dev.azure.com/typefox/Arduino/_build/latest?definitionId=4&branchName=master)
|
||||
|
||||
@@ -29,6 +29,9 @@ Then you can start the browser example again:
|
||||
yarn --cwd browser-app start
|
||||
```
|
||||
|
||||
Click [here](./arduino-ide-extension/README.md) for more details on various IDE services, and the Arduino Pro IDE implementation in general.
|
||||
|
||||
|
||||
## Arduino Pro IDE Electron Application
|
||||
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
|
||||
|
||||
@@ -41,7 +44,7 @@ You can download the latest release applications fom [here](https://github.com/b
|
||||
If you want to get a nightly build, go to the [Azure DevOps page](https://dev.azure.com/typefox/Arduino/_build?definitionId=4),
|
||||
and follow the steps from below.
|
||||
|
||||

|
||||

|
||||
<img width="500" src="static/download_01.gif">
|
||||
<img width="500" src="static/download_02.gif">
|
||||
|
||||
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.
|
||||
|
||||
40
arduino-debugger-extension/package.json
Normal file
40
arduino-debugger-extension/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "arduino-debugger-extension",
|
||||
"version": "0.0.5",
|
||||
"description": "An extension for debugging Arduino programs",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theia/debug": "next",
|
||||
"arduino-ide-extension": "0.0.5",
|
||||
"cdt-gdb-adapter": "^0.0.14",
|
||||
"vscode-debugadapter": "^1.26.0",
|
||||
"vscode-debugprotocol": "^1.26.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "yarn run clean && yarn run build",
|
||||
"clean": "rimraf lib",
|
||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
||||
"build": "tsc && yarn lint",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^2.6.1",
|
||||
"tslint": "^5.5.0",
|
||||
"typescript": "3.5.1"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"build",
|
||||
"data"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/backend-module",
|
||||
"frontend": "lib/browser/frontend-module"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DebugConfigurationManager } from "@theia/debug/lib/browser/debug-configuration-manager";
|
||||
import { injectable } from "inversify";
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDebugConfigurationManager extends DebugConfigurationManager {
|
||||
|
||||
get defaultDebugger(): Promise<string | undefined> {
|
||||
return this.debug.getDebuggersForLanguage('ino').then(debuggers => {
|
||||
if (debuggers.length === 0)
|
||||
return undefined;
|
||||
return debuggers[0].type;
|
||||
});
|
||||
}
|
||||
|
||||
protected async selectDebugType(): Promise<string | undefined> {
|
||||
const widget = this.editorManager.currentEditor;
|
||||
if (!widget) {
|
||||
return this.defaultDebugger;
|
||||
}
|
||||
const { languageId } = widget.editor.document;
|
||||
const debuggers = await this.debug.getDebuggersForLanguage(languageId);
|
||||
if (debuggers.length === 0) {
|
||||
return this.defaultDebugger;
|
||||
}
|
||||
return this.quickPick.show(debuggers.map(
|
||||
({ label, type }) => ({ label, value: type }),
|
||||
{ placeholder: 'Select Environment' })
|
||||
);
|
||||
}
|
||||
|
||||
async createDefaultConfiguration(): Promise<void> {
|
||||
const { model } = this;
|
||||
if (model) {
|
||||
await this.doCreate(model);
|
||||
await this.updateModels();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MenuModelRegistry, Path, MessageService, Command, CommandRegistry } from '@theia/core';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser';
|
||||
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { DebugFrontendApplicationContribution, DebugCommands } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
|
||||
import { DebugSessionOptions } from "@theia/debug/lib/browser/debug-session-options";
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { EditorMode } from "arduino-ide-extension/lib/browser/editor-mode";
|
||||
import { SketchesService } from 'arduino-ide-extension/lib/common/protocol/sketches-service';
|
||||
import { ArduinoToolbar } from 'arduino-ide-extension/lib/browser/toolbar/arduino-toolbar';
|
||||
import { ArduinoDebugConfigurationManager } from './arduino-debug-configuration-manager';
|
||||
|
||||
export namespace ArduinoDebugCommands {
|
||||
export const START_DEBUG: Command = {
|
||||
id: 'arduino-start-debug',
|
||||
label: 'Start Debugging'
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendApplicationContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchesService: SketchesService;
|
||||
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
async start(noDebug?: boolean, debugSessionOptions?: DebugSessionOptions): Promise<void> {
|
||||
const configurations = this.configurations as ArduinoDebugConfigurationManager;
|
||||
let current = debugSessionOptions ? debugSessionOptions : configurations.current;
|
||||
// If no configurations are currently present, create them
|
||||
if (!current) {
|
||||
await configurations.createDefaultConfiguration();
|
||||
current = configurations.current;
|
||||
}
|
||||
if (current) {
|
||||
if (noDebug !== undefined) {
|
||||
current = {
|
||||
...current,
|
||||
configuration: {
|
||||
...current.configuration,
|
||||
noDebug
|
||||
}
|
||||
};
|
||||
}
|
||||
if (current.configuration.type === 'arduino') {
|
||||
const wsStat = this.workspaceService.workspace;
|
||||
let sketchFileURI: URI | undefined;
|
||||
if (wsStat && await this.sketchesService.isSketchFolder(wsStat.uri)) {
|
||||
const wsPath = new Path(wsStat.uri);
|
||||
const sketchFilePath = wsPath.join(wsPath.name + '.ino').toString();
|
||||
sketchFileURI = new URI(sketchFilePath);
|
||||
} else if (this.editorManager.currentEditor) {
|
||||
const editorURI = this.editorManager.currentEditor.getResourceUri();
|
||||
if (editorURI && editorURI.path && editorURI.path.ext === '.ino') {
|
||||
sketchFileURI = editorURI;
|
||||
}
|
||||
}
|
||||
if (sketchFileURI) {
|
||||
await this.editorManager.open(sketchFileURI);
|
||||
await this.manager.start(current);
|
||||
} else {
|
||||
this.messageService.error('Please open a sketch file to start debugging.')
|
||||
}
|
||||
} else {
|
||||
await this.manager.start(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initializeLayout(): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.editorMode.proMode) {
|
||||
super.registerMenus(menus);
|
||||
menus.unregisterMenuAction(DebugCommands.START_NO_DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
if (this.editorMode.proMode) {
|
||||
super.registerKeybindings(keybindings);
|
||||
keybindings.unregisterKeybinding({
|
||||
command: DebugCommands.START_NO_DEBUG.id,
|
||||
keybinding: 'ctrl+f5'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerToolbarItems(toolbar: TabBarToolbarRegistry): void {
|
||||
super.registerToolbarItems(toolbar);
|
||||
toolbar.registerItem({
|
||||
id: ArduinoDebugCommands.START_DEBUG.id,
|
||||
command: ArduinoDebugCommands.START_DEBUG.id,
|
||||
tooltip: 'Start Debugging',
|
||||
priority: 1
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
super.registerCommands(registry);
|
||||
registry.registerCommand(ArduinoDebugCommands.START_DEBUG, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => {
|
||||
registry.executeCommand(DebugCommands.START.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DebugSessionManager } from "@theia/debug/lib/browser/debug-session-manager";
|
||||
import { DebugSessionOptions } from "@theia/debug/lib/browser/debug-session-options";
|
||||
|
||||
export class ArduinoDebugSessionManager extends DebugSessionManager {
|
||||
|
||||
start(options: DebugSessionOptions) {
|
||||
if (options.configuration.type === 'arduino' && this.sessions.find(s => s.configuration.type === 'arduino')) {
|
||||
this.messageService.info('A debug session is already running. You must stop the running session before starting a new one.')
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return super.start(options);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { BoardsServiceClientImpl } from 'arduino-ide-extension/lib/browser/boards/boards-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoVariableResolver implements VariableContribution {
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService
|
||||
|
||||
registerVariables(variables: VariableRegistry): void {
|
||||
variables.registerVariable(<Variable>{
|
||||
name: 'fqbn',
|
||||
description: 'Qualified name of the selected board',
|
||||
resolve: this.resolveFqbn.bind(this),
|
||||
});
|
||||
variables.registerVariable({
|
||||
name: 'port',
|
||||
description: 'Selected upload port',
|
||||
resolve: this.resolvePort.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
protected async resolveFqbn(): Promise<string | undefined> {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
this.messageService.error('No board selected. Please select a board for debugging.');
|
||||
return undefined;
|
||||
}
|
||||
return boardsConfig.selectedBoard.fqbn;
|
||||
}
|
||||
|
||||
protected async resolvePort(): Promise<string | undefined> {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedPort) {
|
||||
return undefined;
|
||||
}
|
||||
return boardsConfig.selectedPort.address;
|
||||
}
|
||||
|
||||
}
|
||||
20
arduino-debugger-extension/src/browser/frontend-module.ts
Normal file
20
arduino-debugger-extension/src/browser/frontend-module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { VariableContribution } from '@theia/variable-resolver/lib/browser';
|
||||
import { ArduinoVariableResolver } from './arduino-variable-resolver';
|
||||
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { DebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
|
||||
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||
import { ArduinoDebugConfigurationManager } from './arduino-debug-configuration-manager';
|
||||
import { ArduinoDebugFrontendApplicationContribution } from './arduino-debug-frontend-application-contribution';
|
||||
import { ArduinoDebugSessionManager } from './arduino-debug-session-manager';
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ArduinoVariableResolver).toSelf().inSingletonScope();
|
||||
bind(VariableContribution).toService(ArduinoVariableResolver);
|
||||
rebind(DebugSessionManager).to(ArduinoDebugSessionManager).inSingletonScope();
|
||||
rebind(DebugConfigurationManager).to(ArduinoDebugConfigurationManager).inSingletonScope();
|
||||
rebind(DebugFrontendApplicationContribution).to(ArduinoDebugFrontendApplicationContribution);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
|
||||
<!--Copyright (C) 2019 TypeFox and others.-->
|
||||
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="#F6F6F6" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M15.1673 18.0687V23.0247C15.1673 23.5637 15.2723 24.5 14.7315 24.5H12.8328V23.3327H14V19.6122L13.7988 19.4022C13.0604 20.0803 12.1008 20.4669 11.0986 20.49C10.0964 20.5132 9.11994 20.1714 8.351 19.5282C7 18.1737 7.13826 16.3327 8.60475 14H4.66726V15.1672H3.50001V13.2685C3.50001 12.7277 4.43626 12.8327 4.97526 12.8327H9.76326L15.1673 18.0687ZM11.6673 5.83275H10.5V4.66725H12.775C13.3123 4.66725 14 4.9245 14 5.4635V9.366L14.8593 10.3862C14.927 9.83979 15.1906 9.33644 15.6013 8.96958C16.0119 8.60271 16.5416 8.39723 17.0923 8.39125C17.2298 8.37945 17.3684 8.39492 17.5 8.43675V5.83275H18.6673V8.88825C18.703 8.99154 18.7618 9.08536 18.8391 9.16265C18.9164 9.23995 19.0102 9.29871 19.1135 9.3345H22.1673V10.5H19.5615C19.593 10.5 19.6105 10.675 19.6105 10.85C19.6058 11.4034 19.4011 11.9365 19.0341 12.3508C18.6671 12.7651 18.1626 13.0326 17.6138 13.104L18.634 14H22.5383C23.0773 14 23.3345 14.6807 23.3345 15.225V17.5H22.1673V16.3327H19.2273L11.6673 8.98275V5.83275ZM14 0C11.2311 0 8.52431 0.821086 6.22202 2.35943C3.91973 3.89776 2.12532 6.08426 1.06569 8.64243C0.00606593 11.2006 -0.271181 14.0155 0.269012 16.7313C0.809205 19.447 2.14258 21.9416 4.10051 23.8995C6.05845 25.8574 8.55301 27.1908 11.2687 27.731C13.9845 28.2712 16.7994 27.9939 19.3576 26.9343C21.9157 25.8747 24.1022 24.0803 25.6406 21.778C27.1789 19.4757 28 16.7689 28 14C28 10.287 26.525 6.72601 23.8995 4.1005C21.274 1.475 17.713 0 14 0V0ZM25.6673 14C25.6692 16.6908 24.7364 19.2988 23.0283 21.378L6.622 4.97175C8.33036 3.57269 10.4009 2.68755 12.5927 2.41935C14.7845 2.15115 17.0074 2.51091 19.0027 3.45676C20.998 4.40262 22.6836 5.89567 23.8635 7.76217C25.0433 9.62867 25.6689 11.7919 25.6673 14ZM2.33276 14C2.33066 11.3091 3.26351 8.70111 4.97176 6.622L21.378 23.03C19.6693 24.4284 17.5987 25.313 15.407 25.5807C13.2153 25.8485 10.9926 25.4884 8.99754 24.5425C7.00244 23.5965 5.31693 22.1036 4.13708 20.2373C2.95722 18.3709 2.33152 16.208 2.33276 14Z" fill="white"/></g><defs><clipPath id="clip0"><rect width="28" height="28" fill="#F6F6F6"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
16
arduino-debugger-extension/src/browser/style/index.css
Normal file
16
arduino-debugger-extension/src/browser/style/index.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.arduino-start-debug-icon {
|
||||
-webkit-mask: url('debug-dark.svg') 50%;
|
||||
mask: url('debug-dark.svg') 50%;
|
||||
-webkit-mask-size: 100%;
|
||||
mask-size: 100%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theia-ui-button-font-color);
|
||||
}
|
||||
|
||||
.arduino-start-debug {
|
||||
border-radius: 12px;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as path from 'path';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { DebugAdapterContribution, DebugAdapterExecutable } from '@theia/debug/lib/common/debug-model';
|
||||
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
|
||||
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
|
||||
import { ArduinoCli } from 'arduino-ide-extension/lib/node/arduino-cli';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
|
||||
|
||||
readonly type = 'arduino';
|
||||
readonly label = 'Arduino';
|
||||
readonly languages = ['c', 'cpp', 'ino'];
|
||||
|
||||
@inject(ArduinoCli) arduinoCli: ArduinoCli;
|
||||
|
||||
getSchemaAttributes(): IJSONSchema[] {
|
||||
return [
|
||||
{
|
||||
'properties': {
|
||||
'sketch': {
|
||||
'type': 'string',
|
||||
'description': 'path to the sketch root ino file',
|
||||
'default': '${file}',
|
||||
},
|
||||
'pauseAtMain': {
|
||||
'description': 'If enabled the debugger will pause at the start of the main function.',
|
||||
'type': 'boolean',
|
||||
'default': false
|
||||
},
|
||||
'debugDebugAdapter': {
|
||||
'type': 'boolean',
|
||||
'description': 'Start the debug adapter in debug mode (with --inspect-brk)',
|
||||
'default': false
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
provideDebugAdapterExecutable(config: DebugConfiguration): DebugAdapterExecutable {
|
||||
const debugAdapterMain = path.join(__dirname, 'debug-adapter', 'main');
|
||||
if (config.debugDebugAdapter) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: ['--inspect-brk', debugAdapterMain],
|
||||
}
|
||||
}
|
||||
return {
|
||||
modulePath: debugAdapterMain,
|
||||
args: [],
|
||||
}
|
||||
}
|
||||
|
||||
provideDebugConfigurations(): DebugConfiguration[] {
|
||||
return [
|
||||
<DebugConfiguration>{
|
||||
name: this.label,
|
||||
type: this.type,
|
||||
request: 'launch'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async resolveDebugConfiguration(config: DebugConfiguration): Promise<DebugConfiguration> {
|
||||
const startFunction = config.pauseAtMain ? 'main' : 'setup';
|
||||
const res: ActualDebugConfig = {
|
||||
...config,
|
||||
arduinoCli: await this.arduinoCli.getExecPath(),
|
||||
fqbn: '${fqbn}',
|
||||
uploadPort: '${port}',
|
||||
initCommands: [
|
||||
`-break-insert -t --function ${startFunction}`
|
||||
]
|
||||
}
|
||||
if (!res.sketch) {
|
||||
res.sketch = '${file}';
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ActualDebugConfig extends DebugConfiguration {
|
||||
arduinoCli?: string;
|
||||
sketch?: string;
|
||||
fqbn?: string;
|
||||
uploadPort?: string;
|
||||
}
|
||||
7
arduino-debugger-extension/src/node/backend-module.ts
Normal file
7
arduino-debugger-extension/src/node/backend-module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model';
|
||||
import { ArduinoDebugAdapterContribution } from './arduino-debug-adapter-contribution';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(DebugAdapterContribution).to(ArduinoDebugAdapterContribution).inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { GDBDebugSession, FrameVariableReference } from 'cdt-gdb-adapter/dist/GDBDebugSession';
|
||||
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
|
||||
import * as mi from 'cdt-gdb-adapter/dist/mi';
|
||||
import { ArduinoGDBBackend } from './arduino-gdb-backend';
|
||||
import { ArduinoVariableHandler } from './arduino-variable-handler';
|
||||
import { Scope, OutputEvent } from 'vscode-debugadapter';
|
||||
|
||||
export interface ArduinoLaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
|
||||
arduinoCli?: string;
|
||||
sketch?: string;
|
||||
fqbn?: string;
|
||||
uploadPort?: string;
|
||||
}
|
||||
|
||||
const GLOBAL_HANDLE_ID = 0xFE;
|
||||
const STATIC_HANDLES_START = 0x010000;
|
||||
const STATIC_HANDLES_FINISH = 0x01FFFF;
|
||||
|
||||
export class ArduinoDebugSession extends GDBDebugSession {
|
||||
|
||||
private _variableHandler: ArduinoVariableHandler;
|
||||
|
||||
get arduinoBackend(): ArduinoGDBBackend {
|
||||
return this.gdb as ArduinoGDBBackend;
|
||||
}
|
||||
|
||||
protected get variableHandler() {
|
||||
if (this._variableHandler) {
|
||||
return this._variableHandler;
|
||||
}
|
||||
if (!this.gdb) {
|
||||
throw new Error("GDB backend is not ready.");
|
||||
}
|
||||
const handler = new ArduinoVariableHandler(this, this.frameHandles, this.variableHandles);
|
||||
this._variableHandler = handler;
|
||||
return handler;
|
||||
}
|
||||
|
||||
protected createBackend(): GDBBackend {
|
||||
return new ArduinoGDBBackend();
|
||||
}
|
||||
|
||||
protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): Promise<void> {
|
||||
try {
|
||||
await this.gdb.sendCommand('-interpreter-exec console "monitor reset halt"')
|
||||
await mi.sendExecContinue(this.gdb);
|
||||
this.sendResponse(response);
|
||||
} catch (err) {
|
||||
this.sendErrorResponse(response, 100, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
const message = 'Pause is not supported on Windows. Please stop the debug session and set a breakpoint instead.';
|
||||
this.sendEvent(new OutputEvent(message));
|
||||
this.sendErrorResponse(response, 1, message);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return super.pauseRequest(response, args);
|
||||
}
|
||||
|
||||
protected async disconnectRequest(response: DebugProtocol.DisconnectResponse): Promise<void> {
|
||||
try {
|
||||
if (this.isRunning) {
|
||||
if (process.platform === 'win32') {
|
||||
// We cannot pause on Windows
|
||||
this.arduinoBackend.kill();
|
||||
this.sendResponse(response);
|
||||
return;
|
||||
}
|
||||
// Need to pause first
|
||||
const waitPromise = new Promise(resolve => this.waitPaused = resolve);
|
||||
this.gdb.pause();
|
||||
await waitPromise;
|
||||
}
|
||||
await this.gdb.sendGDBExit();
|
||||
this.sendResponse(response);
|
||||
} catch (err) {
|
||||
this.sendErrorResponse(response, 1, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
|
||||
try {
|
||||
const frame: FrameVariableReference = {
|
||||
type: 'frame',
|
||||
frameHandle: args.frameId,
|
||||
};
|
||||
// const pins: ObjectVariableReference = {
|
||||
// type: "object",
|
||||
// varobjName: "__pins",
|
||||
// frameHandle: 42000,
|
||||
// }
|
||||
|
||||
response.body = {
|
||||
scopes: [
|
||||
// new Scope('Pins', this.variableHandles.create(pins), false),
|
||||
new Scope('Local', this.variableHandles.create(frame), false),
|
||||
new Scope('Global', GLOBAL_HANDLE_ID, false),
|
||||
// new Scope('Static', STATIC_HANDLES_START + parseInt(args.frameId as any, 10), false)
|
||||
],
|
||||
};
|
||||
|
||||
this.sendResponse(response);
|
||||
} catch (err) {
|
||||
this.sendErrorResponse(response, 1, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise<void> {
|
||||
try {
|
||||
response.body = {
|
||||
variables: [] as DebugProtocol.Variable[]
|
||||
};
|
||||
const ref = this.variableHandles.get(args.variablesReference);
|
||||
if (args.variablesReference === GLOBAL_HANDLE_ID) {
|
||||
// Use hardcoded global handle to load and store global variables
|
||||
response.body.variables = await this.variableHandler.getGlobalVariables();
|
||||
} else if (args.variablesReference >= STATIC_HANDLES_START && args.variablesReference <= STATIC_HANDLES_FINISH) {
|
||||
// Use STATIC_HANDLES_START to shift the framehandles back
|
||||
const frameHandle = args.variablesReference - STATIC_HANDLES_START;
|
||||
response.body.variables = await this.variableHandler.getStaticVariables(frameHandle);
|
||||
} else if (ref && ref.type === 'frame') {
|
||||
// List variables for current frame
|
||||
response.body.variables = await this.handleVariableRequestFrame(ref);
|
||||
} else if (ref && ref.varobjName === '__pins') {
|
||||
response.body.variables = await this.variableHandler.handlePinStatusRequest();
|
||||
} else if (ref && ref.type === 'object') {
|
||||
// List data under any variable
|
||||
response.body.variables = await this.handleVariableRequestObject(ref);
|
||||
}
|
||||
this.sendResponse(response);
|
||||
} catch (err) {
|
||||
this.sendErrorResponse(response, 1, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'arduino-ide-extension/lib/node/fs-extra'
|
||||
import { spawn } from 'child_process';
|
||||
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
|
||||
import { MIFrameInfo } from 'cdt-gdb-adapter/dist/mi';
|
||||
import { ArduinoLaunchRequestArguments } from './arduino-debug-session';
|
||||
import { ArduinoParser } from './arduino-parser';
|
||||
|
||||
export class ArduinoGDBBackend extends GDBBackend {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.parser = new ArduinoParser(this);
|
||||
}
|
||||
|
||||
spawn(requestArgs: ArduinoLaunchRequestArguments): Promise<void> {
|
||||
if (!requestArgs.sketch) {
|
||||
throw new Error('Missing argument: sketch');
|
||||
}
|
||||
if (!requestArgs.fqbn) {
|
||||
throw new Error('Missing argument: fqbn')
|
||||
}
|
||||
const sketchFS = fs.statSync(requestArgs.sketch);
|
||||
const sketchDir = sketchFS.isFile() ? path.dirname(requestArgs.sketch) : requestArgs.sketch;
|
||||
const command = requestArgs.arduinoCli || 'arduino-cli';
|
||||
const args = [
|
||||
'debug',
|
||||
'-p', requestArgs.uploadPort || 'none',
|
||||
'-b', requestArgs.fqbn,
|
||||
sketchDir
|
||||
];
|
||||
const proc = spawn(command, args);
|
||||
this.proc = proc;
|
||||
this.out = proc.stdin;
|
||||
return (this.parser as ArduinoParser).parseFull(proc);
|
||||
}
|
||||
|
||||
sendFileExecAndSymbols(): Promise<void> {
|
||||
// The program file is already sent by `arduino-cli`
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
sendExecInterrupt(threadId?: number) {
|
||||
let command = '-exec-interrupt';
|
||||
if (threadId) {
|
||||
command += ` --thread ${threadId}`;
|
||||
}
|
||||
return this.sendCommand(command);
|
||||
}
|
||||
|
||||
sendStackInfoFrame(threadId: number, frameId: number): Promise<{ frame: MIFrameInfo }> {
|
||||
const command = `-stack-info-frame --thread ${threadId} --frame ${frameId}`;
|
||||
return this.sendCommand(command);
|
||||
}
|
||||
|
||||
sendTargetDetach(): Promise<void> {
|
||||
return this.sendCommand('-target-detach');
|
||||
}
|
||||
|
||||
kill(): void {
|
||||
if (!this.proc) {
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
spawn('taskkill', ['/pid', this.proc.pid.toString(), '/f', '/t']);
|
||||
} else {
|
||||
this.proc.kill('SIGKILL');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import { Readable } from 'stream';
|
||||
import { MIParser } from "cdt-gdb-adapter/dist/MIParser";
|
||||
|
||||
const LINE_REGEX = /(.*)(\r?\n)/;
|
||||
|
||||
export class ArduinoParser extends MIParser {
|
||||
|
||||
protected rejectReady?: (error: Error) => void;
|
||||
|
||||
parseFull(proc: ChildProcessWithoutNullStreams): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Detect errors when the child process could not be spawned
|
||||
proc.on('error', reject);
|
||||
|
||||
this.waitReady = () => {
|
||||
this.rejectReady = undefined;
|
||||
resolve();
|
||||
}
|
||||
this.rejectReady = (error: Error) => {
|
||||
this.waitReady = undefined;
|
||||
reject(error);
|
||||
}
|
||||
// Detect unexpected termination
|
||||
proc.on('exit', () => {
|
||||
if (this.rejectReady) {
|
||||
this.rejectReady(new Error('The gdb debugger terminated unexpectedly.'));
|
||||
}
|
||||
});
|
||||
this.readInputStream(proc.stdout);
|
||||
this.readErrorStream(proc.stderr);
|
||||
});
|
||||
}
|
||||
|
||||
private readInputStream(stream: Readable) {
|
||||
let buff = '';
|
||||
stream.on('data', chunk => {
|
||||
buff += chunk.toString();
|
||||
let regexArray = LINE_REGEX.exec(buff);
|
||||
while (regexArray) {
|
||||
const line = regexArray[1];
|
||||
this.line = line;
|
||||
this.pos = 0;
|
||||
this.handleLine();
|
||||
// Detect error emitted as log message
|
||||
if (this.rejectReady && line.toLowerCase().startsWith('&"error')) {
|
||||
this.pos = 1;
|
||||
this.rejectReady(new Error(this.handleCString() || regexArray[1]));
|
||||
this.rejectReady = undefined;
|
||||
}
|
||||
buff = buff.substring(regexArray[1].length + regexArray[2].length);
|
||||
regexArray = LINE_REGEX.exec(buff);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private readErrorStream(stream: Readable) {
|
||||
let buff = '';
|
||||
stream.on('data', chunk => {
|
||||
buff += chunk.toString();
|
||||
let regexArray = LINE_REGEX.exec(buff);
|
||||
while (regexArray) {
|
||||
const line = regexArray[1];
|
||||
this.gdb.emit('consoleStreamOutput', line + '\n', 'stderr');
|
||||
// Detect error emitted on the stderr stream
|
||||
if (this.rejectReady && line.toLowerCase().startsWith('error')) {
|
||||
this.rejectReady(new Error(line));
|
||||
this.rejectReady = undefined;
|
||||
}
|
||||
buff = buff.substring(regexArray[1].length + regexArray[2].length);
|
||||
regexArray = LINE_REGEX.exec(buff);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as path from 'path';
|
||||
import { DebugProtocol } from "vscode-debugprotocol";
|
||||
import { Handles } from 'vscode-debugadapter/lib/handles';
|
||||
import { FrameReference, VariableReference } from "cdt-gdb-adapter/dist/GDBDebugSession";
|
||||
import { VarManager } from 'cdt-gdb-adapter/dist/varManager';
|
||||
import * as mi from 'cdt-gdb-adapter/dist/mi';
|
||||
import { ArduinoDebugSession } from "./arduino-debug-session";
|
||||
import { ArduinoGDBBackend } from './arduino-gdb-backend';
|
||||
|
||||
export class ArduinoVariableHandler {
|
||||
|
||||
protected readonly gdb: ArduinoGDBBackend;
|
||||
protected readonly varMgr: VarManager;
|
||||
|
||||
protected globalHandle: number;
|
||||
|
||||
constructor(protected readonly session: ArduinoDebugSession,
|
||||
protected frameHandles: Handles<FrameReference>,
|
||||
protected variableHandles: Handles<VariableReference>) {
|
||||
this.gdb = session.arduinoBackend;
|
||||
this.varMgr = new VarManager(this.gdb);
|
||||
}
|
||||
|
||||
createGlobalHandle() {
|
||||
this.globalHandle = this.frameHandles.create({
|
||||
threadId: -1,
|
||||
frameId: -1
|
||||
});
|
||||
}
|
||||
|
||||
/** TODO */
|
||||
async getGlobalVariables(): Promise<DebugProtocol.Variable[]> {
|
||||
throw new Error('Global variables are not supported yet.');
|
||||
const frame = this.frameHandles.get(this.globalHandle);
|
||||
const symbolInfo: any[] = [] // this.symbolTable.getGlobalVariables();
|
||||
const variables: DebugProtocol.Variable[] = [];
|
||||
|
||||
for (const symbol of symbolInfo) {
|
||||
const name = `global_var_${symbol.name}`;
|
||||
const variable = await this.getVariables(frame, name, symbol.name, -1);
|
||||
variables.push(variable);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/** TODO */
|
||||
async getStaticVariables(frameHandle: number): Promise<DebugProtocol.Variable[]> {
|
||||
throw new Error('Static variables are not supported yet.');
|
||||
const frame = this.frameHandles.get(frameHandle);
|
||||
const result = await this.gdb.sendStackInfoFrame(frame.threadId, frame.frameId);
|
||||
const file = path.normalize(result.frame.file || '');
|
||||
const symbolInfo: any[] = [] // this.symbolTable.getStaticVariables(file);
|
||||
const variables: DebugProtocol.Variable[] = [];
|
||||
|
||||
// Fetch stack depth to obtain frameId/threadId/depth tuple
|
||||
const stackDepth = await mi.sendStackInfoDepth(this.gdb, { maxDepth: 100 });
|
||||
const depth = parseInt(stackDepth.depth, 10);
|
||||
|
||||
for (const symbol of symbolInfo) {
|
||||
const name = `${file}_static_var_${symbol.name}`;
|
||||
const variable = await this.getVariables(frame, name, symbol.name, depth);
|
||||
variables.push(variable);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
private async getVariables(frame: FrameReference, name: string, expression: string, depth: number): Promise<DebugProtocol.Variable> {
|
||||
let global = this.varMgr.getVar(frame.frameId, frame.threadId, depth, name);
|
||||
|
||||
if (global) {
|
||||
// Update value if it is already loaded
|
||||
const vup = await mi.sendVarUpdate(this.gdb, { name });
|
||||
const update = vup.changelist[0];
|
||||
if (update && update.in_scope === 'true' && update.name === global.varname) {
|
||||
global.value = update.value;
|
||||
}
|
||||
} else {
|
||||
// create var in GDB and store it in the varMgr
|
||||
const varCreateResponse = await mi.sendVarCreate(this.gdb, {
|
||||
name,
|
||||
frame: 'current',
|
||||
expression,
|
||||
});
|
||||
|
||||
global = this.varMgr.addVar(frame.frameId, frame.threadId, depth, name, true, false, varCreateResponse);
|
||||
}
|
||||
|
||||
return {
|
||||
name: expression,
|
||||
value: (global.value === void 0) ? '<unknown>' : global.value,
|
||||
type: global.type,
|
||||
variablesReference: parseInt(global.numchild, 10) > 0
|
||||
? this.variableHandles.create({
|
||||
frameHandle: this.globalHandle,
|
||||
type: 'object',
|
||||
varobjName: global.varname,
|
||||
})
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async handlePinStatusRequest(): Promise<DebugProtocol.Variable[]> {
|
||||
const variables: DebugProtocol.Variable[] = [];
|
||||
variables.push({
|
||||
name: "D2",
|
||||
type: "gpio",
|
||||
value: "0x00",
|
||||
variablesReference: 0
|
||||
})
|
||||
return variables;
|
||||
}
|
||||
|
||||
}
|
||||
10
arduino-debugger-extension/src/node/debug-adapter/main.ts
Normal file
10
arduino-debugger-extension/src/node/debug-adapter/main.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as process from 'process';
|
||||
import { logger } from 'vscode-debugadapter/lib/logger';
|
||||
import { ArduinoDebugSession } from './arduino-debug-session';
|
||||
import { DebugSession } from 'vscode-debugadapter';
|
||||
|
||||
process.on('uncaughtException', (err: any) => {
|
||||
logger.error(JSON.stringify(err));
|
||||
});
|
||||
|
||||
DebugSession.run(ArduinoDebugSession);
|
||||
31
arduino-debugger-extension/tsconfig.json
Normal file
31
arduino-debugger-extension/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noEmitOnError": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "lib",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"files": [
|
||||
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
|
||||
]
|
||||
}
|
||||
37
arduino-debugger-extension/tslint.json
Normal file
37
arduino-debugger-extension/tslint.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [true, "check-space"],
|
||||
"curly": false,
|
||||
"forin": false,
|
||||
"indent": [true, "spaces"],
|
||||
"max-line-length": [true, 180],
|
||||
"no-trailing-whitespace": false,
|
||||
"no-unused-expression": true,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"radix": true,
|
||||
"trailing-comma": [false],
|
||||
"triple-equals": [true, "allow-null-check"],
|
||||
"typedef-whitespace": [true, {
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}],
|
||||
"variable-name": false,
|
||||
"whitespace": [true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
||||
52
arduino-ide-extension/README.md
Normal file
52
arduino-ide-extension/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
## Arduino IDE Extension
|
||||
|
||||
Arduino Pro IDE is based on Theia, and most of its IDE features, UIs and customizations are implemented in this Theia extension.
|
||||
|
||||
### IDE Services
|
||||
|
||||
IDE services typically have a backend part in [src/node/](./src/node/) and a front-end part in [src/browser/](./src/browser/).
|
||||
|
||||
#### Boards Service
|
||||
|
||||
The Boards Service continuously checks the computer's ports, in order to detect when you connect or disconnect an Arduino board.
|
||||
|
||||
The Boards Manager lists all the known board types, and allows downloading new cores to get additional board types.
|
||||
|
||||
- [src/common/protocol/boards-service.ts](./src/common/protocol/boards-service.ts) implements the common classes and interfaces
|
||||
- [src/node/boards-service-impl.ts](./src/node/boards-service-impl.ts) implements the service backend:
|
||||
- discovering ports & boards
|
||||
- searching for compatible board types
|
||||
- installing new board types
|
||||
- [src/browser/boards/boards-list-widget.ts](./src/browser/boards/boards-service-client-impl.ts) implements the Boards Manager front-end:
|
||||
- browsing/searching available board types
|
||||
- installing new board types
|
||||
|
||||
#### Core Service
|
||||
|
||||
The Core Service is responsible for building your sketches and uploading them to a board.
|
||||
|
||||
- [src/common/protocol/core-service.ts](./src/common/protocol/core-service.ts) implements the common classes and interfaces
|
||||
- [src/node/core-service-impl.ts](./src/node/core-service-impl.ts) implements the service backend:
|
||||
- compiling a sketch for a selected board type
|
||||
- uploading a sketch to a connected board
|
||||
|
||||
#### Monitor Service
|
||||
|
||||
The Monitor Service allows getting information back from sketches running on your Arduino boards.
|
||||
|
||||
- [src/common/protocol/monitor-service.ts](./src/common/protocol/monitor-service.ts) implements the common classes and interfaces
|
||||
- [src/node/monitor-service-impl.ts](./src/node/monitor-service-impl.ts) implements the service backend:
|
||||
- connecting to / disconnecting from a board
|
||||
- receiving and sending data
|
||||
- [src/browser/monitor/monitor-widget.tsx](./src/browser/monitor/monitor-widget.tsx) implements the serial monitor front-end:
|
||||
- viewing the output from a connected board
|
||||
- entering data to send to the board
|
||||
|
||||
#### Config Service
|
||||
|
||||
The Config Service knows about your system, like for example the default sketch locations.
|
||||
|
||||
- [src/common/protocol/config-service.ts](./src/common/protocol/config-service.ts) implements the common classes and interfaces
|
||||
- [src/node/config-service-impl.ts](./src/node/config-service-impl.ts) implements the service backend:
|
||||
- getting the `arduino-cli` version and configuration
|
||||
- checking whether a file is in a data or sketch directory
|
||||
@@ -1,35 +1,39 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^0.4.0",
|
||||
"@grpc/grpc-js": "^0.6.12",
|
||||
"@theia/application-package": "next",
|
||||
"@theia/core": "next",
|
||||
"@theia/cpp": "next",
|
||||
"@theia/editor": "next",
|
||||
"@theia/filesystem": "next",
|
||||
"@theia/git": "next",
|
||||
"@theia/languages": "next",
|
||||
"@theia/markers": "next",
|
||||
"@theia/monaco": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@theia/navigator": "next",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/search-in-workspace": "next",
|
||||
"@theia/cpp": "next",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/which": "^1.3.1",
|
||||
"@types/react-select": "^3.0.0",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/google-protobuf": "^3.7.1",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/react-select": "^3.0.0",
|
||||
"@types/which": "^1.3.1",
|
||||
"css-element-queries": "^1.2.0",
|
||||
"react-select": "^3.0.4",
|
||||
"dateformat": "^3.0.3",
|
||||
"google-protobuf": "^3.11.0",
|
||||
"p-queue": "^5.0.0",
|
||||
"ps-tree": "^1.2.0",
|
||||
"react-select": "^3.0.4",
|
||||
"semver": "^6.3.0",
|
||||
"string-natural-compare": "^2.0.3",
|
||||
"tree-kill": "^1.2.1",
|
||||
"upath": "^1.1.2",
|
||||
@@ -43,21 +47,39 @@
|
||||
"generate-protocol": "node ./scripts/generate-protocol.js",
|
||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
"watch": "tsc -w"
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./test/**/*.test.ts\""
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
"reflect-metadata/Reflect"
|
||||
],
|
||||
"reporter": "spec",
|
||||
"colors": true,
|
||||
"watch-extensions": "ts,tsx",
|
||||
"timeout": 10000
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.7",
|
||||
"@types/chai-string": "^1.4.2",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"chai": "^4.2.0",
|
||||
"chai-string": "^1.5.0",
|
||||
"decompress": "^4.2.0",
|
||||
"decompress-targz": "^4.1.1",
|
||||
"decompress-unzip": "^4.0.1",
|
||||
"download": "^7.1.0",
|
||||
"grpc-tools": "^1.7.3",
|
||||
"grpc_tools_node_protoc_ts": "^2.5.0",
|
||||
"grpc-tools": "^1.8.0",
|
||||
"grpc_tools_node_protoc_ts": "^2.5.8",
|
||||
"mocha": "^7.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"ncp": "^2.0.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"shelljs": "^0.8.3",
|
||||
"ts-node": "^8.6.2",
|
||||
"tslint": "^5.5.0",
|
||||
"typescript": "2.9.1",
|
||||
"typescript": "3.5.3",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.1.0"
|
||||
},
|
||||
@@ -77,4 +99,4 @@
|
||||
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
|
||||
(() => {
|
||||
|
||||
const DEFAULT_VERSION = 'latest'; // require('moment')().format('YYYYMMDD');
|
||||
const DEFAULT_VERSION = '0.9.0'; // require('moment')().format('YYYYMMDD');
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const downloader = require('./downloader');
|
||||
@@ -21,7 +20,7 @@
|
||||
.option('cli-version', {
|
||||
alias: 'cv',
|
||||
default: DEFAULT_VERSION,
|
||||
describe: `The version of the 'arduino-cli' to download with the YYYYMMDD format, or 'latest'. Defaults to ${DEFAULT_VERSION}.`
|
||||
describe: `The version of the 'arduino-cli' to download, or 'nightly-latest'. Defaults to ${DEFAULT_VERSION}.`
|
||||
})
|
||||
.option('force-download', {
|
||||
alias: 'fd',
|
||||
@@ -35,7 +34,7 @@
|
||||
const { platform, arch } = process;
|
||||
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const cli = path.join(build, `arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`);
|
||||
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
|
||||
|
||||
const suffix = (() => {
|
||||
switch (platform) {
|
||||
@@ -56,7 +55,7 @@
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
|
||||
downloader.download(url, cli, 'arduino-cli', force);
|
||||
const url = `https://downloads.arduino.cc/arduino-cli${version.startsWith('nightly-') ? '/nightly' : ''}/arduino-cli_${version}_${suffix}`;
|
||||
downloader.downloadUnzipFile(url, cli, 'arduino-cli', force);
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
(() => {
|
||||
|
||||
const DEFAULT_ALS_VERSION = 'nightly';
|
||||
const DEFAULT_CLANGD_VERSION = '8.0.1';
|
||||
const DEFAULT_CLANGD_VERSION = '9.0.0';
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const downloader = require('./downloader');
|
||||
@@ -23,7 +22,7 @@
|
||||
.option('clangd-version', {
|
||||
alias: 'cv',
|
||||
default: DEFAULT_CLANGD_VERSION,
|
||||
choices: ['8.0.1'],
|
||||
choices: ['8.0.1', '9.0.0'],
|
||||
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
|
||||
})
|
||||
.option('force-download', {
|
||||
@@ -39,22 +38,25 @@
|
||||
const { platform, arch } = process;
|
||||
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const als = path.join(build, `arduino-language-server${os.platform() === 'win32' ? '.exe' : ''}`);
|
||||
const clangd = path.join(build, `clangd${os.platform() === 'win32' ? '.exe' : ''}`);
|
||||
const alsTarget = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
|
||||
|
||||
let alsSuffix, clangdSuffix;
|
||||
let clangdTarget, alsSuffix, clangdSuffix;
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
clangdTarget = path.join(build, 'bin', 'clangd')
|
||||
alsSuffix = 'Darwin_amd64.zip';
|
||||
clangdSuffix = 'macos.zip';
|
||||
break;
|
||||
case 'linux':
|
||||
clangdTarget = path.join(build, 'bin', 'clangd')
|
||||
alsSuffix = 'Linux_amd64.zip';
|
||||
clangdSuffix = 'linux.zip'
|
||||
break;
|
||||
case 'win32':
|
||||
clangdTarget = path.join(build, 'clangd.exe')
|
||||
alsSuffix = 'Windows_NT_amd64.zip';
|
||||
clangdSuffix = 'windows.zip';
|
||||
break;
|
||||
case 'linux':
|
||||
alsSuffix = 'Linux_amd64.zip';
|
||||
break;
|
||||
}
|
||||
if (!alsSuffix) {
|
||||
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
|
||||
@@ -62,11 +64,9 @@
|
||||
}
|
||||
|
||||
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${alsSuffix}`;
|
||||
downloader.download(alsUrl, als, 'arduino-language-server', force);
|
||||
downloader.downloadUnzipAll(alsUrl, build, alsTarget, force);
|
||||
|
||||
if (clangdSuffix) {
|
||||
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
|
||||
downloader.download(clangdUrl, clangd, 'clangd', force);
|
||||
}
|
||||
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
|
||||
downloader.downloadUnzipAll(clangdUrl, build, clangdTarget, force);
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -17,9 +17,13 @@ process.on('uncaughtException', error => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
exports.download = async (url, targetFile, filePrefix, force) => {
|
||||
const { platform, arch } = process;
|
||||
|
||||
/**
|
||||
* @param url {string} Download URL
|
||||
* @param targetFile {string} Path to the file to copy from the decompressed archive
|
||||
* @param filePrefix {string} Prefix of the file name found in the archive
|
||||
* @param force {boolean} Whether to download even if the target file exists
|
||||
*/
|
||||
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
@@ -65,7 +69,48 @@ exports.download = async (url, targetFile, filePrefix, force) => {
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
shell.echo(`Could not find file: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
} else {
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
}
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url {string} Download URL
|
||||
* @param targetDir {string} Directory into which to decompress the archive
|
||||
* @param targetFile {string} Path to the main file expected after decompressing
|
||||
* @param force {boolean} Whether to download even if the target file exists
|
||||
*/
|
||||
exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
if (shell.mkdir('-p', targetDir).code !== 0) {
|
||||
shell.echo('Could not create new directory.');
|
||||
shell.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
|
||||
shell.echo('>>> Decompressing...');
|
||||
const files = await decompress(data, targetDir, {
|
||||
plugins: [
|
||||
unzip(),
|
||||
untargz()
|
||||
]
|
||||
});
|
||||
if (files.length === 0) {
|
||||
shell.echo('Error ocurred while decompressing the archive.');
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Decompressing succeeded.');
|
||||
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
shell.echo(`Could not find file: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,21 @@ export namespace ArduinoCommands {
|
||||
id: 'arduino-verify',
|
||||
label: 'Verify Sketch'
|
||||
}
|
||||
export const VERIFY_TOOLBAR: Command = {
|
||||
id: 'arduino-verify-toolbar',
|
||||
}
|
||||
|
||||
export const UPLOAD: Command = {
|
||||
id: 'arduino-upload',
|
||||
label: 'Upload Sketch'
|
||||
}
|
||||
export const UPLOAD_TOOLBAR: Command = {
|
||||
id: 'arduino-upload-toolbar',
|
||||
}
|
||||
|
||||
export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
|
||||
id: "arduino-toggle-compile-for-debug"
|
||||
}
|
||||
|
||||
export const SHOW_OPEN_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-show-open-context-menu',
|
||||
@@ -42,4 +52,7 @@ export namespace ArduinoCommands {
|
||||
export const TOGGLE_ADVANCED_MODE: Command = {
|
||||
id: "arduino-toggle-advanced-mode"
|
||||
}
|
||||
export const TOGGLE_ADVANCED_MODE_TOOLBAR: Command = {
|
||||
id: "arduino-toggle-advanced-mode-toolbar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,25 @@ import { injectable, inject, postConstruct } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
|
||||
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { BoardsService } from '../common/protocol/boards-service';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { CoreService } from '../common/protocol/core-service';
|
||||
import { WorkspaceServiceExt } from './workspace-service-ext';
|
||||
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
|
||||
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
|
||||
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||
import {
|
||||
ContextMenuRenderer,
|
||||
OpenerService,
|
||||
Widget,
|
||||
StatusBar,
|
||||
ShellLayoutRestorer,
|
||||
StatusBarAlignment,
|
||||
QuickOpenService
|
||||
ContextMenuRenderer, Widget, StatusBar, StatusBarAlignment, FrontendApplicationContribution,
|
||||
FrontendApplication, KeybindingContribution, KeybindingRegistry
|
||||
} from '@theia/core/lib/browser';
|
||||
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
|
||||
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
|
||||
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
|
||||
@@ -39,11 +31,19 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { MonitorService } from '../common/protocol/monitor-service';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { ArduinoWorkspaceService } from './arduino-workspace-service';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-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 { EditorMode } from './editor-mode';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
|
||||
@@ -57,16 +57,9 @@ export namespace ArduinoToolbarContextMenu {
|
||||
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
|
||||
}
|
||||
|
||||
export namespace ArduinoAdvancedMode {
|
||||
export const LS_ID = 'arduino-advanced-mode';
|
||||
export const TOGGLED: boolean = (() => {
|
||||
const advancedModeStr = window.localStorage.getItem(LS_ID);
|
||||
return advancedModeStr === 'true';
|
||||
})();
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution {
|
||||
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
|
||||
TabBarToolbarContribution, CommandContribution, MenuContribution, KeybindingContribution, ColorContribution {
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
@@ -77,21 +70,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorService)
|
||||
protected readonly monitorService: MonitorService;
|
||||
|
||||
@inject(WorkspaceServiceExt)
|
||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||
|
||||
@inject(ToolOutputServiceClient)
|
||||
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
|
||||
|
||||
@inject(QuickPickService)
|
||||
protected readonly quickPickService: QuickPickService;
|
||||
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
@@ -110,12 +94,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
|
||||
@@ -126,28 +104,40 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commands: CommandRegistry;
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
@inject(ShellLayoutRestorer)
|
||||
protected readonly layoutRestorer: ShellLayoutRestorer;
|
||||
|
||||
@inject(QuickOpenService)
|
||||
protected readonly quickOpenService: QuickOpenService;
|
||||
|
||||
@inject(ArduinoWorkspaceService)
|
||||
protected readonly workspaceService: ArduinoWorkspaceService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
protected boardsToolbarItem: BoardsToolBarItem | null;
|
||||
protected wsSketchCount: number = 0;
|
||||
@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(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
protected application: FrontendApplication;
|
||||
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
@@ -171,121 +161,134 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.application = app;
|
||||
// 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] as Array<FrontendApplicationContribution>) {
|
||||
|
||||
if (viewContribution.initializeLayout) {
|
||||
viewContribution.initializeLayout(this.application);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.VERIFY.id,
|
||||
command: ArduinoCommands.VERIFY.id,
|
||||
command: ArduinoCommands.VERIFY_TOOLBAR.id,
|
||||
tooltip: 'Verify'
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.UPLOAD.id,
|
||||
command: ArduinoCommands.UPLOAD.id,
|
||||
command: ArduinoCommands.UPLOAD_TOOLBAR.id,
|
||||
tooltip: 'Upload'
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
|
||||
command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
|
||||
tooltip: 'Open'
|
||||
tooltip: 'Open',
|
||||
priority: 2
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.SAVE_SKETCH.id,
|
||||
command: ArduinoCommands.SAVE_SKETCH.id,
|
||||
tooltip: 'Save'
|
||||
tooltip: 'Save',
|
||||
priority: 2
|
||||
});
|
||||
registry.registerItem({
|
||||
id: BoardsToolBarItem.TOOLBAR_ID,
|
||||
render: () => <BoardsToolBarItem
|
||||
key='boardsToolbarItem'
|
||||
ref={ref => this.boardsToolbarItem = ref}
|
||||
commands={this.commands}
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceClient={this.boardsServiceClient}
|
||||
boardService={this.boardsService} />,
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left'
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
priority: 2
|
||||
});
|
||||
registry.registerItem({
|
||||
id: 'toggle-serial-monitor',
|
||||
command: MonitorViewContribution.OPEN_SERIAL_MONITOR,
|
||||
tooltip: 'Toggle Serial Monitor',
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
|
||||
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
||||
tooltip: 'Toggle Serial Monitor'
|
||||
});
|
||||
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
|
||||
command: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
|
||||
command: ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR.id,
|
||||
tooltip: 'Toggle Advanced Mode',
|
||||
text: (ArduinoAdvancedMode.TOGGLED ? '$(toggle-on)' : '$(toggle-off)'),
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
|
||||
text: (this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)')
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
// TODO: use proper API https://github.com/eclipse-theia/theia/pull/6599
|
||||
const allHandlers: { [id: string]: CommandHandler[] } = (registry as any)._handlers;
|
||||
|
||||
// Make sure to reveal the `Explorer` before executing `New File` and `New Folder`.
|
||||
for (const command of [WorkspaceCommands.NEW_FILE, WorkspaceCommands.NEW_FOLDER]) {
|
||||
const { id } = command;
|
||||
const handlers = allHandlers[id].slice();
|
||||
registry.unregisterCommand(id);
|
||||
registry.registerCommand(command);
|
||||
for (const handler of handlers) {
|
||||
const wrapper: CommandHandler = {
|
||||
execute: (...args: any[]) => {
|
||||
this.fileNavigatorContributions.openView({ reveal: true }).then(() => handler.execute(args));
|
||||
},
|
||||
isVisible: (...args: any[]) => {
|
||||
return handler.isVisible!(args);
|
||||
},
|
||||
isEnabled: (args: any[]) => {
|
||||
return handler.isEnabled!(args);
|
||||
},
|
||||
isToggled: (args: any[]) => {
|
||||
return handler.isToggled!(args);
|
||||
}
|
||||
};
|
||||
if (!handler.isEnabled) {
|
||||
delete wrapper.isEnabled;
|
||||
}
|
||||
if (!handler.isToggled) {
|
||||
delete wrapper.isToggled;
|
||||
}
|
||||
if (!handler.isVisible) {
|
||||
delete wrapper.isVisible;
|
||||
}
|
||||
registry.registerHandler(id, wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
registry.registerCommand(ArduinoCommands.VERIFY, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: widget => true,
|
||||
execute: async () => {
|
||||
const widget = this.getCurrentWidget();
|
||||
if (widget instanceof EditorWidget) {
|
||||
await widget.saveable.save();
|
||||
}
|
||||
|
||||
const uri = this.toUri(widget);
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
if (!boardsConfig.selectedBoard.fqbn) {
|
||||
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
|
||||
}
|
||||
await this.coreService.compile({ uri: uri.toString(), board: boardsConfig.selectedBoard });
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
execute: this.verify.bind(this)
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.VERIFY_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: this.verify.bind(this)
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
|
||||
execute: () => {
|
||||
this.editorMode.toggleCompileForDebug();
|
||||
this.editorMode.menuContentChanged.fire();
|
||||
},
|
||||
isToggled: () => this.editorMode.compileForDebug
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.UPLOAD, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: widget => true,
|
||||
execute: async () => {
|
||||
const widget = this.getCurrentWidget();
|
||||
if (widget instanceof EditorWidget) {
|
||||
await widget.saveable.save();
|
||||
}
|
||||
|
||||
const uri = this.toUri(widget);
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionConfig = this.monitorConnection.connectionConfig;
|
||||
await this.monitorConnection.disconnect();
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
const { selectedPort } = boardsConfig;
|
||||
if (!selectedPort) {
|
||||
throw new Error('No ports selected. Please select a port.');
|
||||
}
|
||||
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort.address });
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (connectionConfig) {
|
||||
await this.monitorConnection.connect(connectionConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
execute: this.upload.bind(this)
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.UPLOAD_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: this.upload.bind(this)
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (widget: Widget, target: EventTarget) => {
|
||||
if (this.wsSketchCount) {
|
||||
const el = (target as HTMLElement).parentElement;
|
||||
@@ -296,27 +299,30 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.commands.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
|
||||
this.commandRegistry.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR, {
|
||||
isEnabled: () => true,
|
||||
execute: () => this.doOpenFile()
|
||||
})
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
|
||||
isEnabled: () => true,
|
||||
execute: async (sketch: Sketch) => {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
|
||||
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (sketch: Sketch) => {
|
||||
registry.executeCommand(CommonCommands.SAVE_ALL.id);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, {
|
||||
execute: async uri => {
|
||||
try {
|
||||
@@ -332,6 +338,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
|
||||
isEnabled: () => true,
|
||||
execute: async () => {
|
||||
@@ -340,20 +347,94 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
this.boardsServiceClient.boardsConfig = boardsConfig;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
|
||||
execute: () => {
|
||||
const oldModeState = ArduinoAdvancedMode.TOGGLED;
|
||||
window.localStorage.setItem(ArduinoAdvancedMode.LS_ID, oldModeState ? 'false' : 'true');
|
||||
registry.executeCommand('reset.layout');
|
||||
},
|
||||
isToggled: () => this.editorMode.proMode,
|
||||
execute: () => this.editorMode.toggleProMode()
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
|
||||
isToggled: () => ArduinoAdvancedMode.TOGGLED
|
||||
})
|
||||
isToggled: () => this.editorMode.proMode,
|
||||
execute: () => this.editorMode.toggleProMode()
|
||||
});
|
||||
}
|
||||
|
||||
protected async verify() {
|
||||
const widget = this.getCurrentWidget();
|
||||
if (widget instanceof EditorWidget) {
|
||||
await widget.saveable.save();
|
||||
}
|
||||
|
||||
const uri = this.toUri(widget);
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
if (!boardsConfig.selectedBoard.fqbn) {
|
||||
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
|
||||
}
|
||||
// Reveal the Output view asynchronously (don't await it)
|
||||
this.outputContribution.openView({ reveal: true });
|
||||
await this.coreService.compile({
|
||||
uri: uri.toString(),
|
||||
board: boardsConfig.selectedBoard,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected async upload() {
|
||||
const widget = this.getCurrentWidget();
|
||||
if (widget instanceof EditorWidget) {
|
||||
await widget.saveable.save();
|
||||
}
|
||||
|
||||
const uri = this.toUri(widget);
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
const { selectedPort } = boardsConfig;
|
||||
if (!selectedPort) {
|
||||
throw new Error('No ports selected. Please select a port.');
|
||||
}
|
||||
// Reveal the Output view asynchronously (don't await it)
|
||||
this.outputContribution.openView({ reveal: true });
|
||||
await this.coreService.upload({
|
||||
uri: uri.toString(),
|
||||
board: boardsConfig.selectedBoard,
|
||||
port: selectedPort.address,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry) {
|
||||
if (!ArduinoAdvancedMode.TOGGLED) {
|
||||
if (!this.editorMode.proMode) {
|
||||
// If are not in pro-mode, we have to disable the context menu for the tabs.
|
||||
// Such as `Close`, `Close All`, etc.
|
||||
for (const command of [
|
||||
@@ -362,7 +443,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
CommonCommands.CLOSE_RIGHT_TABS,
|
||||
CommonCommands.CLOSE_ALL_TABS,
|
||||
CommonCommands.COLLAPSE_PANEL,
|
||||
CommonCommands.TOGGLE_MAXIMIZED
|
||||
CommonCommands.TOGGLE_MAXIMIZED,
|
||||
FileNavigatorCommands.REVEAL_IN_NAVIGATOR
|
||||
]) {
|
||||
registry.unregisterMenuAction(command);
|
||||
}
|
||||
@@ -370,8 +452,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
|
||||
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
|
||||
|
||||
registry.unregisterMenuAction(WorkspaceCommands.NEW_FOLDER);
|
||||
|
||||
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
|
||||
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
|
||||
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
|
||||
@@ -385,15 +465,20 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
}
|
||||
|
||||
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH, {
|
||||
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
|
||||
label: 'Optimize for Debugging',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH, {
|
||||
commandId: ArduinoCommands.VERIFY.id,
|
||||
label: 'Verify/Compile',
|
||||
order: '1'
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH, {
|
||||
commandId: ArduinoCommands.UPLOAD.id,
|
||||
label: 'Upload',
|
||||
order: '2'
|
||||
order: '3'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, {
|
||||
commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id,
|
||||
@@ -418,6 +503,17 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
return menuId;
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
keybindings.registerKeybinding({
|
||||
command: ArduinoCommands.VERIFY.id,
|
||||
keybinding: 'ctrlcmd+alt+v'
|
||||
});
|
||||
keybindings.registerKeybinding({
|
||||
command: ArduinoCommands.UPLOAD.id,
|
||||
keybinding: 'ctrlcmd+alt+u'
|
||||
});
|
||||
}
|
||||
|
||||
protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
|
||||
this.sketchService.getSketches().then(sketches => {
|
||||
this.wsSketchCount = sketches.length;
|
||||
@@ -425,8 +521,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
const command: Command = {
|
||||
id: 'openSketch' + sketch.name
|
||||
}
|
||||
this.commands.registerCommand(command, {
|
||||
execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
|
||||
this.commandRegistry.registerCommand(command, {
|
||||
execute: () => this.commandRegistry.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
|
||||
@@ -466,7 +562,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
if (destinationFile && !destinationFile.isDirectory) {
|
||||
const message = await this.validate(destinationFile);
|
||||
if (!message) {
|
||||
await this.workspaceService.open(destinationFileUri);
|
||||
this.workspaceService.open(destinationFileUri);
|
||||
return destinationFileUri;
|
||||
} else {
|
||||
this.messageService.warn(message);
|
||||
@@ -516,4 +612,34 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerColors(colors: ColorRegistry): void {
|
||||
colors.register(
|
||||
{
|
||||
id: 'arduino.branding.primary',
|
||||
defaults: {
|
||||
dark: 'statusBar.background',
|
||||
light: 'statusBar.background'
|
||||
},
|
||||
description: 'The primary branding color, such as dialog titles, library, and board manager list labels.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.branding.secondary',
|
||||
defaults: {
|
||||
dark: 'statusBar.background',
|
||||
light: 'statusBar.background'
|
||||
},
|
||||
description: 'Secondary branding color for list selections, dropdowns, and widget borders.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.foreground',
|
||||
defaults: {
|
||||
dark: 'editorWidget.background',
|
||||
light: 'editorWidget.background',
|
||||
hc: 'editorWidget.background'
|
||||
},
|
||||
description: 'Color of the Arduino Pro IDE foreground which is used for dialogs, such as the Select Board dialog.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser
|
||||
import { LanguageClientContribution } from '@theia/languages/lib/browser';
|
||||
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from './arduino-frontend-contribution';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
|
||||
@@ -26,34 +26,30 @@ import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
|
||||
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { ArduinoWorkspaceService } from './arduino-workspace-service';
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
import { ArduinoTheme } from './arduino-theme';
|
||||
import { MenuContribution } from '@theia/core';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { SilentOutlineViewContribution } from './customization/silent-outline-contribution';
|
||||
import { ArduinoOutlineViewContribution } from './customization/arduino-outline-contribution';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { SilentProblemContribution } from './customization/silent-problem-contribution';
|
||||
import { SilentNavigatorContribution } from './customization/silent-navigator-contribution';
|
||||
import { ArduinoProblemContribution } from './customization/arduino-problem-contribution';
|
||||
import { ArduinoNavigatorContribution } from './customization/arduino-navigator-contribution';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
|
||||
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||
import { ArduinoOutputToolContribution } from './customization/silent-output-tool-contribution';
|
||||
import { ArduinoOutputToolContribution } from './customization/arduino-output-tool-contribution';
|
||||
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
|
||||
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
|
||||
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
|
||||
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser';
|
||||
import { ApplicationShell, ShellLayoutRestorer, KeybindingContribution } from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import { ArduinoApplicationShell } from './customization/arduino-application-shell';
|
||||
import { ArduinoFrontendApplication } from './customization/arduino-frontend-application';
|
||||
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
|
||||
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
|
||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { SilentScmContribution } from './customization/silent-scm-contribution';
|
||||
import { ArduinoScmContribution } from './customization/arduino-scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution';
|
||||
import { ArduinoSearchInWorkspaceContribution } from './customization/arduino-search-in-workspace-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||
import { LibraryItemRenderer } from './library/library-item-renderer';
|
||||
import { BoardItemRenderer } from './boards/boards-item-renderer';
|
||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
||||
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
|
||||
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
|
||||
@@ -70,8 +66,21 @@ import { ArduinoProblemManager } from './markers/arduino-problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
|
||||
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ListItemRenderer } from './components/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
MonacoThemingService.register({
|
||||
id: 'arduinoTheme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/arduino.color-theme.json')
|
||||
});
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
|
||||
ElementQueries.listen();
|
||||
ElementQueries.init();
|
||||
@@ -81,7 +90,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
||||
bind(MenuContribution).toService(ArduinoFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
|
||||
bind(KeybindingContribution).toService(ArduinoFrontendContribution);
|
||||
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
|
||||
bind(ColorContribution).toService(ArduinoFrontendContribution);
|
||||
|
||||
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
|
||||
@@ -90,9 +101,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
|
||||
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
|
||||
|
||||
// Library list widget
|
||||
bind(LibraryListWidget).toSelf();
|
||||
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
||||
@@ -101,7 +114,6 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
createWidget: () => context.container.get(LibraryListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
|
||||
bind(LibraryItemRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
|
||||
@@ -117,6 +129,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
}).inSingletonScope();
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
|
||||
bind(BoardsServiceClient).toDynamicValue(context => {
|
||||
const client = context.container.get(BoardsServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
|
||||
@@ -135,7 +148,6 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
createWidget: () => context.container.get(BoardsListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
|
||||
bind(BoardItemRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
@@ -158,7 +170,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
}).inSingletonScope();
|
||||
|
||||
// The workspace service extension
|
||||
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt) => {
|
||||
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt: WorkspaceServiceExt) => {
|
||||
WebSocketConnectionProvider.createProxy(container, WorkspaceServiceExtPath, workspaceServiceExt);
|
||||
// Eagerly active the core, library, and boards services.
|
||||
container.get(CoreService);
|
||||
@@ -170,6 +182,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
|
||||
// Serial Monitor
|
||||
bind(MonitorModel).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||
bind(MonitorWidget).toSelf();
|
||||
bindViewContribution(bind, MonitorViewContribution);
|
||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||
@@ -193,55 +206,39 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
}).inSingletonScope();
|
||||
|
||||
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
|
||||
rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope();
|
||||
rebind(WorkspaceService).toService(ArduinoWorkspaceService);
|
||||
|
||||
const themeService = ThemeService.get();
|
||||
themeService.register(...ArduinoTheme.themes);
|
||||
|
||||
// Customizing default Theia layout
|
||||
if (!ArduinoAdvancedMode.TOGGLED) {
|
||||
unbind(OutlineViewContribution);
|
||||
bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope();
|
||||
unbind(ProblemContribution);
|
||||
bind(ProblemContribution).to(SilentProblemContribution).inSingletonScope();
|
||||
unbind(FileNavigatorContribution);
|
||||
bind(FileNavigatorContribution).to(SilentNavigatorContribution).inSingletonScope();
|
||||
unbind(OutputToolbarContribution);
|
||||
bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
|
||||
unbind(EditorContribution);
|
||||
bind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
|
||||
unbind(MonacoStatusBarContribution);
|
||||
bind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
|
||||
unbind(ApplicationShell);
|
||||
bind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
|
||||
unbind(ScmContribution);
|
||||
bind(ScmContribution).to(SilentScmContribution).inSingletonScope();
|
||||
unbind(SearchInWorkspaceFrontendContribution);
|
||||
bind(SearchInWorkspaceFrontendContribution).to(SilentSearchInWorkspaceContribution).inSingletonScope();
|
||||
} else {
|
||||
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
|
||||
document.body.classList.add(ArduinoAdvancedMode.LS_ID);
|
||||
}
|
||||
unbind(FrontendApplication);
|
||||
bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
|
||||
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
|
||||
bind(EditorMode).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(EditorMode);
|
||||
rebind(OutlineViewContribution).to(ArduinoOutlineViewContribution).inSingletonScope();
|
||||
rebind(ProblemContribution).to(ArduinoProblemContribution).inSingletonScope();
|
||||
rebind(FileNavigatorContribution).to(ArduinoNavigatorContribution).inSingletonScope();
|
||||
rebind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
|
||||
rebind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
|
||||
rebind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
|
||||
rebind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
|
||||
rebind(ScmContribution).to(ArduinoScmContribution).inSingletonScope();
|
||||
rebind(SearchInWorkspaceFrontendContribution).to(ArduinoSearchInWorkspaceContribution).inSingletonScope();
|
||||
rebind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
|
||||
|
||||
// Monaco customizations
|
||||
unbind(MonacoEditorProvider);
|
||||
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
|
||||
bind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
|
||||
rebind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
|
||||
|
||||
// Decorator customizations
|
||||
unbind(TabBarDecoratorService);
|
||||
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
|
||||
bind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
|
||||
rebind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
|
||||
|
||||
// Problem markers
|
||||
unbind(ProblemManager);
|
||||
bind(ArduinoProblemManager).toSelf().inSingletonScope();
|
||||
bind(ProblemManager).toService(ArduinoProblemManager);
|
||||
rebind(ProblemManager).toService(ArduinoProblemManager);
|
||||
|
||||
// About dialog to show the CLI version
|
||||
unbind(AboutDialog);
|
||||
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
|
||||
bind(AboutDialog).toService(ArduinoAboutDialog);
|
||||
rebind(AboutDialog).toService(ArduinoAboutDialog);
|
||||
|
||||
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
|
||||
bind(ArduinoShellLayoutRestorer).toSelf().inSingletonScope();
|
||||
rebind(ShellLayoutRestorer).toService(ArduinoShellLayoutRestorer);
|
||||
});
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
|
||||
import { Theme } from '@theia/core/lib/browser/theming';
|
||||
import { MonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
|
||||
|
||||
const ARDUINO_CSS = require('../../src/browser/style/arduino.useable.css');
|
||||
const ARDUINO_JSON = MonacoThemeRegistry.SINGLETON.register(
|
||||
require('../../src/browser/data/arduino.color-theme.json'), {}, 'arduino', 'vs').name!;
|
||||
|
||||
export class ArduinoTheme {
|
||||
|
||||
static readonly arduino: Theme = {
|
||||
id: 'arduino-theme',
|
||||
label: 'Arduino Light Theme',
|
||||
description: 'Arduino Light Theme',
|
||||
editorTheme: ARDUINO_JSON,
|
||||
activate() {
|
||||
ARDUINO_CSS.use();
|
||||
},
|
||||
deactivate() {
|
||||
ARDUINO_CSS.unuse();
|
||||
}
|
||||
};
|
||||
|
||||
static readonly themes: Theme[] = [
|
||||
ArduinoTheme.arduino
|
||||
];
|
||||
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class ArduinoWorkspaceRootResolver {
|
||||
}
|
||||
|
||||
protected isValid(uri: string): MaybePromise<boolean> {
|
||||
return this.options.isValid.bind(this)(uri);
|
||||
return this.options.isValid(uri);
|
||||
}
|
||||
|
||||
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
|
||||
@@ -59,10 +59,10 @@ export class ArduinoWorkspaceRootResolver {
|
||||
if (hash
|
||||
&& hash.length > 1
|
||||
&& hash.startsWith('#')) {
|
||||
const path = hash.slice(1); // Trim the leading `#`.
|
||||
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
|
||||
const path = hash.slice(1); // Trim the leading `#`.
|
||||
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MessageService } from '@theia/core';
|
||||
import { LabelProvider } from '@theia/core/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { SketchesService } from '../common/protocol/sketches-service';
|
||||
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
|
||||
import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
|
||||
import { EditorMode } from './editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoWorkspaceService extends WorkspaceService {
|
||||
@@ -18,25 +19,47 @@ export class ArduinoWorkspaceService extends WorkspaceService {
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
async getDefaultWorkspacePath(): Promise<string | undefined> {
|
||||
const [hash, recentWorkspaces, recentSketches] = await Promise.all([
|
||||
window.location.hash,
|
||||
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
|
||||
this.server.getRecentWorkspaces()
|
||||
]);
|
||||
const toOpen = await new ArduinoWorkspaceRootResolver({
|
||||
isValid: this.isValid.bind(this)
|
||||
}).resolve({
|
||||
hash,
|
||||
recentWorkspaces,
|
||||
recentSketches
|
||||
});
|
||||
if (toOpen) {
|
||||
const { uri } = toOpen;
|
||||
await this.server.setMostRecentlyUsedWorkspace(uri);
|
||||
return toOpen.uri;
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
private workspaceUri?: Promise<string | undefined>;
|
||||
|
||||
protected getDefaultWorkspaceUri(): Promise<string | undefined> {
|
||||
if (this.workspaceUri) {
|
||||
// Avoid creating a new sketch twice
|
||||
return this.workspaceUri;
|
||||
}
|
||||
return (await this.sketchService.createNewSketch()).uri;
|
||||
this.workspaceUri = (async () => {
|
||||
try {
|
||||
const hash = window.location.hash;
|
||||
const [recentWorkspaces, recentSketches] = await Promise.all([
|
||||
this.server.getRecentWorkspaces(),
|
||||
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
|
||||
]);
|
||||
const toOpen = await new ArduinoWorkspaceRootResolver({
|
||||
isValid: this.isValid.bind(this)
|
||||
}).resolve({ hash, recentWorkspaces, recentSketches });
|
||||
if (toOpen) {
|
||||
const { uri } = toOpen;
|
||||
await this.server.setMostRecentlyUsedWorkspace(uri);
|
||||
return toOpen.uri;
|
||||
}
|
||||
const { sketchDirUri } = (await this.configService.getConfiguration());
|
||||
this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`)
|
||||
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
|
||||
} catch (err) {
|
||||
this.logger.fatal(`Failed to determine the sketch directory: ${err}`)
|
||||
this.messageService.error(
|
||||
'There was an error creating the sketch directory. ' +
|
||||
'See the log for more details. ' +
|
||||
'The application will probably not work as expected.')
|
||||
return super.getDefaultWorkspaceUri();
|
||||
}
|
||||
})();
|
||||
return this.workspaceUri;
|
||||
}
|
||||
|
||||
private async isValid(uri: string): Promise<boolean> {
|
||||
@@ -46,7 +69,7 @@ export class ArduinoWorkspaceService extends WorkspaceService {
|
||||
}
|
||||
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
|
||||
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
|
||||
if (!ArduinoAdvancedMode.TOGGLED) {
|
||||
if (this.editorMode.proMode) {
|
||||
return true;
|
||||
}
|
||||
const sketchFolder = await this.sketchService.isSketchFolder(uri);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
|
||||
import { BoardsService, Board } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { InstallationProgressDialog } from '../components/installation-progress-dialog';
|
||||
import { InstallationProgressDialog } from '../components/progress-dialog';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
|
||||
|
||||
@@ -41,23 +41,23 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
||||
for (const candidate of candidates) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Yes', 'Install Manually').then(async answer => {
|
||||
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => {
|
||||
if (answer === 'Yes') {
|
||||
const dialog = new InstallationProgressDialog(candidate.name);
|
||||
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
|
||||
dialog.open();
|
||||
try {
|
||||
await this.boardsService.install(candidate);
|
||||
await this.boardsService.install({ item: candidate });
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
if (answer) {
|
||||
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
|
||||
return <React.Fragment>
|
||||
<div className='search'>
|
||||
<input type='search' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
|
||||
<input type='search' className='theia-input' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
|
||||
<i className='fa fa-search'></i>
|
||||
</div>
|
||||
<div className='boards list'>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { injectable } from 'inversify';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
import { BoardPackage } from '../../common/protocol/boards-service';
|
||||
|
||||
@injectable()
|
||||
export class BoardItemRenderer extends ListItemRenderer<BoardPackage> {
|
||||
|
||||
renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise<void>): React.ReactNode {
|
||||
const name = <span className='name'>{item.name}</span>;
|
||||
const author = <span className='author'>{item.author}</span>;
|
||||
const installedVersion = !!item.installedVersion && <div className='version-info'>
|
||||
<span className='version'>Version {item.installedVersion}</span>
|
||||
<span className='installed'>INSTALLED</span>
|
||||
</div>;
|
||||
|
||||
const summary = <div className='summary'>{item.summary}</div>;
|
||||
const description = <div className='summary'>{item.description}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
|
||||
const installButton = item.installable && !item.installedVersion &&
|
||||
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
|
||||
|
||||
const versions = (() => {
|
||||
const { availableVersions } = item;
|
||||
if (!!item.installedVersion || availableVersions.length === 0) {
|
||||
return undefined;
|
||||
} else if (availableVersions.length === 1) {
|
||||
return <label>{availableVersions[0]}</label>
|
||||
} else {
|
||||
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
|
||||
}
|
||||
})();
|
||||
|
||||
return <div className='component-list-item noselect'>
|
||||
<div className='header'>
|
||||
<span>{name} by {author}</span>
|
||||
{installedVersion}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{summary}
|
||||
{description}
|
||||
</div>
|
||||
<div className='info'>
|
||||
{moreInfo}
|
||||
</div>
|
||||
<div className='footer'>
|
||||
{installButton}
|
||||
{versions}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../components/component-list/list-widget';
|
||||
import { BoardItemRenderer } from './boards-item-renderer';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardPackage> {
|
||||
@@ -11,7 +11,7 @@ export class BoardsListWidget extends ListWidget<BoardPackage> {
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) {
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
|
||||
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceClientImpl implements BoardsServiceClient {
|
||||
export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected storageService: LocalStorageService;
|
||||
|
||||
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
|
||||
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
|
||||
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
|
||||
@@ -31,11 +37,11 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
|
||||
|
||||
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
|
||||
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
|
||||
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
|
||||
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.loadState();
|
||||
async onStart(): Promise<void> {
|
||||
return this.loadState();
|
||||
}
|
||||
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
@@ -87,6 +93,11 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
|
||||
this.onBoardInstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
|
||||
this.logger.info('Board uninstalled: ', JSON.stringify(event));
|
||||
this.onBoardUninstalledEmitter.fire(event);
|
||||
}
|
||||
|
||||
set boardsConfig(config: BoardsConfig.Config) {
|
||||
this.logger.info('Board config changed: ', JSON.stringify(config));
|
||||
this._boardsConfig = config;
|
||||
@@ -100,6 +111,56 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
|
||||
return this._boardsConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
|
||||
*/
|
||||
canVerify(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } {
|
||||
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn('No boards selected.', { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
|
||||
*/
|
||||
canUploadTo(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
options: { silent: boolean } = { silent: true }): config is RecursiveRequired<BoardsConfig.Config> {
|
||||
|
||||
if (!this.canVerify(config, options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { name } = config.selectedBoard;
|
||||
if (!config.selectedPort) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard.fqbn) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected saveState(): Promise<void> {
|
||||
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
|
||||
}
|
||||
@@ -108,15 +169,10 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
|
||||
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
|
||||
if (storedValidBoardsConfig) {
|
||||
this.latestValidBoardsConfig = storedValidBoardsConfig;
|
||||
if (this.canUploadTo(this.latestValidBoardsConfig)) {
|
||||
this.boardsConfig = this.latestValidBoardsConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected canVerify(config: BoardsConfig.Config | undefined): config is BoardsConfig.Config & { selectedBoard: Board } {
|
||||
return !!config && !!config.selectedBoard;
|
||||
}
|
||||
|
||||
protected canUploadTo(config: BoardsConfig.Config | undefined): config is RecursiveRequired<BoardsConfig.Config> {
|
||||
return this.canVerify(config) && !!config.selectedPort && !!config.selectedBoard.fqbn;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
});
|
||||
}
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS, {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Styles } from 'react-select/src/styles';
|
||||
import { Props } from 'react-select/src/components';
|
||||
import { ThemeConfig } from 'react-select/src/theme';
|
||||
|
||||
export class ArduinoSelect<T> extends Select<T> {
|
||||
|
||||
constructor(props: Readonly<Props<T>>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const controlHeight = 27; // from `monitor.css` -> `.serial-monitor-container .head` (`height: 27px;`)
|
||||
const styles: Styles = {
|
||||
control: styles => ({
|
||||
...styles,
|
||||
minWidth: 120,
|
||||
color: 'var(--theia-foreground)'
|
||||
}),
|
||||
dropdownIndicator: styles => ({
|
||||
...styles,
|
||||
padding: 0
|
||||
}),
|
||||
indicatorSeparator: () => ({
|
||||
display: 'none'
|
||||
}),
|
||||
indicatorsContainer: () => ({
|
||||
padding: '0px 5px'
|
||||
}),
|
||||
menu: styles => ({
|
||||
...styles,
|
||||
marginTop: 0
|
||||
})
|
||||
};
|
||||
const theme: ThemeConfig = theme => ({
|
||||
...theme,
|
||||
borderRadius: 0,
|
||||
spacing: {
|
||||
controlHeight,
|
||||
baseUnit: 2,
|
||||
menuGutter: 4
|
||||
}, colors: {
|
||||
...theme.colors,
|
||||
// `primary50`??? it's crazy but apparently, without this, we would get a light-blueish
|
||||
// color when selecting an option in the select by clicking and then not releasing the button.
|
||||
// https://react-select.com/styles#overriding-the-theme
|
||||
primary50: 'var(--theia-list-activeSelectionBackground)',
|
||||
}
|
||||
});
|
||||
const DropdownIndicator = () => <span className='fa fa-caret-down caret' />;
|
||||
return <Select
|
||||
{...this.props}
|
||||
components={{ DropdownIndicator }}
|
||||
theme={theme}
|
||||
styles={styles}
|
||||
classNamePrefix='arduino-select'
|
||||
isSearchable={false}
|
||||
/>
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +1,66 @@
|
||||
import * as React from 'react';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
export class ComponentListItem<T> extends React.Component<ComponentListItem.Props<T>> {
|
||||
export class ComponentListItem<T extends ArduinoComponent> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
|
||||
|
||||
constructor(props: ComponentListItem.Props<T>) {
|
||||
super(props);
|
||||
if (props.item.installable) {
|
||||
const version = props.item.availableVersions.filter(version => version !== props.item.installedVersion)[0];
|
||||
this.state = {
|
||||
selectedVersion: version
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async install(item: T): Promise<void> {
|
||||
await this.props.install(item);
|
||||
const toInstall = this.state.selectedVersion;
|
||||
const version = this.props.item.availableVersions.filter(version => version !== this.state.selectedVersion)[0];
|
||||
this.setState({
|
||||
selectedVersion: version
|
||||
});
|
||||
try {
|
||||
await this.props.install(item, toInstall);
|
||||
} catch {
|
||||
this.setState({
|
||||
selectedVersion: toInstall
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
await this.props.uninstall(item);
|
||||
}
|
||||
|
||||
protected onVersionChange(version: Installable.Version) {
|
||||
this.setState({ selectedVersion: version });
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { item, itemRenderer, install } = this.props;
|
||||
return itemRenderer.renderItem(item, install.bind(this));
|
||||
const { item, itemRenderer } = this.props;
|
||||
return itemRenderer.renderItem(
|
||||
Object.assign(this.state, { item }),
|
||||
this.install.bind(this),
|
||||
this.uninstall.bind(this),
|
||||
this.onVersionChange.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ComponentListItem {
|
||||
|
||||
export interface Props<T> {
|
||||
export interface Props<T extends ArduinoComponent> {
|
||||
readonly item: T;
|
||||
readonly install: (item: T) => Promise<void>;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
selectedVersion?: Installable.Version;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ComponentListItem } from './component-list-item';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
|
||||
export class ComponentList<T extends ArduinoComponent> extends React.Component<ComponentList.Props<T>> {
|
||||
|
||||
protected container?: HTMLElement;
|
||||
|
||||
@@ -29,18 +31,20 @@ export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
|
||||
key={this.props.itemLabel(item)}
|
||||
item={item}
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install} />
|
||||
install={this.props.install}
|
||||
uninstall={this.props.uninstall} />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ComponentList {
|
||||
|
||||
export interface Props<T> {
|
||||
export interface Props<T extends ArduinoComponent> {
|
||||
readonly items: T[];
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly install: (item: T) => Promise<void>;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly resolveContainer: (element: HTMLElement) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import debounce = require('lodash.debounce');
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { Searchable } from '../../../common/protocol/searchable';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { InstallationProgressDialog } from '../installation-progress-dialog';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog';
|
||||
import { SearchBar } from './search-bar';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { ComponentList } from './component-list';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
export class FilterableListContainer<T> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
|
||||
export class FilterableListContainer<T extends ArduinoComponent> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
|
||||
|
||||
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
|
||||
super(props);
|
||||
@@ -18,12 +21,18 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
componentDidMount(): void {
|
||||
this.search = debounce(this.search, 500);
|
||||
this.handleFilterTextChange('');
|
||||
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
// See: arduino/arduino-pro-ide#101
|
||||
// Resets the top of the perfect scroll-bar's thumb.
|
||||
this.props.container.updateScrollBar();
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <div className={'filterable-list-container'}>
|
||||
{this.renderSearchFilter()}
|
||||
@@ -51,6 +60,7 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
|
||||
itemLabel={itemLabel}
|
||||
itemRenderer={itemRenderer}
|
||||
install={this.install.bind(this)}
|
||||
uninstall={this.uninstall.bind(this)}
|
||||
resolveContainer={resolveContainer}
|
||||
/>
|
||||
}
|
||||
@@ -75,12 +85,34 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
|
||||
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
|
||||
}
|
||||
|
||||
protected async install(item: T): Promise<void> {
|
||||
protected async install(item: T, version: Installable.Version): Promise<void> {
|
||||
const { installable, searchable, itemLabel } = this.props;
|
||||
const dialog = new InstallationProgressDialog(itemLabel(item));
|
||||
const dialog = new InstallationProgressDialog(itemLabel(item), version);
|
||||
dialog.open();
|
||||
try {
|
||||
await installable.install(item);
|
||||
await installable.install({ item, version });
|
||||
const { items } = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
const uninstall = await new ConfirmDialog({
|
||||
title: 'Uninstall',
|
||||
msg: `Do you want to uninstall ${item.name}?`,
|
||||
ok: 'Yes',
|
||||
cancel: 'No'
|
||||
}).open();
|
||||
if (!uninstall) {
|
||||
return;
|
||||
}
|
||||
const { installable, searchable, itemLabel } = this.props;
|
||||
const dialog = new UninstallationProgressDialog(itemLabel(item));
|
||||
dialog.open();
|
||||
try {
|
||||
await installable.uninstall({ item });
|
||||
const { items } = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} finally {
|
||||
@@ -92,7 +124,8 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
|
||||
|
||||
export namespace FilterableListContainer {
|
||||
|
||||
export interface Props<T> {
|
||||
export interface Props<T extends ArduinoComponent> {
|
||||
readonly container: ListWidget<T>;
|
||||
readonly installable: Installable<T>;
|
||||
readonly searchable: Searchable<T>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ComponentListItem } from './component-list-item';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListItemRenderer<T> {
|
||||
export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
|
||||
@inject(WindowService)
|
||||
protected windowService: WindowService;
|
||||
|
||||
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
|
||||
protected onMoreInfoClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
this.windowService.openNewWindow(target.href, { external: true });
|
||||
@@ -16,6 +19,84 @@ export abstract class ListItemRenderer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
abstract renderItem(item: T, install: (item: T) => Promise<void>): React.ReactNode;
|
||||
renderItem(
|
||||
input: ComponentListItem.State & { item: T },
|
||||
install: (item: T) => Promise<void>,
|
||||
uninstall: (item: T) => Promise<void>,
|
||||
onVersionChange: (version: Installable.Version) => void
|
||||
): React.ReactNode {
|
||||
|
||||
}
|
||||
const { item } = input;
|
||||
let nameAndAuthor: JSX.Element;
|
||||
if (item.name && item.author) {
|
||||
const name = <span className='name'>{item.name}</span>;
|
||||
const author = <span className='author'>{item.author}</span>;
|
||||
nameAndAuthor = <span>{name} by {author}</span>
|
||||
} else if (item.name) {
|
||||
nameAndAuthor = <span className='name'>{item.name}</span>;
|
||||
} else if ((item as any).id) {
|
||||
nameAndAuthor = <span className='name'>{(item as any).id}</span>;
|
||||
} else {
|
||||
nameAndAuthor = <span className='name'>Unknown</span>;
|
||||
}
|
||||
const onClickUninstall = () => uninstall(item);
|
||||
const installedVersion = !!item.installedVersion && <div className='version-info'>
|
||||
<span className='version'>Version {item.installedVersion}</span>
|
||||
<span className='installed' onClick={onClickUninstall} />
|
||||
</div>;
|
||||
|
||||
const summary = <div className='summary'>{item.summary}</div>;
|
||||
const description = <div className='summary'>{item.description}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>More info</a>;
|
||||
const onClickInstall = () => install(item);
|
||||
const installButton = item.installable &&
|
||||
<button className='theia-button install' onClick={onClickInstall}>INSTALL</button>;
|
||||
|
||||
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const version = event.target.value;
|
||||
if (version) {
|
||||
onVersionChange(version);
|
||||
}
|
||||
}
|
||||
|
||||
const versions = (() => {
|
||||
const { availableVersions } = item;
|
||||
if (availableVersions.length === 0) {
|
||||
return undefined;
|
||||
} else if (availableVersions.length === 1) {
|
||||
return <label>{availableVersions[0]}</label>
|
||||
} else {
|
||||
return <select
|
||||
className='theia-select'
|
||||
value={input.selectedVersion}
|
||||
onChange={onSelectChange}>
|
||||
{
|
||||
item.availableVersions
|
||||
.filter(version => version !== item.installedVersion) // Filter the version that is currently installed.
|
||||
.map(version => <option value={version} key={version}>{version}</option>)
|
||||
}
|
||||
</select>;
|
||||
}
|
||||
})();
|
||||
|
||||
return <div className='component-list-item noselect'>
|
||||
<div className='header'>
|
||||
{nameAndAuthor}
|
||||
{installedVersion}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{summary}
|
||||
{description}
|
||||
</div>
|
||||
<div className='info'>
|
||||
{moreInfo}
|
||||
</div>
|
||||
<div className='footer'>
|
||||
{installButton}
|
||||
{versions}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListWidget } from './list-widget';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution<T> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
|
||||
export abstract class ListWidgetFrontendContribution<T extends ArduinoComponent> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { Searchable } from '../../../common/protocol/searchable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { FilterableListContainer } from './filterable-list-container';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidget<T> extends ReactWidget {
|
||||
export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget {
|
||||
|
||||
/**
|
||||
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
|
||||
@@ -61,6 +62,7 @@ export abstract class ListWidget<T> extends ReactWidget {
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <FilterableListContainer<T>
|
||||
container={this}
|
||||
resolveContainer={this.deferredContainer.resolve}
|
||||
resolveFocus={this.onFocusResolved}
|
||||
searchable={this.options.searchable}
|
||||
@@ -74,10 +76,16 @@ export abstract class ListWidget<T> extends ReactWidget {
|
||||
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
|
||||
}
|
||||
|
||||
updateScrollBar(): void {
|
||||
if (this.scrollBar) {
|
||||
this.scrollBar.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ListWidget {
|
||||
export interface Options<T> {
|
||||
export interface Options<T extends ArduinoComponent> {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly iconClass: string;
|
||||
|
||||
@@ -10,7 +10,7 @@ export class SearchBar extends React.Component<SearchBar.Props> {
|
||||
render(): React.ReactNode {
|
||||
return <input
|
||||
ref={this.setRef}
|
||||
className={SearchBar.Styles.SEARCH_BAR_CLASS}
|
||||
className={`theia-input ${SearchBar.Styles.SEARCH_BAR_CLASS}`}
|
||||
type='text'
|
||||
placeholder='Filter your search...'
|
||||
size={1}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { AbstractDialog } from '@theia/core/lib/browser';
|
||||
|
||||
export class InstallationProgressDialog extends AbstractDialog<undefined> {
|
||||
|
||||
readonly value = undefined;
|
||||
|
||||
constructor(componentName: string) {
|
||||
super({ title: 'Installation in progress' });
|
||||
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AbstractDialog } from '@theia/core/lib/browser';
|
||||
|
||||
export class InstallationProgressDialog extends AbstractDialog<undefined> {
|
||||
|
||||
readonly value = undefined;
|
||||
|
||||
constructor(componentName: string, version: string) {
|
||||
super({ title: 'Installation in progress' });
|
||||
this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UninstallationProgressDialog extends AbstractDialog<undefined> {
|
||||
|
||||
readonly value = undefined;
|
||||
|
||||
constructor(componentName: string) {
|
||||
super({ title: 'Uninstallation in progress' });
|
||||
this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +1,35 @@
|
||||
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoApplicationShell extends ApplicationShell {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
protected refreshBottomPanelToggleButton() {
|
||||
if (this.editorMode.proMode) {
|
||||
super.refreshBottomPanelToggleButton();
|
||||
}
|
||||
}
|
||||
|
||||
protected async track(widget: Widget): Promise<void> {
|
||||
const tracker = (this as any).tracker as FocusTracker<Widget>;
|
||||
tracker.add(widget);
|
||||
this.disableClose(Saveable.apply(widget));
|
||||
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
|
||||
for (const toTrack of await widget.getTrackableWidgets()) {
|
||||
tracker.add(toTrack);
|
||||
this.disableClose(Saveable.apply(toTrack));
|
||||
}
|
||||
if (widget.onDidChangeTrackableWidgets) {
|
||||
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
|
||||
if (this.editorMode.proMode) {
|
||||
super.track(widget);
|
||||
} else {
|
||||
const tracker = (this as any).tracker as FocusTracker<Widget>;
|
||||
tracker.add(widget);
|
||||
this.disableClose(Saveable.apply(widget));
|
||||
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
|
||||
for (const toTrack of await widget.getTrackableWidgets()) {
|
||||
tracker.add(toTrack);
|
||||
this.disableClose(Saveable.apply(toTrack));
|
||||
}
|
||||
if (widget.onDidChangeTrackableWidgets) {
|
||||
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FileMenuContribution } from '@theia/workspace/lib/browser';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFileMenuContribution extends FileMenuContribution {
|
||||
|
||||
registerMenus(registry: MenuModelRegistry) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import { injectable, inject } from 'inversify';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from '../arduino-frontend-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendApplication extends FrontendApplication {
|
||||
@@ -16,12 +17,15 @@ export class ArduinoFrontendApplication extends FrontendApplication {
|
||||
@inject(ArduinoFrontendContribution)
|
||||
protected readonly frontendContribution: ArduinoFrontendContribution;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
protected async initializeLayout(): Promise<void> {
|
||||
super.initializeLayout().then(() => {
|
||||
return super.initializeLayout().then(() => {
|
||||
// If not in PRO mode, we open the sketch file with all the related files.
|
||||
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
|
||||
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
|
||||
if (!ArduinoAdvancedMode.TOGGLED) {
|
||||
if (!this.editorMode.proMode) {
|
||||
this.workspaceService.roots.then(roots => {
|
||||
for (const root of roots) {
|
||||
this.fileSystem.exists(root.uri).then(exists => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoNavigatorContribution extends FileNavigatorContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoOutlineViewContribution extends OutlineViewContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { injectable } from 'inversify';
|
||||
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
super.registerToolbarItems(toolbarRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser';
|
||||
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoProblemContribution extends ProblemContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
protected setStatusBarElement(problemStat: ProblemStat): void {
|
||||
if (this.editorMode.proMode) {
|
||||
super.setStatusBarElement(problemStat);
|
||||
}
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
keybindings.registerKeybinding({
|
||||
command: this.toggleCommand.id,
|
||||
keybinding: 'ctrlcmd+alt+shift+m'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoScmContribution extends ScmContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
|
||||
if (this.editorMode.proMode) {
|
||||
super.setStatusBarEntry(id, entry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
if (this.editorMode.proMode) {
|
||||
return super.initializeLayout(app);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class SilentNavigatorContribution extends FileNavigatorContribution {
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class SilentOutlineViewContribution extends OutlineViewContribution {
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class SilentProblemContribution extends ProblemContribution {
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
}
|
||||
|
||||
protected setStatusBarElement(problemStat: ProblemStat) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { StatusBarEntry } from '@theia/core/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class SilentScmContribution extends ScmContribution {
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
}
|
||||
|
||||
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class SilentSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
|
||||
|
||||
async initializeLayout(app: FrontendApplication): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +1,115 @@
|
||||
{
|
||||
"tokenColors": [
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#D35400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "punctuation",
|
||||
"scope": [
|
||||
"punctuation.section",
|
||||
"meta.function-call.c",
|
||||
"meta.block.c",
|
||||
"meta.function.c",
|
||||
"entity.name.function.preprocessor.c",
|
||||
"meta.preprocessor.macro.c"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#434f54"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "strings",
|
||||
"scope": [
|
||||
"string.quoted.double"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
],
|
||||
"colors": {
|
||||
"editor.background": "#FFFFFF",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editor.foreground": "#434f54",
|
||||
"editorWhitespace.foreground": "#BFBFBF",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#ffcb00"
|
||||
},
|
||||
"name": "Arduino"
|
||||
}
|
||||
"tokenColors": [
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#D35400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "punctuation",
|
||||
"scope": [
|
||||
"punctuation.section",
|
||||
"meta.function-call.c",
|
||||
"meta.block.c",
|
||||
"meta.function.c",
|
||||
"entity.name.function.preprocessor.c",
|
||||
"meta.preprocessor.macro.c"
|
||||
],
|
||||
"settings": {
|
||||
"foreground": "#434f54"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "strings",
|
||||
"scope": [
|
||||
"string.quoted.double"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
],
|
||||
"colors": {
|
||||
"list.highlightForeground": "#006468",
|
||||
"list.activeSelectionBackground": "#006468",
|
||||
"editor.background": "#ffffff",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editor.foreground": "#434f54",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#ffcb00",
|
||||
"focusBorder": "#4db7bb99",
|
||||
"menubar.selectionBackground": "#ffffff",
|
||||
"menubar.selectionForeground": "#212121",
|
||||
"menu.selectionBackground": "#dae3e3",
|
||||
"menu.selectionForeground": "#212121",
|
||||
"editorGroupHeader.tabsBackground": "#f7f9f9",
|
||||
"button.background": "#4db7bb",
|
||||
"titleBar.activeBackground": "#006468",
|
||||
"titleBar.activeForeground": "#ffffff",
|
||||
"terminal.background": "#000000",
|
||||
"terminal.foreground": "#e0e0e0",
|
||||
"dropdown.border": "#ececec",
|
||||
"dropdown.background": "#ececec",
|
||||
"activityBar.background": "#ececec",
|
||||
"activityBar.foreground": "#616161",
|
||||
"statusBar.background": "#006468",
|
||||
"secondaryButton.background": "#b5c8c9",
|
||||
"secondaryButton.hoverBackground": "#dae3e3",
|
||||
"arduino.branding.primary": "#00979d",
|
||||
"arduino.branding.secondary": "#b5c8c9",
|
||||
"arduino.foreground": "#edf1f1"
|
||||
},
|
||||
"type": "light",
|
||||
"name": "Arduino"
|
||||
}
|
||||
|
||||
72
arduino-ide-extension/src/browser/editor-mode.ts
Normal file
72
arduino-ide-extension/src/browser/editor-mode.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
|
||||
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class EditorMode implements FrontendApplicationContribution {
|
||||
|
||||
readonly menuContentChanged = new Emitter<void>();
|
||||
|
||||
protected app: FrontendApplication;
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.app = app;
|
||||
if (this.proMode) {
|
||||
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
|
||||
document.body.classList.add(EditorMode.PRO_MODE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
get proMode(): boolean {
|
||||
const value = window.localStorage.getItem(EditorMode.PRO_MODE_KEY);
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
async toggleProMode(): Promise<void> {
|
||||
const oldState = this.proMode;
|
||||
const inAdvancedMode = !oldState;
|
||||
window.localStorage.setItem(EditorMode.PRO_MODE_KEY, String(inAdvancedMode));
|
||||
if (!inAdvancedMode) {
|
||||
const { shell } = this.app;
|
||||
// Close all widgets that are neither editor nor `Output` / `Boards Manager` / `Library Manager`.
|
||||
for (const area of ['left', 'right', 'bottom', 'main'] as Array<ApplicationShell.Area>) {
|
||||
shell.closeTabs(area, title => !this.isInSimpleMode(title.owner));
|
||||
}
|
||||
}
|
||||
// `storeLayout` has a sync API but the implementation is async, we store the layout manually before we reload the page.
|
||||
// See: https://github.com/eclipse-theia/theia/issues/6579
|
||||
// XXX: hack instead of injecting the `ArduinoShellLayoutRestorer` we have to retrieve it from the application to avoid DI cycle.
|
||||
const layoutRestorer = (this.app as any).layoutRestorer as ArduinoShellLayoutRestorer
|
||||
await layoutRestorer.storeLayoutAsync(this.app);
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
protected isInSimpleMode(widget: Widget): boolean {
|
||||
return widget instanceof EditorWidget
|
||||
|| widget instanceof OutputWidget
|
||||
|| widget instanceof BoardsListWidget
|
||||
|| widget instanceof LibraryListWidget;
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
const value = window.localStorage.getItem(EditorMode.COMPILE_FOR_DEBUG_KEY);
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
async toggleCompileForDebug(): Promise<void> {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(EditorMode.COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace EditorMode {
|
||||
export const PRO_MODE_KEY = 'arduino-advanced-mode';
|
||||
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
|
||||
}
|
||||
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 962 B |
@@ -2,6 +2,7 @@ import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { Board, BoardPackage } from '../../common/protocol/boards-service';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
|
||||
@@ -25,6 +26,18 @@ export class ArduinoLanguageClientContribution extends BaseLanguageClientContrib
|
||||
@postConstruct()
|
||||
protected init() {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
|
||||
const restartIfAffected = (pkg: BoardPackage) => {
|
||||
if (!this.boardConfig) {
|
||||
this.restart();
|
||||
return;
|
||||
}
|
||||
const { selectedBoard } = this.boardConfig;
|
||||
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
|
||||
this.restart();
|
||||
}
|
||||
}
|
||||
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
|
||||
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
|
||||
}
|
||||
|
||||
selectBoard(config: BoardsConfig.Config): void {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { injectable } from 'inversify';
|
||||
import { Library } from '../../common/protocol/library-service';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export class LibraryItemRenderer extends ListItemRenderer<Library> {
|
||||
|
||||
renderItem(item: Library, install: (item: Library) => Promise<void>): React.ReactNode {
|
||||
const name = <span className='name'>{item.name}</span>;
|
||||
const author = <span className='author'>by {item.author}</span>;
|
||||
const installedVersion = !!item.installedVersion && <div className='version-info'>
|
||||
<span className='version'>Version {item.installedVersion}</span>
|
||||
<span className='installed'>INSTALLED</span>
|
||||
</div>;
|
||||
|
||||
const summary = <div className='summary'>{item.summary}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
|
||||
const installButton = item.installable && !item.installedVersion &&
|
||||
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
|
||||
|
||||
const versions = (() => {
|
||||
const { availableVersions } = item;
|
||||
if (!!item.installedVersion || availableVersions.length === 0) {
|
||||
return undefined;
|
||||
} else if (availableVersions.length === 1) {
|
||||
return <label>{availableVersions[0]}</label>
|
||||
} else {
|
||||
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
|
||||
}
|
||||
})();
|
||||
|
||||
return <div className='component-list-item noselect'>
|
||||
<div className='header'>
|
||||
<span>{name} {author}</span>
|
||||
{installedVersion}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{summary}
|
||||
</div>
|
||||
<div className='info'>
|
||||
{moreInfo}
|
||||
</div>
|
||||
<div className='footer'>
|
||||
{installButton}
|
||||
{versions}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Library, LibraryService } from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../components/component-list/list-widget';
|
||||
import { LibraryItemRenderer } from './library-item-renderer';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidget extends ListWidget<Library> {
|
||||
@@ -11,7 +11,7 @@ export class LibraryListWidget extends ListWidget<Library> {
|
||||
|
||||
constructor(
|
||||
@inject(LibraryService) protected service: LibraryService,
|
||||
@inject(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) {
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
|
||||
|
||||
super({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
|
||||
@@ -14,20 +14,20 @@ export class LibraryListWidgetFrontendContribution extends AbstractViewContribut
|
||||
widgetName: LibraryListWidget.WIDGET_LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'left',
|
||||
rank: 600
|
||||
rank: 700
|
||||
},
|
||||
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||
toggleKeybinding: 'ctrlcmd+shift+l'
|
||||
});
|
||||
}
|
||||
|
||||
initializeLayout(): void {
|
||||
// NOOP
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.SKETCH, {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS, {
|
||||
commandId: this.toggleCommand.id,
|
||||
label: 'Manage Libraries...'
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { Diagnostic } from 'vscode-languageserver-types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { Marker } from '@theia/markers/lib/common/marker';
|
||||
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
|
||||
import { ConfigService } from '../../common/protocol/config-service';
|
||||
@@ -10,12 +11,18 @@ export class ArduinoProblemManager extends ProblemManager {
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
protected dataDirUri: URI | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
super.init();
|
||||
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
|
||||
this.configService.getConfiguration()
|
||||
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
|
||||
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));
|
||||
}
|
||||
|
||||
setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker<Diagnostic>[] {
|
||||
|
||||
@@ -1,53 +1,265 @@
|
||||
import { injectable, inject } from "inversify";
|
||||
import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service";
|
||||
import { Emitter, Event } from "@theia/core";
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
|
||||
@injectable()
|
||||
export class MonitorConnection {
|
||||
|
||||
@inject(MonitorModel)
|
||||
protected readonly monitorModel: MonitorModel;
|
||||
|
||||
@inject(MonitorService)
|
||||
protected readonly monitorService: MonitorService;
|
||||
|
||||
connectionId: string | undefined;
|
||||
@inject(MonitorServiceClientImpl)
|
||||
protected readonly monitorServiceClient: MonitorServiceClientImpl;
|
||||
|
||||
protected _connectionConfig: ConnectionConfig | undefined;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
|
||||
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
get connectionConfig(): ConnectionConfig | undefined {
|
||||
return this._connectionConfig;
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly applicationState: FrontendApplicationStateService;
|
||||
|
||||
protected state: MonitorConnection.State | undefined;
|
||||
/**
|
||||
* Note: The idea is to toggle this property from the UI (`Monitor` view)
|
||||
* and the boards config and the boards attachment/detachment logic can be at on place, here.
|
||||
*/
|
||||
protected _autoConnect: boolean = false;
|
||||
protected readonly onConnectionChangedEmitter = new Emitter<MonitorConnection.State | undefined>();
|
||||
/**
|
||||
* This emitter forwards all read events **iff** the connection is established.
|
||||
*/
|
||||
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
|
||||
|
||||
/**
|
||||
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
|
||||
* we adjust the reconnection delay.
|
||||
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
|
||||
*/
|
||||
protected monitorErrors: MonitorError[] = [];
|
||||
protected reconnectTimeout?: number;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
// Forward the messages from the board **iff** connected.
|
||||
this.monitorServiceClient.onRead(event => {
|
||||
if (this.connected) {
|
||||
this.onReadEmitter.fire(event);
|
||||
}
|
||||
});
|
||||
this.monitorServiceClient.onError(async error => {
|
||||
let shouldReconnect = false;
|
||||
if (this.state) {
|
||||
const { code, config } = error;
|
||||
const { board, port } = config;
|
||||
const options = { timeout: 3000 };
|
||||
switch (code) {
|
||||
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
|
||||
console.debug(`Connection was canceled by client: ${MonitorConnection.State.toString(this.state)}.`);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_BUSY: {
|
||||
this.messageService.warn(`Connection failed. Serial port is busy: ${Port.toString(port)}.`, options);
|
||||
shouldReconnect = this.autoConnect;
|
||||
this.monitorErrors.push(error);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
|
||||
this.messageService.info(`Disconnected ${Board.toString(board, { useFqbn: false })} from ${Port.toString(port)}.`, options);
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
this.messageService.error(`Unexpected error. Reconnecting ${Board.toString(board)} on port ${Port.toString(port)}.`, options);
|
||||
console.error(JSON.stringify(error));
|
||||
shouldReconnect = this.connected && this.autoConnect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const oldState = this.state;
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
if (shouldReconnect) {
|
||||
if (this.monitorErrors.length >= 10) {
|
||||
this.messageService.warn(`Failed to reconnect ${Board.toString(board, { useFqbn: false })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(port)} serial port is busy. after 10 consecutive attempts.`);
|
||||
this.monitorErrors.length = 0;
|
||||
} else {
|
||||
const attempts = (this.monitorErrors.length || 1);
|
||||
if (this.reconnectTimeout !== undefined) {
|
||||
// Clear the previous timer.
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
const timeout = attempts * 1000;
|
||||
this.messageService.warn(`Reconnecting ${Board.toString(board, { useFqbn: false })} to ${Port.toString(port)} in ${attempts} seconds...`, { timeout });
|
||||
this.reconnectTimeout = window.setTimeout(() => this.connect(oldState.config), timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
|
||||
this.boardsServiceClient.onBoardsChanged(event => {
|
||||
if (this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
|
||||
const { attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) {
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.disconnect()
|
||||
.then(() => this.connect({ board, port, baudRate }));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Handles the `baudRate` changes by reconnecting if required.
|
||||
this.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'baudRate' && this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
this.handleBoardConfigChange(boardsConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async connect(config: ConnectionConfig): Promise<string | undefined> {
|
||||
if (this.connectionId) {
|
||||
await this.disconnect();
|
||||
}
|
||||
const { connectionId } = await this.monitorService.connect(config);
|
||||
this.connectionId = connectionId;
|
||||
this._connectionConfig = config;
|
||||
|
||||
this.onConnectionChangedEmitter.fire(this.connectionId);
|
||||
|
||||
return connectionId;
|
||||
get connected(): boolean {
|
||||
return !!this.state;
|
||||
}
|
||||
|
||||
async disconnect(): Promise<boolean> {
|
||||
let result = true;
|
||||
const connections = await this.monitorService.getConnectionIds();
|
||||
if (this.connectionId && connections.findIndex(id => id === this.connectionId) >= 0) {
|
||||
console.log('>>> Disposing existing monitor connection before establishing a new one...');
|
||||
result = await this.monitorService.disconnect(this.connectionId);
|
||||
if (!result) {
|
||||
// TODO: better!!!
|
||||
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
|
||||
} else {
|
||||
console.log(`<<< Disposed ${this.connectionId} connection.`);
|
||||
this.connectionId = undefined;
|
||||
this._connectionConfig = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.connectionId);
|
||||
get monitorConfig(): MonitorConfig | undefined {
|
||||
return this.state ? this.state.config : undefined;
|
||||
}
|
||||
|
||||
get autoConnect(): boolean {
|
||||
return this._autoConnect;
|
||||
}
|
||||
|
||||
set autoConnect(value: boolean) {
|
||||
const oldValue = this._autoConnect;
|
||||
this._autoConnect = value;
|
||||
// When we enable the auto-connect, we have to connect
|
||||
if (!oldValue && value) {
|
||||
// We have to make sure the previous boards config has been restored.
|
||||
// Otherwise, we might start the auto-connection without configured boards.
|
||||
this.applicationState.reachedState('started_contributions').then(() => {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
this.handleBoardConfigChange(boardsConfig);
|
||||
});
|
||||
} else if (oldValue && !value) {
|
||||
if (this.reconnectTimeout !== undefined) {
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
this.monitorErrors.length = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
async connect(config: MonitorConfig): Promise<Status> {
|
||||
if (this.connected) {
|
||||
const disconnectStatus = await this.disconnect();
|
||||
if (!Status.isOK(disconnectStatus)) {
|
||||
return disconnectStatus;
|
||||
}
|
||||
}
|
||||
console.info(`>>> Creating serial monitor connection for ${Board.toString(config.board)} on port ${Port.toString(config.port)}...`);
|
||||
const connectStatus = await this.monitorService.connect(config);
|
||||
if (Status.isOK(connectStatus)) {
|
||||
this.state = { config };
|
||||
console.info(`<<< Serial monitor connection created for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
|
||||
}
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
return Status.isOK(connectStatus);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<Status> {
|
||||
if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy.
|
||||
return Status.OK;
|
||||
}
|
||||
console.log('>>> Disposing existing monitor connection...');
|
||||
const status = await this.monitorService.disconnect();
|
||||
if (Status.isOK(status)) {
|
||||
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`);
|
||||
} else {
|
||||
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
|
||||
}
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the data to the connected serial monitor.
|
||||
* The desired EOL is appended to `data`, you do not have to add it.
|
||||
* It is a NOOP if connected.
|
||||
*/
|
||||
async send(data: string): Promise<Status> {
|
||||
if (!this.connected) {
|
||||
return Status.NOT_CONNECTED;
|
||||
}
|
||||
return new Promise<Status>(resolve => {
|
||||
this.monitorService.send(data + this.monitorModel.lineEnding)
|
||||
.then(() => resolve(Status.OK));
|
||||
});
|
||||
}
|
||||
|
||||
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
|
||||
return this.onConnectionChangedEmitter.event;
|
||||
}
|
||||
|
||||
get onRead(): Event<MonitorReadEvent> {
|
||||
return this.onReadEmitter.event;
|
||||
}
|
||||
|
||||
protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise<void> {
|
||||
if (this.autoConnect) {
|
||||
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
|
||||
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
|
||||
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
|
||||
this.boardsService.getAvailablePorts().then(({ ports }) => {
|
||||
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
|
||||
new Promise<void>(resolve => {
|
||||
// First, disconnect if connected.
|
||||
if (this.connected) {
|
||||
this.disconnect().then(() => resolve());
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}).then(() => {
|
||||
// Then (re-)connect.
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.connect({ board, port, baudRate });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace MonitorConnection {
|
||||
|
||||
export interface State {
|
||||
readonly config: MonitorConfig;
|
||||
}
|
||||
|
||||
export namespace State {
|
||||
export function toString(state: State): string {
|
||||
const { config } = state;
|
||||
const { board, port } = config;
|
||||
return `${Board.toString(board)} ${Port.toString(port)}`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +1,119 @@
|
||||
import { injectable } from "inversify";
|
||||
import { Emitter } from "@theia/core";
|
||||
|
||||
export namespace MonitorModel {
|
||||
export interface Data {
|
||||
autoscroll: boolean,
|
||||
timestamp: boolean,
|
||||
baudRate: number,
|
||||
lineEnding: string
|
||||
}
|
||||
}
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class MonitorModel {
|
||||
export class MonitorModel implements FrontendApplicationContribution {
|
||||
|
||||
protected readonly onChangeEmitter = new Emitter<void>();
|
||||
protected static STORAGE_ID = 'arduino-monitor-model';
|
||||
|
||||
readonly onChange = this.onChangeEmitter.event;
|
||||
@inject(LocalStorageService)
|
||||
protected readonly localStorageService: LocalStorageService;
|
||||
|
||||
protected _autoscroll: boolean = true;
|
||||
protected _timestamp: boolean = false;
|
||||
baudRate: number;
|
||||
lineEnding: string = '\n';
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
protected readonly onChangeEmitter: Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>;
|
||||
protected _autoscroll: boolean;
|
||||
protected _timestamp: boolean;
|
||||
protected _baudRate: MonitorConfig.BaudRate;
|
||||
protected _lineEnding: MonitorModel.EOL;
|
||||
|
||||
constructor() {
|
||||
this._autoscroll = true;
|
||||
this._timestamp = false;
|
||||
this._baudRate = MonitorConfig.BaudRate.DEFAULT;
|
||||
this._lineEnding = MonitorModel.EOL.DEFAULT;
|
||||
this.onChangeEmitter = new Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>();
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.localStorageService.getData<MonitorModel.State>(MonitorModel.STORAGE_ID).then(state => {
|
||||
if (state) {
|
||||
this.restoreState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.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;
|
||||
}
|
||||
|
||||
toggleAutoscroll(): void {
|
||||
this._autoscroll = !this._autoscroll;
|
||||
this.onChangeEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
toggleTimestamp(): void {
|
||||
this._timestamp = !this._timestamp;
|
||||
this.onChangeEmitter.fire(undefined);
|
||||
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'timestamp', value: this._timestamp }));
|
||||
}
|
||||
|
||||
restore(model: MonitorModel.Data) {
|
||||
this._autoscroll = model.autoscroll;
|
||||
this._timestamp = model.timestamp;
|
||||
this.baudRate = model.baudRate;
|
||||
this.lineEnding = model.lineEnding;
|
||||
get baudRate(): MonitorConfig.BaudRate {
|
||||
return this._baudRate;
|
||||
}
|
||||
|
||||
store(): MonitorModel.Data {
|
||||
return {
|
||||
set baudRate(baudRate: MonitorConfig.BaudRate) {
|
||||
this._baudRate = baudRate;
|
||||
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'baudRate', value: this._baudRate }));
|
||||
}
|
||||
|
||||
get lineEnding(): MonitorModel.EOL {
|
||||
return this._lineEnding;
|
||||
}
|
||||
|
||||
set lineEnding(lineEnding: MonitorModel.EOL) {
|
||||
this._lineEnding = lineEnding;
|
||||
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'lineEnding', value: this._lineEnding }));
|
||||
}
|
||||
|
||||
protected restoreState(state: MonitorModel.State): void {
|
||||
this._autoscroll = state.autoscroll;
|
||||
this._timestamp = state.timestamp;
|
||||
this._baudRate = state.baudRate;
|
||||
this._lineEnding = state.lineEnding;
|
||||
}
|
||||
|
||||
protected async storeState(): Promise<void> {
|
||||
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
|
||||
autoscroll: this._autoscroll,
|
||||
timestamp: this._timestamp,
|
||||
baudRate: this.baudRate,
|
||||
lineEnding: this.lineEnding
|
||||
baudRate: this._baudRate,
|
||||
lineEnding: this._lineEnding
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace MonitorModel {
|
||||
|
||||
export interface State {
|
||||
autoscroll: boolean;
|
||||
timestamp: boolean;
|
||||
baudRate: MonitorConfig.BaudRate;
|
||||
lineEnding: EOL;
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ export class MonitorServiceClientImpl implements MonitorServiceClient {
|
||||
|
||||
notifyRead(event: MonitorReadEvent): void {
|
||||
this.onReadEmitter.fire(event);
|
||||
const { connectionId, data } = event;
|
||||
console.log(`Received data from ${connectionId}: ${data}`);
|
||||
const { data } = event;
|
||||
console.debug(`Received data: ${data}`);
|
||||
}
|
||||
|
||||
notifyError(error: MonitorError): void {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { MonitorWidget } from "./monitor-widget";
|
||||
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
|
||||
import { ArduinoMenus } from "../arduino-frontend-contribution";
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
|
||||
export namespace SerialMonitor {
|
||||
export namespace Commands {
|
||||
@@ -29,7 +29,8 @@ export namespace SerialMonitor {
|
||||
@injectable()
|
||||
export class MonitorViewContribution extends AbstractViewContribution<MonitorWidget> implements TabBarToolbarContribution {
|
||||
|
||||
static readonly OPEN_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
|
||||
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
|
||||
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR = MonitorWidget.ID + ':toggle-toolbar';
|
||||
|
||||
@inject(MonitorModel) protected readonly model: MonitorModel;
|
||||
|
||||
@@ -40,8 +41,8 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
defaultWidgetOptions: {
|
||||
area: 'bottom'
|
||||
},
|
||||
toggleCommandId: MonitorViewContribution.OPEN_SERIAL_MONITOR,
|
||||
toggleKeybinding: 'ctrl+shift+m'
|
||||
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
|
||||
toggleKeybinding: 'ctrlcmd+shift+m'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,18 +55,18 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
}
|
||||
}
|
||||
|
||||
async registerToolbarItems(registry: TabBarToolbarRegistry) {
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: 'monitor-autoscroll',
|
||||
render: () => this.renderAutoScrollButton(),
|
||||
isVisible: widget => widget instanceof MonitorWidget,
|
||||
onDidChange: this.model.onChange
|
||||
onDidChange: this.model.onChange as any // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
|
||||
});
|
||||
registry.registerItem({
|
||||
id: 'monitor-timestamp',
|
||||
render: () => this.renderTimestampButton(),
|
||||
isVisible: widget => widget instanceof MonitorWidget,
|
||||
onDidChange: this.model.onChange
|
||||
onDidChange: this.model.onChange as any // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
|
||||
});
|
||||
registry.registerItem({
|
||||
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
|
||||
@@ -80,7 +81,7 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
isVisible: widget => widget instanceof MonitorWidget,
|
||||
execute: widget => {
|
||||
if (widget instanceof MonitorWidget) {
|
||||
widget.clear();
|
||||
widget.clearConsole();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -89,8 +90,17 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
execute: () => this.openView({
|
||||
toggle: true,
|
||||
activate: true
|
||||
}),
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
|
||||
})
|
||||
});
|
||||
const toolbarCmd = {
|
||||
id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR
|
||||
}
|
||||
commands.registerCommand(toolbarCmd, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
|
||||
execute: () => this.openView({
|
||||
toggle: true,
|
||||
activate: true
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -106,7 +116,7 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
}
|
||||
|
||||
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
|
||||
protected async doToggleAutoScroll() {
|
||||
protected async doToggleAutoScroll(): Promise<void> {
|
||||
this.model.toggleAutoscroll();
|
||||
}
|
||||
|
||||
@@ -121,7 +131,8 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
||||
}
|
||||
|
||||
protected readonly toggleTimestamp = () => this.doToggleTimestamp();
|
||||
protected async doToggleTimestamp() {
|
||||
protected async doToggleTimestamp(): Promise<void> {
|
||||
this.model.toggleTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,285 +1,117 @@
|
||||
import { ReactWidget, Message, Widget, StatefulWidget } from "@theia/core/lib/browser";
|
||||
import { postConstruct, injectable, inject } from "inversify";
|
||||
import * as React from 'react';
|
||||
import Select, { components } from 'react-select';
|
||||
import { Styles } from "react-select/src/styles";
|
||||
import { ThemeConfig } from "react-select/src/theme";
|
||||
import { OptionsType } from "react-select/src/types";
|
||||
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
|
||||
import { MessageService } from "@theia/core";
|
||||
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
|
||||
import { MonitorConnection } from "./monitor-connection";
|
||||
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
|
||||
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
|
||||
import { BoardsConfig } from "../boards/boards-config";
|
||||
import { MonitorModel } from "./monitor-model";
|
||||
|
||||
export namespace SerialMonitorSendField {
|
||||
export interface Props {
|
||||
onSend: (text: string) => void
|
||||
}
|
||||
|
||||
export interface State {
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorSendField extends React.Component<SerialMonitorSendField.Props, SerialMonitorSendField.State> {
|
||||
|
||||
protected inputField: HTMLInputElement | null;
|
||||
|
||||
constructor(props: SerialMonitorSendField.Props) {
|
||||
super(props);
|
||||
this.state = { value: '' };
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.inputField) {
|
||||
this.inputField.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <React.Fragment>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
ref={ref => this.inputField = ref}
|
||||
type='text' id='serial-monitor-send'
|
||||
autoComplete='off'
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange} />
|
||||
<input className="btn" type="submit" value="Submit" />
|
||||
</form>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
protected handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({ value: event.target.value });
|
||||
}
|
||||
|
||||
protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
this.props.onSend(this.state.value);
|
||||
this.setState({ value: '' });
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SerialMonitorOutput {
|
||||
export interface Props {
|
||||
lines: string[];
|
||||
model: MonitorModel;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
|
||||
protected theEnd: HTMLDivElement | null;
|
||||
|
||||
render() {
|
||||
let result = '';
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
whiteSpace: 'pre',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
for (const text of this.props.lines) {
|
||||
result += text;
|
||||
}
|
||||
return <React.Fragment>
|
||||
<div style={style}>{result}</div>
|
||||
<div style={{ float: "left", clear: "both" }}
|
||||
ref={(el) => { this.theEnd = el; }}>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected scrollToBottom() {
|
||||
if (this.theEnd) {
|
||||
this.theEnd.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.model.autoscroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.model.autoscroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { postConstruct, injectable, inject } from 'inversify';
|
||||
import { OptionsType } from 'react-select/src/types';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable'
|
||||
import { ReactWidget, Message, Widget, MessageLoop } from '@theia/core/lib/browser/widgets';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { ArduinoSelect } from '../components/arduino-select';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorConnection } from './monitor-connection';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
||||
export class MonitorWidget extends ReactWidget {
|
||||
|
||||
static readonly ID = 'serial-monitor';
|
||||
|
||||
protected lines: string[];
|
||||
protected tempData: string;
|
||||
@inject(MonitorModel)
|
||||
protected readonly monitorModel: MonitorModel;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(MonitorServiceClientImpl)
|
||||
protected readonly monitorServiceClient: MonitorServiceClientImpl;
|
||||
|
||||
protected widgetHeight: number;
|
||||
|
||||
protected continuePreviousConnection: boolean;
|
||||
/**
|
||||
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
|
||||
*/
|
||||
protected focusNode: HTMLElement | undefined;
|
||||
/**
|
||||
* Guard against re-rendering the view after the close was requested.
|
||||
* See: https://github.com/eclipse-theia/theia/issues/6704
|
||||
*/
|
||||
protected closing = false;
|
||||
protected readonly clearOutputEmitter = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl,
|
||||
@inject(MonitorConnection) protected readonly connection: MonitorConnection,
|
||||
@inject(MonitorService) protected readonly monitorService: MonitorService,
|
||||
@inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl,
|
||||
@inject(MessageService) protected readonly messageService: MessageService,
|
||||
@inject(BoardsService) protected readonly boardsService: BoardsService,
|
||||
@inject(MonitorModel) protected readonly model: MonitorModel
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.id = MonitorWidget.ID;
|
||||
this.title.label = 'Serial Monitor';
|
||||
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
|
||||
|
||||
this.lines = [];
|
||||
this.tempData = '';
|
||||
|
||||
this.title.closable = true;
|
||||
this.scrollOptions = undefined;
|
||||
|
||||
this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => {
|
||||
this.tempData += data;
|
||||
if (this.tempData.endsWith('\n')) {
|
||||
if (this.model.timestamp) {
|
||||
const nu = new Date();
|
||||
const h = (100 + nu.getHours()).toString().substr(1)
|
||||
const min = (100 + nu.getMinutes()).toString().substr(1)
|
||||
const sec = (100 + nu.getSeconds()).toString().substr(1)
|
||||
const ms = (1000 + nu.getMilliseconds()).toString().substr(1);
|
||||
this.tempData = `${h}:${min}:${sec}.${ms} -> ` + this.tempData;
|
||||
}
|
||||
this.lines.push(this.tempData);
|
||||
this.tempData = '';
|
||||
this.update();
|
||||
}
|
||||
}));
|
||||
|
||||
// TODO onError
|
||||
this.toDispose.push(this.clearOutputEmitter);
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.update();
|
||||
this.toDispose.push(this.monitorConnection.onConnectionChanged(() => this.clearConsole()));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.lines = [];
|
||||
clearConsole(): void {
|
||||
this.clearOutputEmitter.fire(undefined);
|
||||
this.update();
|
||||
}
|
||||
|
||||
storeState(): MonitorModel.Data {
|
||||
return this.model.store();
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
restoreState(oldState: MonitorModel.Data): void {
|
||||
this.model.restore(oldState);
|
||||
}
|
||||
|
||||
protected onAfterAttach(msg: Message) {
|
||||
protected onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
this.clear();
|
||||
this.connect();
|
||||
this.toDisposeOnDetach.push(
|
||||
this.boardsServiceClient.onBoardsChanged(async states => {
|
||||
const currentConnectionConfig = this.connection.connectionConfig;
|
||||
const connectedBoard = states.newState.boards
|
||||
.filter(AttachedSerialBoard.is)
|
||||
.find(board => {
|
||||
const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board;
|
||||
if (AttachedSerialBoard.is(potentiallyConnected)) {
|
||||
return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (connectedBoard && currentConnectionConfig) {
|
||||
this.continuePreviousConnection = true;
|
||||
this.connection.connect(currentConnectionConfig);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.toDisposeOnDetach.push(
|
||||
this.boardsServiceClient.onBoardsConfigChanged(async boardConfig => {
|
||||
this.connect();
|
||||
})
|
||||
)
|
||||
|
||||
this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => {
|
||||
if (!this.continuePreviousConnection) {
|
||||
this.clear();
|
||||
} else {
|
||||
this.continuePreviousConnection = false;
|
||||
}
|
||||
}));
|
||||
this.monitorConnection.autoConnect = true;
|
||||
}
|
||||
|
||||
protected onBeforeDetach(msg: Message) {
|
||||
super.onBeforeDetach(msg);
|
||||
this.connection.disconnect();
|
||||
onCloseRequest(msg: Message): void {
|
||||
this.closing = true;
|
||||
this.monitorConnection.autoConnect = false;
|
||||
if (this.monitorConnection.connected) {
|
||||
this.monitorConnection.disconnect();
|
||||
}
|
||||
super.onCloseRequest(msg);
|
||||
}
|
||||
|
||||
protected onResize(msg: Widget.ResizeMessage) {
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
// TODO: `this.isAttached`
|
||||
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
|
||||
if (!this.closing && this.isAttached) {
|
||||
super.onUpdateRequest(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected onResize(msg: Widget.ResizeMessage): void {
|
||||
super.onResize(msg);
|
||||
this.widgetHeight = msg.height;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected async connect() {
|
||||
const config = await this.getConnectionConfig();
|
||||
if (config) {
|
||||
this.connection.connect(config);
|
||||
}
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
|
||||
const baudRate = this.model.baudRate;
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { selectedBoard, selectedPort } = boardsConfig;
|
||||
if (!selectedBoard) {
|
||||
this.messageService.warn('No boards selected.');
|
||||
return;
|
||||
}
|
||||
const { name } = selectedBoard;
|
||||
if (!selectedPort) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`);
|
||||
return;
|
||||
}
|
||||
const attachedBoards = await this.boardsService.getAttachedBoards();
|
||||
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
|
||||
if (!connectedBoard) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
baudRate,
|
||||
board: selectedBoard,
|
||||
port: selectedPort.address
|
||||
}
|
||||
protected onFocusResolved = (element: HTMLElement | undefined) => {
|
||||
this.focusNode = element;
|
||||
requestAnimationFrame(() => MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest));
|
||||
}
|
||||
|
||||
protected getLineEndings(): OptionsType<SelectOption> {
|
||||
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
|
||||
return [
|
||||
{
|
||||
label: 'No Line Ending',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
label: 'Newline',
|
||||
label: 'New Line',
|
||||
value: '\n'
|
||||
},
|
||||
{
|
||||
@@ -290,109 +122,212 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
||||
label: 'Both NL & CR',
|
||||
value: '\r\n'
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected getBaudRates(): OptionsType<SelectOption> {
|
||||
const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
|
||||
return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
|
||||
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
|
||||
const baudRates: Array<MonitorConfig.BaudRate> = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
|
||||
return baudRates.map(baudRate => ({ label: baudRate + ' baud', value: baudRate }));
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
const le = this.getLineEndings();
|
||||
const br = this.getBaudRates();
|
||||
const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding);
|
||||
const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate);
|
||||
return <React.Fragment>
|
||||
<div className='serial-monitor-container'>
|
||||
<div className='head'>
|
||||
<div className='send'>
|
||||
<SerialMonitorSendField onSend={this.onSend} />
|
||||
</div>
|
||||
<div className='config'>
|
||||
{this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)}
|
||||
{this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)}
|
||||
</div>
|
||||
const { baudRates, lineEndings } = this;
|
||||
const lineEnding = lineEndings.find(item => item.value === this.monitorModel.lineEnding) || lineEndings[1]; // Defaults to `\n`.
|
||||
const baudRate = baudRates.find(item => item.value === this.monitorModel.baudRate) || baudRates[4]; // Defaults to `9600`.
|
||||
return <div className='serial-monitor'>
|
||||
<div className='head'>
|
||||
<div className='send'>
|
||||
<SerialMonitorSendInput
|
||||
monitorConfig={this.monitorConnection.monitorConfig}
|
||||
resolveFocus={this.onFocusResolved}
|
||||
onSend={this.onSend} />
|
||||
</div>
|
||||
<div id='serial-monitor-output-container'>
|
||||
<SerialMonitorOutput model={this.model} lines={this.lines} />
|
||||
<div className='config'>
|
||||
<div className='select'>
|
||||
<ArduinoSelect
|
||||
maxMenuHeight={this.widgetHeight - 40}
|
||||
options={lineEndings}
|
||||
defaultValue={lineEnding}
|
||||
onChange={this.onChangeLineEnding} />
|
||||
</div>
|
||||
<div className='select'>
|
||||
<ArduinoSelect
|
||||
className='select'
|
||||
maxMenuHeight={this.widgetHeight - 40}
|
||||
options={baudRates}
|
||||
defaultValue={baudRate}
|
||||
onChange={this.onChangeBaudRate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
<div className='body'>
|
||||
<SerialMonitorOutput
|
||||
monitorModel={this.monitorModel}
|
||||
monitorConnection={this.monitorConnection}
|
||||
clearConsoleEvent={this.clearOutputEmitter.event} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected readonly onSend = (value: string) => this.doSend(value);
|
||||
protected async doSend(value: string) {
|
||||
const { connectionId } = this.connection;
|
||||
if (connectionId) {
|
||||
this.monitorService.send(connectionId, value + this.model.lineEnding);
|
||||
protected async doSend(value: string): Promise<void> {
|
||||
this.monitorConnection.send(value);
|
||||
}
|
||||
|
||||
protected readonly onChangeLineEnding = (option: SelectOption<MonitorModel.EOL>) => {
|
||||
this.monitorModel.lineEnding = option.value;
|
||||
}
|
||||
|
||||
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
|
||||
this.monitorModel.baudRate = option.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
readonly monitorConfig?: MonitorConfig;
|
||||
readonly onSend: (text: string) => void;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
}
|
||||
export interface State {
|
||||
text: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorSendInput extends React.Component<SerialMonitorSendInput.Props, SerialMonitorSendInput.State> {
|
||||
|
||||
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
||||
super(props);
|
||||
this.state = { text: '' };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSend = this.onSend.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <input
|
||||
ref={this.setRef}
|
||||
type='text'
|
||||
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
|
||||
placeholder={this.placeholder}
|
||||
value={this.state.text}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown} />
|
||||
}
|
||||
|
||||
protected get placeholder(): string {
|
||||
const { monitorConfig } = this.props;
|
||||
if (!monitorConfig) {
|
||||
return 'Not connected. Select a board and a port to connect automatically.'
|
||||
}
|
||||
const { board, port } = monitorConfig;
|
||||
return `Message (${isOSX ? '⌘' : 'Ctrl'}+Enter to send message to '${Board.toString(board, { useFqbn: false })}' on '${Port.toString(port)}')`;
|
||||
}
|
||||
|
||||
protected setRef = (element: HTMLElement | null) => {
|
||||
if (this.props.resolveFocus) {
|
||||
this.props.resolveFocus(element || undefined);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly onChangeLineEnding = (le: SelectOption) => {
|
||||
this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n';
|
||||
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
this.setState({ text: event.target.value });
|
||||
}
|
||||
|
||||
protected readonly onChangeBaudRate = async (br: SelectOption) => {
|
||||
await this.connection.disconnect();
|
||||
this.model.baudRate = typeof br.value === 'number' ? br.value : 9600;
|
||||
this.clear();
|
||||
const config = await this.getConnectionConfig();
|
||||
if (config) {
|
||||
await this.connection.connect(config);
|
||||
}
|
||||
protected onSend(): void {
|
||||
this.props.onSend(this.state.text);
|
||||
this.setState({ text: '' });
|
||||
}
|
||||
|
||||
protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
|
||||
const height = 25;
|
||||
const selectStyles: Styles = {
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
width: 200,
|
||||
border: 'none'
|
||||
}),
|
||||
dropdownIndicator: (p, s) => ({
|
||||
...p,
|
||||
padding: 0
|
||||
}),
|
||||
indicatorSeparator: (p, s) => ({
|
||||
display: 'none'
|
||||
}),
|
||||
indicatorsContainer: (p, s) => ({
|
||||
padding: '0 5px'
|
||||
}),
|
||||
menu: (p, s) => ({
|
||||
...p,
|
||||
marginTop: 0
|
||||
})
|
||||
};
|
||||
const theme: ThemeConfig = theme => ({
|
||||
...theme,
|
||||
borderRadius: 0,
|
||||
spacing: {
|
||||
controlHeight: height,
|
||||
baseUnit: 2,
|
||||
menuGutter: 4
|
||||
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
|
||||
if (keyCode) {
|
||||
const { key, meta, ctrl } = keyCode;
|
||||
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
|
||||
this.onSend();
|
||||
}
|
||||
});
|
||||
const DropdownIndicator = (
|
||||
props: React.Props<typeof components.DropdownIndicator>
|
||||
) => {
|
||||
return (
|
||||
<span className='fa fa-caret-down caret'></span>
|
||||
);
|
||||
};
|
||||
return <Select
|
||||
options={options}
|
||||
defaultValue={defaultVal}
|
||||
onChange={onChange}
|
||||
components={{ DropdownIndicator }}
|
||||
theme={theme}
|
||||
styles={selectStyles}
|
||||
maxMenuHeight={this.widgetHeight - 40}
|
||||
classNamePrefix='sms'
|
||||
className='serial-monitor-select'
|
||||
id={id}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace SerialMonitorOutput {
|
||||
export interface Props {
|
||||
readonly monitorModel: MonitorModel;
|
||||
readonly monitorConnection: MonitorConnection;
|
||||
readonly clearConsoleEvent: Event<void>;
|
||||
}
|
||||
export interface State {
|
||||
content: string;
|
||||
timestamp: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props, SerialMonitorOutput.State> {
|
||||
|
||||
/**
|
||||
* Do not touch it. It is used to be able to "follow" the serial monitor log.
|
||||
*/
|
||||
protected anchor: HTMLElement | null;
|
||||
protected toDisposeBeforeUnmount = new DisposableCollection();
|
||||
|
||||
constructor(props: Readonly<SerialMonitorOutput.Props>) {
|
||||
super(props);
|
||||
this.state = { content: '', timestamp: this.props.monitorModel.timestamp };
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <React.Fragment>
|
||||
<div style={({ whiteSpace: 'pre', fontFamily: 'monospace' })}>
|
||||
{this.state.content}
|
||||
</div>
|
||||
<div style={{ float: 'left', clear: 'both' }} ref={element => { this.anchor = element; }} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.scrollToBottom();
|
||||
let chunk = '';
|
||||
this.toDisposeBeforeUnmount.pushAll([
|
||||
this.props.monitorConnection.onRead(({ data }) => {
|
||||
chunk += data;
|
||||
const eolIndex = chunk.indexOf('\n');
|
||||
if (eolIndex !== -1) {
|
||||
const line = chunk.substring(0, eolIndex + 1);
|
||||
chunk = chunk.slice(eolIndex + 1);
|
||||
const content = `${this.state.content}${this.state.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : ''}${line}`;
|
||||
this.setState({ content });
|
||||
}
|
||||
}),
|
||||
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
|
||||
this.props.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'timestamp') {
|
||||
const { timestamp } = this.props.monitorModel;
|
||||
this.setState({ timestamp });
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
|
||||
this.toDisposeBeforeUnmount.dispose();
|
||||
}
|
||||
|
||||
protected scrollToBottom(): void {
|
||||
if (this.props.monitorModel.autoscroll && this.anchor) {
|
||||
this.anchor.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface SelectOption<T> {
|
||||
readonly label: string;
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoShellLayoutRestorer extends ShellLayoutRestorer {
|
||||
|
||||
// Workaround for https://github.com/eclipse-theia/theia/issues/6579.
|
||||
async storeLayoutAsync(app: FrontendApplication): Promise<void> {
|
||||
if (this.shouldStoreLayout) {
|
||||
try {
|
||||
this.logger.info('>>> Storing the layout...');
|
||||
const layoutData = app.shell.getLayoutData();
|
||||
const serializedLayoutData = this.deflate(layoutData);
|
||||
await this.storageService.setData(this.storageKey, serializedLayoutData);
|
||||
this.logger.info('<<< The layout has been successfully stored.');
|
||||
} catch (error) {
|
||||
await this.storageService.setData(this.storageKey, undefined);
|
||||
this.logger.error('Error during serialization of layout data', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Title, Widget } from '@phosphor/widgets';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
||||
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { ConfigService } from '../../common/protocol/config-service';
|
||||
@@ -11,12 +12,19 @@ export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
|
||||
protected dataDirUri: URI | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
super.init();
|
||||
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
|
||||
this.configService.getConfiguration()
|
||||
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
|
||||
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));
|
||||
}
|
||||
|
||||
getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
|
||||
|
||||
51
arduino-ide-extension/src/browser/style/arduino-select.css
Normal file
51
arduino-ide-extension/src/browser/style/arduino-select.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.arduino-select__control {
|
||||
border: var(--theia-sideBar-background) var(--theia-border-width) solid !important;
|
||||
background: var(--theia-sideBar-background) !important;
|
||||
}
|
||||
|
||||
.arduino-select__control:hover {
|
||||
border: var(--theia-focusBorder) var(--theia-border-width) solid !important;
|
||||
}
|
||||
|
||||
.arduino-select__control--is-focused {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.arduino-select__option--is-selected {
|
||||
background-color: var(--theia-list-activeSelectionBackground) !important;
|
||||
color: var(--theia-list-activeSelectionForeground) !important;
|
||||
border-color: var(--theia-focusBorder) !important;
|
||||
}
|
||||
|
||||
.arduino-select__option--is-focused {
|
||||
background-color: var(--theia-list-hoverBackground) !important;
|
||||
border-color: var(--theia-focusBorder) !important;
|
||||
}
|
||||
|
||||
.arduino-select__option--is-focused.arduino-select__option--is-selected {
|
||||
background-color: var(--theia-list-activeSelectionBackground) !important;
|
||||
color: var(--theia-list-activeSelectionForeground) !important;
|
||||
border-color: var(--theia-focusBorder) !important;
|
||||
}
|
||||
|
||||
.arduino-select__menu {
|
||||
background-color: var(--theia-sideBar-background) !important;
|
||||
border: 1px solid var(--theia-focusBorder) !important;
|
||||
top: auto !important; /* to align the top of the menu with the bottom of the control */
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.arduino-select__control.arduino-select__control--menu-is-open {
|
||||
border: 1px solid !important;
|
||||
border-color: var(--theia-focusBorder) !important;
|
||||
border-bottom-color: var(--theia-sideBar-background) !important; /* if the bottom border color has the same color as the background of the control, we make the border "invisible" */
|
||||
}
|
||||
|
||||
.arduino-select__value-container .arduino-select__single-value {
|
||||
color: var(--theia-descriptionForeground) !important;
|
||||
}
|
||||
|
||||
.arduino-select__menu-list {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
/*
|
||||
The following CSS variables define the main public API for styling Theia.
|
||||
These variables should be used by all extensions wherever possible. In other
|
||||
words, extensions should not define custom colors, sizes, etc. unless
|
||||
absolutely necessary. This enables users to adapt the visual theme of Theia
|
||||
by changing these variables.
|
||||
|
||||
Many variables appear in ordered sequences (0,1,2,...). For most of these
|
||||
sequences, colors go from dark to bright for dark themes, and from bright to
|
||||
dark for bright themes. A few sequences are used to create contrast against
|
||||
the others, e.g. font and accent colors, and thus their brightness goes the
|
||||
other way around.
|
||||
|
||||
Throughout Theia, we are mostly following principles from Google's Material
|
||||
Design when selecting colors. We are not, however, following all of MD as it
|
||||
is not optimized for dense, information rich UIs.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Custom Theme Colors */
|
||||
--theia-arduino-light: rgb(0, 100, 104);
|
||||
--theia-arduino-light1: rgb(23, 161, 165);
|
||||
--theia-arduino-light2: rgb(218, 226, 228);
|
||||
--theia-arduino-light3: rgb(237, 241, 242);
|
||||
--theia-arduino-terminal: rgb(0, 0, 0);
|
||||
|
||||
/* Borders: Width and color (bright to dark) */
|
||||
|
||||
--theia-border-width: 1px;
|
||||
--theia-panel-border-width: 2px;
|
||||
--theia-border-color0: var(--md-grey-100);
|
||||
--theia-border-color1: var(--md-grey-200);
|
||||
--theia-border-color2: var(--md-grey-300);
|
||||
--theia-border-color3: var(--md-grey-400);
|
||||
|
||||
|
||||
/* UI fonts: Family, size and color (dark to bright)
|
||||
---------------------------------------------------
|
||||
The UI font CSS variables are used for the typography all of the Theia
|
||||
user interface elements that are not directly user-generated content.
|
||||
*/
|
||||
|
||||
--theia-ui-font-scale-factor: 1.2;
|
||||
--theia-ui-font-size0: calc(var(--theia-ui-font-size1) / var(--theia-ui-font-scale-factor));
|
||||
--theia-ui-font-size1: 13px; /* Base font size */
|
||||
--theia-ui-font-size2: calc(var(--theia-ui-font-size1) * var(--theia-ui-font-scale-factor));
|
||||
--theia-ui-font-size3: calc(var(--theia-ui-font-size2) * var(--theia-ui-font-scale-factor));
|
||||
--theia-ui-icon-font-size: 14px; /* Ensures px perfect FontAwesome icons */
|
||||
--theia-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
|
||||
--theia-ui-font-color0: var(--md-grey-900);
|
||||
--theia-ui-font-color1: var(--md-grey-700);
|
||||
--theia-ui-font-color2: var(--md-grey-500);
|
||||
--theia-ui-font-color3: var(--md-grey-300);
|
||||
|
||||
/* Special font colors */
|
||||
|
||||
--theia-ui-icon-font-color: #ffffff;
|
||||
--theia-ui-bar-font-color0: var(--theia-ui-font-color0);
|
||||
--theia-ui-bar-font-color1: var(--theia-inverse-ui-font-color0); /* var(--theia-ui-font-color1); */
|
||||
|
||||
/* Use the inverse UI colors against the brand/accent/warn/error colors. */
|
||||
|
||||
--theia-inverse-ui-font-color0: rgba(255, 255, 255, 1.0);
|
||||
--theia-inverse-ui-font-color2: rgba(255, 255, 255, 0.7);
|
||||
--theia-inverse-ui-font-color3: rgba(255, 255, 255, 0.5);
|
||||
|
||||
/* Content fonts: Family, size and color (dark to bright)
|
||||
Content font variables are used for typography of user-generated content.
|
||||
*/
|
||||
|
||||
--theia-content-font-size: 13px;
|
||||
--theia-content-line-height: 1.5;
|
||||
--theia-content-font-color0: black;
|
||||
--theia-content-font-color1: black;
|
||||
--theia-content-font-color2: var(--md-grey-700);
|
||||
--theia-content-font-color3: var(--md-grey-500);
|
||||
--theia-code-font-size: 13px;
|
||||
--theia-code-line-height: 17px;
|
||||
--theia-code-padding: 5px;
|
||||
--theia-code-font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
|
||||
--theia-terminal-font-family: monospace;
|
||||
--theia-ui-padding: 6px;
|
||||
|
||||
/* Tab Icon Colors */
|
||||
--theia-tab-icon-color: var(--theia-ui-font-color1);
|
||||
--theia-tab-font-color: #000;
|
||||
|
||||
/* Main layout colors (bright to dark)
|
||||
------------------------------------ */
|
||||
|
||||
--theia-layout-color0: #ffffff;
|
||||
--theia-layout-color1: #f7f9f9;
|
||||
--theia-layout-color2: #ececec;
|
||||
--theia-layout-color3: var(--theia-arduino-light2);
|
||||
--theia-layout-color4: #dcdcdc;
|
||||
|
||||
/* Brand colors */
|
||||
|
||||
--theia-brand-color0: var(--theia-arduino-light);
|
||||
--theia-brand-color1: var(--theia-arduino-light1);
|
||||
--theia-brand-color2: var(--theia-arduino-light2);
|
||||
--theia-brand-color3: var(--theia-arduino-light3);
|
||||
|
||||
/* Secondary Brand colors */
|
||||
|
||||
--theia-secondary-brand-color0: var(--md-grey-700);
|
||||
--theia-secondary-brand-color1: #b5c8c9;
|
||||
--theia-secondary-brand-color2: var(--theia-arduino-light2);
|
||||
--theia-secondary-brand-color3: var(--theia-arduino-light3);
|
||||
|
||||
/* Accent colors (dark to bright): Use these to create contrast to layout colors. */
|
||||
|
||||
--theia-accent-color0: var(--theia-arduino-light);
|
||||
--theia-accent-color1: rgb(77, 183, 187, 1.0);
|
||||
--theia-accent-color2: rgb(77, 183, 187, 0.8);
|
||||
--theia-accent-color3: rgb(77, 183, 187, 0.6);
|
||||
--theia-accent-color4: rgba(77, 183, 187, 0.4);
|
||||
|
||||
/* Accent colors with opacity */
|
||||
|
||||
--theia-transparent-accent-color0: hsla(210, 63%, 46%, 0.3);
|
||||
--theia-transparent-accent-color1: hsla(207, 66%, 56%, 0.3);
|
||||
--theia-transparent-accent-color2: hsla(207, 64%, 85%, 0.3);
|
||||
--theia-transparent-accent-color3: hsla(205, 70%, 91%, 0.3);
|
||||
|
||||
/* State colors (warn, error, success, info)
|
||||
------------------------------------------ */
|
||||
--theia-warn-color0: var(--md-amber-500);
|
||||
--theia-warn-color1: var(--md-amber-400);
|
||||
--theia-warn-color2: var(--md-amber-300);
|
||||
--theia-warn-color3: var(--md-amber-200);
|
||||
|
||||
--theia-warn-font-color0: var(--md-grey-900);
|
||||
|
||||
--theia-error-color0: var(--md-red-400);
|
||||
--theia-error-color1: #da5b4a;
|
||||
--theia-error-color2: var(--md-red-200);
|
||||
--theia-error-color3: var(--md-red-100);
|
||||
|
||||
--theia-error-font-color0: var(--md-grey-300);
|
||||
|
||||
--theia-success-color0: var(--md-green-500);
|
||||
--theia-success-color1: var(--md-green-300);
|
||||
--theia-success-color2: var(--md-green-100);
|
||||
--theia-success-color3: var(--md-green-50);
|
||||
|
||||
--theia-success-font-color0: var(--md-grey-300);
|
||||
|
||||
--theia-info-color0: var(--md-cyan-700);
|
||||
--theia-info-color1: var(--md-cyan-500);
|
||||
--theia-info-color2: var(--md-cyan-300);
|
||||
--theia-info-color3: var(--md-cyan-100);
|
||||
|
||||
--theia-info-font-color0: var(--md-grey-300);
|
||||
|
||||
--theia-disabled-color0: var(--md-grey-500);
|
||||
--theia-disabled-color1: var(--md-grey-300);
|
||||
--theia-disabled-color2: var(--md-grey-200);
|
||||
--theia-disabled-color3: var(--md-grey-50);
|
||||
|
||||
--theia-added-color0: rgba(0, 255, 0, 0.8);
|
||||
--theia-removed-color0: rgba(230, 0, 0, 0.8);
|
||||
--theia-modified-color0: rgba(0, 100, 150, 0.8);
|
||||
|
||||
/* Background for selected text */
|
||||
--theia-selected-text-background: var(--theia-accent-color3);
|
||||
|
||||
/* Colors to highlight words in widgets like tree or editors */
|
||||
|
||||
--theia-word-highlight-color0: rgba(168, 172, 148, 0.7);
|
||||
--theia-word-highlight-color1: rgba(253, 255, 0, 0.2);
|
||||
--theia-word-highlight-match-color0: rgba(234, 92, 0, 0.33);
|
||||
--theia-word-highlight-match-color1: rgba(234, 92, 0, 0.5);
|
||||
--theia-word-highlight-replace-color0: rgba(155, 185, 85, 0.2);
|
||||
|
||||
/* Scroll-bars */
|
||||
|
||||
--theia-scrollbar-width: 10px;
|
||||
--theia-scrollbar-rail-width: 10px;
|
||||
--theia-scrollbar-thumb-color: hsla(0, 0%, 61%, .4);
|
||||
--theia-scrollbar-rail-color: transparent;
|
||||
--theia-scrollbar-active-thumb-color: hsla(0, 0%, 39%, .4);
|
||||
--theia-scrollbar-active-rail-color: transparent;
|
||||
/* Menu */
|
||||
--theia-menu-color0: var(--theia-arduino-light);
|
||||
--theia-menu-color1: var(--theia-layout-color0);
|
||||
--theia-menu-color2: #dae3e3;
|
||||
/* Statusbar */
|
||||
--theia-statusbar-color: var(--theia-arduino-light);
|
||||
--theia-statusBar-font-color: var(--theia-inverse-ui-font-color0);
|
||||
--theia-statusBar-font-size: 12px;
|
||||
/* Buttons */
|
||||
--theia-ui-button-color: var(--theia-accent-color1);
|
||||
--theia-ui-button-color-hover: var(--theia-accent-color2);
|
||||
--theia-ui-button-font-color: var(--theia-arduino-light);
|
||||
--theia-ui-button-color-secondary: var(--theia-secondary-brand-color1);
|
||||
--theia-ui-button-color-secondary-hover: var(--theia-menu-color2);
|
||||
--theia-ui-button-font-color-secondary: var(--theia-inverse-ui-font-color0);
|
||||
--theia-ui-button-color-disabled: var(--theia-accent-color3);
|
||||
--theia-ui-button-font-color-disabled: var(--theia-ui-font-color2);
|
||||
--theia-ui-button-color-secondary-disabled: var(--theia-disabled-color1);
|
||||
--theia-ui-button-font-color-secondary-disabled: var(--theia-ui-font-color2);
|
||||
/* Expand/collapse element */
|
||||
--theia-ui-expand-button-color: var(--theia-accent-color4);
|
||||
--theia-ui-expand-button-font-color: var(--theia-ui-font-color1);
|
||||
/* Dialogs */
|
||||
--theia-ui-dialog-header-color: var(--theia-arduino-light);
|
||||
--theia-ui-dialog-header-font-color: var(--theia-inverse-ui-font-color0);
|
||||
--theia-ui-dialog-color: rgb(236, 241, 241);
|
||||
--theia-ui-dialog-font-color: black;
|
||||
/* Variables */
|
||||
--theia-variable-name-color: #9B46B0;
|
||||
--theia-variable-value-color: rgba(108, 108, 108, 0.8);
|
||||
--theia-number-variable-color: #09885A;
|
||||
--theia-boolean-variable-color: #0000FF;
|
||||
--theia-string-variable-color: #A31515;
|
||||
/* ANSI color */
|
||||
--theia-ansi-black-color: gray;
|
||||
--theia-ansi-red-color: #BE1717;
|
||||
--theia-ansi-green-color: #338A2F;
|
||||
--theia-ansi-yellow-color: #BEB817;
|
||||
--theia-ansi-blue-color: darkblue;
|
||||
--theia-ansi-magenta-color: darkmagenta;
|
||||
--theia-ansi-cyan-color: darkcyan;
|
||||
--theia-ansi-white-color: #BDBDBD;
|
||||
/* ANSI background color */
|
||||
--theia-ansi-black-background-color: gray;
|
||||
--theia-ansi-red-background-color: #BE1717;
|
||||
--theia-ansi-green-background-color: #338A2F;
|
||||
--theia-ansi-yellow-background-color: #BEB817;
|
||||
--theia-ansi-blue-background-color: darkblue;
|
||||
--theia-ansi-magenta-background-color: darkmagenta;
|
||||
--theia-ansi-cyan-background-color: darkcyan;
|
||||
--theia-ansi-white-background-color: #BDBDBD;
|
||||
|
||||
/* Output */
|
||||
--theia-output-font-color: var(--theia-ui-font-color3);
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ div.dialogContent.select-board-dialog > div.head .title {
|
||||
font-weight: 400;
|
||||
letter-spacing: .02em;
|
||||
font-size: 1.2em;
|
||||
color: #00979d;
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
margin: 17px 0;
|
||||
}
|
||||
|
||||
@@ -24,18 +24,18 @@ div#select-board-dialog .selectBoardContainer .head .text {
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer .body .list .item.selected {
|
||||
background: var(--theia-ui-button-color-secondary-hover);
|
||||
background: var(--theia-secondaryButton-hoverBackground);
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
|
||||
color: var(--theia-arduino-light);
|
||||
div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .search,
|
||||
#select-board-dialog .selectBoardContainer .search input,
|
||||
#select-board-dialog .selectBoardContainer .list,
|
||||
#select-board-dialog .selectBoardContainer .list {
|
||||
background: white; /* TODO find a theia color instead! */
|
||||
background: var(--theia-editor-background);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .search input {
|
||||
@@ -47,7 +47,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
display: flex;
|
||||
color: var(--theia-content-font-color0);
|
||||
color: var(--theia-editor-foreground);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .search input:focus {
|
||||
@@ -79,7 +79,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .container .content .loading {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
color: #7f8c8d;
|
||||
color: var(--theia-arduino-branding-secondary);
|
||||
padding: 10px 5px 10px 10px;
|
||||
text-transform: uppercase;
|
||||
/* The max, min-height comes from `.body .list` 265px + 47px top padding - 2 * 10px top padding */
|
||||
@@ -99,7 +99,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .detail {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
color: var(--theia-disabled-color0);
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
width: 155px; /* used heuristics for the calculation */
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
@@ -107,11 +107,11 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item.missing {
|
||||
color: var(--theia-disabled-color0);
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item:hover {
|
||||
background: var(--theia-ui-button-color-secondary-hover);
|
||||
background: var(--theia-secondaryButton-hoverBackground);
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list {
|
||||
@@ -140,15 +140,12 @@ button.theia-button {
|
||||
}
|
||||
|
||||
button.theia-button.secondary {
|
||||
background-color: #b5c8c9;
|
||||
color: #000;
|
||||
box-shadow: 0 4px #95a5a6;
|
||||
background-color: var(--theia-secondaryButton-background);
|
||||
color: var(--theia-foreground);
|
||||
}
|
||||
|
||||
button.theia-button.main {
|
||||
color: #fff;
|
||||
/* background-color: #00979c; */
|
||||
box-shadow: 0 4px var(--theia-accent-color0);
|
||||
color: var(--theia-button-foreground);
|
||||
}
|
||||
|
||||
.dialogControl {
|
||||
@@ -192,15 +189,17 @@ button.theia-button.main {
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item {
|
||||
background: var(--theia-layout-color1);
|
||||
background: var(--theia-tab-unfocusedActiveBackground);
|
||||
color: var(--theia-foreground);
|
||||
height: 22px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0px 3px 0px 3px;
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-list {
|
||||
border: 3px solid var(--theia-border-color2);
|
||||
border: 3px solid var(--theia-activityBar-background);
|
||||
margin: -3px;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -210,16 +209,16 @@ button.theia-button.main {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: var(--theia-ui-font-color1);
|
||||
background: var(--theia-layout-color1);
|
||||
color: var(--theia-foreground);
|
||||
background: var(--theia-tab-unfocusedActiveBackground);
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item .fa-check {
|
||||
color: var(--theia-accent-color1);
|
||||
color: var(--theia-arduino-branding-primary);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.arduino-boards-dropdown-item.selected,
|
||||
.arduino-boards-dropdown-item:hover {
|
||||
background: var(--theia-layout-color3);
|
||||
background: var(--theia-list-hoverBackground);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
}
|
||||
|
||||
.p-MenuBar-item.p-mod-active {
|
||||
color: var(--theia-ui-bar-font-color0);
|
||||
color: var(--theia-menubar-selectionForeground);
|
||||
}
|
||||
@@ -2,4 +2,34 @@
|
||||
@import './board-select-dialog.css';
|
||||
@import './main.css';
|
||||
@import './editor.css';
|
||||
@import './monitor.css';
|
||||
@import './monitor.css';
|
||||
@import './arduino-select.css';
|
||||
|
||||
.theia-input.warning:focus {
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
outline-offset: -1px;
|
||||
opacity: 1 !important;
|
||||
color: var(--theia-warningForeground);
|
||||
background-color: var(--theia-warningBackground);
|
||||
}
|
||||
|
||||
.theia-input.warning {
|
||||
background-color: var(--theia-warningBackground);
|
||||
}
|
||||
|
||||
.theia-input.warning::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: var(--theia-warningForeground);
|
||||
background-color: var(--theia-warningBackground);
|
||||
opacity: 1; /* Firefox */
|
||||
}
|
||||
|
||||
.theia-input.warning:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||
color: var(--theia-warningForeground);
|
||||
background-color: var(--theia-warningBackground);
|
||||
}
|
||||
|
||||
.theia-input.warning::-ms-input-placeholder { /* Microsoft Edge */
|
||||
color: var(--theia-warningForeground);
|
||||
background-color: var(--theia-warningBackground);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
.library-tab-icon {
|
||||
-webkit-mask: url('library-tab-icon.svg');
|
||||
mask: url('library-tab-icon.svg');
|
||||
-webkit-mask: url('../icons/library-tab-icon.svg');
|
||||
mask: url('../icons/library-tab-icon.svg');
|
||||
}
|
||||
|
||||
.arduino-list-widget {
|
||||
color: var(--theia-ui-font-color1);
|
||||
color: var(--theia-foreground);
|
||||
}
|
||||
|
||||
.arduino-list-widget .search-bar {
|
||||
margin: 0px 10px 10px 15px;
|
||||
border-color: var(--theia-border-color3);
|
||||
}
|
||||
|
||||
.arduino-list-widget .search-filters {
|
||||
margin: 0px 10px 0px 15px;
|
||||
border-color: var(--theia-border-color3);
|
||||
}
|
||||
|
||||
.arduino-list-widget .search-bar:focus {
|
||||
border-color: var(--theia-accent-color3);
|
||||
}
|
||||
|
||||
.arduino-list-widget .filterable-list-container .search-filters .filter {
|
||||
margin: 0px 0px 10px 0px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.arduino-list-widget .filterable-list-container .search-filters .filter .title {
|
||||
margin: 0px 10px 0px 0px;
|
||||
align-self: center;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--theia-ui-font-size0);
|
||||
}
|
||||
|
||||
.arduino-list-widget .filterable-list-container .search-filters .filter > select {
|
||||
width: 100%;
|
||||
border-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.filterable-list-container {
|
||||
@@ -50,15 +28,26 @@
|
||||
}
|
||||
|
||||
.filterable-list-container .items-container > div:nth-child(odd) {
|
||||
background-color: var(--theia-layout-color0);
|
||||
background-color: var(--theia-sideBar-background);
|
||||
filter: contrast(105%);
|
||||
}
|
||||
|
||||
.filterable-list-container .items-container > div:nth-child(even) {
|
||||
background-color: var(--theia-layout-color1);
|
||||
background-color: var(--theia-sideBar-background);
|
||||
filter: contrast(95%);
|
||||
}
|
||||
|
||||
.filterable-list-container .items-container > div:hover {
|
||||
background-color: var(--theia-layout-color2);
|
||||
background-color: var(--theia-sideBar-background);
|
||||
filter: contrast(90%);
|
||||
}
|
||||
|
||||
/* Perfect scrollbar does not like if we explicitly set the `background-color` of the contained elements.
|
||||
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
|
||||
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
|
||||
https://github.com/arduino/arduino-pro-ide/issues/82 */
|
||||
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.component-list-item {
|
||||
@@ -67,18 +56,9 @@
|
||||
}
|
||||
|
||||
.component-list-item:hover {
|
||||
background: var(--theia-accent-color4);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.component-list-item:hover .meta-info {
|
||||
color: var(--theia-ui-font-color1);
|
||||
}
|
||||
|
||||
.component-list-item .meta-info {
|
||||
color: var(--theia-ui-font-color3);
|
||||
}
|
||||
|
||||
.component-list-item .header {
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
@@ -97,26 +77,38 @@
|
||||
|
||||
.component-list-item .header .author {
|
||||
font-weight: bold;
|
||||
color: var(--theia-ui-font-color2);
|
||||
color: var(--theia-panelTitle-inactiveForeground);
|
||||
}
|
||||
|
||||
.component-list-item:hover .header .author {
|
||||
color: var(--theia-ui-font-color1);
|
||||
color: var(--theia-foreground);
|
||||
}
|
||||
|
||||
.component-list-item .header .version {
|
||||
color: var(--theia-ui-font-color2);
|
||||
color: var(--theia-panelTitle-inactiveForeground);
|
||||
}
|
||||
|
||||
.component-list-item .header .installed {
|
||||
.component-list-item .footer .theia-button.install {
|
||||
height: auto; /* resets the default Theia button height in the filterable list widget */
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:before {
|
||||
margin-left: 4px;
|
||||
display: inline-block;
|
||||
justify-self: end;
|
||||
background-color: var(--theia-accent-color1);
|
||||
background-color: var(--theia-button-background);
|
||||
padding: 2px 4px 2px 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
max-height: calc(1em + 4px);
|
||||
color: var(--theia-inverse-ui-font-color0);
|
||||
color: var(--theia-button-foreground);
|
||||
content: 'INSTALLED';
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:hover:before {
|
||||
background-color: var(--theia-button-foreground);
|
||||
color: var(--theia-button-background);
|
||||
content: 'UNINSTALL';
|
||||
}
|
||||
|
||||
.component-list-item[min-width~="170px"] .footer {
|
||||
@@ -146,14 +138,10 @@
|
||||
}
|
||||
|
||||
.component-list-item .info a {
|
||||
color: var(--theia-brand-color1);
|
||||
color: var(--theia-button-background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.component-list-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.component-list-item strong.installed {
|
||||
color: rgb(0, 151, 157)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
.p-TabBar[data-orientation='horizontal'].theia-app-bottom {
|
||||
background: var(--theia-layout-color1);
|
||||
#theia-bottom-content-panel .p-TabBar[data-orientation='horizontal'].theia-app-bottom {
|
||||
background: var(--theia-editorGroupHeader-tabsBackground);
|
||||
}
|
||||
|
||||
.theia-output {
|
||||
background: var(--theia-arduino-terminal);
|
||||
background: var(--theia-terminal-background);
|
||||
color: var(--theia-terminal-foreground);
|
||||
}
|
||||
|
||||
#outputView {
|
||||
@@ -20,11 +21,11 @@
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: var(--theia-ui-button-color);
|
||||
background: var(--theia-button-background);
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-tool-item > div:hover {
|
||||
background: var(--theia-ui-button-color-hover);
|
||||
background: var(--theia-button-hoverBackground);
|
||||
}
|
||||
|
||||
.arduino-verify, .arduino-upload {
|
||||
@@ -34,7 +35,7 @@
|
||||
.arduino-tool-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: var(--theia-ui-button-font-color);
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
-webkit-mask: url(../icons/mask-buttons.svg);
|
||||
mask: url(../icons/mask-buttons.svg);
|
||||
-webkit-mask-size: 800%;
|
||||
@@ -76,6 +77,14 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#theia-top-panel .p-TabBar-toolbar {
|
||||
padding-left: 4px !important; /* moves the `verify`, upload and other toolbar items to the left */
|
||||
}
|
||||
|
||||
.p-Widget .p-MenuBar {
|
||||
padding-left: 1px !important; /* moves the menubar: `File`, `Edit`, etc to the left */
|
||||
}
|
||||
|
||||
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.right {
|
||||
justify-content: flex-start;
|
||||
min-width: 190px;
|
||||
@@ -86,29 +95,11 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.arduino-tool-item.item.connected-boards {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.arduino-tool-item.item.connected-boards select {
|
||||
line-height: var(--theia-content-line-height);
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
color: var(--theia-ui-font-color1);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: linear-gradient(45deg, transparent 50%, var(--theia-ui-font-color1) 50%), linear-gradient(135deg, var(--theia-ui-font-color1) 50%, transparent 50%);
|
||||
background-position: calc(100% - 6px) 8px, calc(100% - 2px) 8px, 100% 0;
|
||||
background-size: 4px 5px;
|
||||
background-repeat: no-repeat;
|
||||
padding-left: 3px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.arduino-toolbar-tooltip {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--theia-ui-font-color3);
|
||||
color: var(--theia-titleBar-activeForeground);
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item > div.arduino-toggle-advanced-mode {
|
||||
@@ -126,11 +117,11 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theia-ui-button-font-color);
|
||||
color: var(--theia-titleBar-activeBackground);;
|
||||
}
|
||||
|
||||
.monaco-editor .margin {
|
||||
border-right: 2px solid var(--theia-border-color1);
|
||||
border-right: 2px solid var(--theia-sideBar-background);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -142,4 +133,8 @@
|
||||
|
||||
.theia-sidepanel-toolbar .theia-sidepanel-title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock {
|
||||
background-color: var(--theia-arduino-foreground);
|
||||
}
|
||||
|
||||
@@ -5,63 +5,45 @@
|
||||
background-position-x: 19px;
|
||||
}
|
||||
|
||||
.serial-monitor-container {
|
||||
.serial-monitor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head {
|
||||
.serial-monitor .head {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
background: var(--theia-layout-color0);
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .send {
|
||||
.serial-monitor .head .send {
|
||||
display: flex;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .send .btn {
|
||||
display: flex;
|
||||
padding: 0 5px;
|
||||
align-items: center;
|
||||
color: var(--theia-ui-font-color1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.serial-monitor-container .head .send .btn:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .send form {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
background: var(--theia-layout-color2);
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .send input#serial-monitor-send {
|
||||
color: var(--theia-ui-font-color1);
|
||||
flex: 1;
|
||||
.serial-monitor .head .send > input {
|
||||
line-height: var(--theia-content-line-height);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .send input:focus {
|
||||
outline: none;
|
||||
.serial-monitor .head .send > input:focus {
|
||||
border-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .config {
|
||||
.serial-monitor .head .config {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.serial-monitor-container .head .config .serial-monitor-select {
|
||||
margin-left: 5px;
|
||||
.serial-monitor .head .config .select {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
#serial-monitor-output-container {
|
||||
.serial-monitor .body {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 6px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-monitor {
|
||||
@@ -72,55 +54,10 @@
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-monitor.toggled {
|
||||
background: var(--theia-secondary-brand-color1);
|
||||
background: var(--theia-sideBarSectionHeader-background);
|
||||
filter: contrast(80%);
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item .clear-all {
|
||||
background: var(--theia-icon-clear) no-repeat;
|
||||
}
|
||||
|
||||
/* React Select Styles */
|
||||
.serial-monitor-select .sms__control {
|
||||
border: var(--theia-border-color1) var(--theia-border-width) solid;
|
||||
background: var(--theia-layout-color2);
|
||||
}
|
||||
|
||||
.serial-monitor-select .sms__control--is-focused {
|
||||
border-color: var(--theia-border-color2) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.sms__control--is-focused:hover {
|
||||
border-color: var(--theia-border-color2) !important;
|
||||
}
|
||||
|
||||
.serial-monitor-select .sms__option--is-selected {
|
||||
background-color: var(--theia-ui-button-color-secondary-hover);
|
||||
color: var(--theia-content-font-color0);
|
||||
}
|
||||
|
||||
.serial-monitor-select .sms__option--is-focused {
|
||||
background-color: var(--theia-ui-button-color-secondary-hover);
|
||||
}
|
||||
|
||||
.serial-monitor-select .sms__menu {
|
||||
background-color: var(--theia-layout-color1);
|
||||
border: 1px solid var(--theia-border-color2);
|
||||
border-top: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.serial-monitor-select .sms__control.sms__control--menu-is-open {
|
||||
border: 1px solid;
|
||||
border-color: var(--theia-border-color2) !important;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.serial-monitor-select .sms__value-container .sms__single-value {
|
||||
color: var(--theia-ui-font-color1);
|
||||
}
|
||||
|
||||
.sms__menu-list {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export class ToolOutputServiceClientImpl implements ToolOutputServiceClient {
|
||||
const channel = this.outputChannelManager.getChannel(`Arduino: ${tool}`);
|
||||
channel.setVisibility(true);
|
||||
channel.append(chunk);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ArduinoToolbarComponent extends React.Component<ArduinoToolbarCompo
|
||||
}
|
||||
}
|
||||
const command = this.props.commands.getCommand(item.command);
|
||||
const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} ${command && this.props.commandIsEnabled(command.id) ? ' enabled' : ''}`
|
||||
const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} ${command && this.props.commandIsEnabled(command.id) ? 'enabled' : ''}`
|
||||
return <div key={item.id} className={cls} >
|
||||
<div className={item.id}>
|
||||
<div
|
||||
@@ -91,7 +91,7 @@ export class ArduinoToolbar extends ReactWidget {
|
||||
|
||||
protected updateItems(items: Array<TabBarToolbarItem | ReactTabBarToolbarItem>): void {
|
||||
this.items.clear();
|
||||
const revItems = items.reverse();
|
||||
const revItems = items.sort(TabBarToolbarItem.PRIORITY_COMPARATOR).reverse();
|
||||
for (const item of revItems) {
|
||||
this.items.set(item.id, item);
|
||||
}
|
||||
@@ -137,4 +137,4 @@ export namespace ArduinoToolbar {
|
||||
export function is(maybeToolbarWidget: any): maybeToolbarWidget is ArduinoToolbar {
|
||||
return maybeToolbarWidget instanceof ArduinoToolbar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Installable } from './installable';
|
||||
|
||||
export interface ArduinoComponent {
|
||||
readonly name: string;
|
||||
@@ -6,8 +7,8 @@ export interface ArduinoComponent {
|
||||
readonly description: string;
|
||||
readonly moreInfoLink?: string;
|
||||
|
||||
readonly availableVersions: string[];
|
||||
readonly availableVersions: Installable.Version[];
|
||||
readonly installable: boolean;
|
||||
|
||||
readonly installedVersion?: string;
|
||||
readonly installedVersion?: Installable.Version;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os';
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { Detailable } from './detailable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
|
||||
|
||||
@@ -46,15 +47,20 @@ export interface BoardInstalledEvent {
|
||||
readonly pkg: Readonly<BoardPackage>;
|
||||
}
|
||||
|
||||
export interface BoardUninstalledEvent {
|
||||
readonly pkg: Readonly<BoardPackage>;
|
||||
}
|
||||
|
||||
export const BoardsServiceClient = Symbol('BoardsServiceClient');
|
||||
export interface BoardsServiceClient {
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
|
||||
notifyBoardInstalled(event: BoardInstalledEvent): void
|
||||
notifyBoardUninstalled(event: BoardUninstalledEvent): void
|
||||
}
|
||||
|
||||
export const BoardsServicePath = '/services/boards-service';
|
||||
export const BoardsService = Symbol('BoardsService');
|
||||
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
|
||||
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, Detailable<BoardDetails>, JsonRpcServer<BoardsServiceClient> {
|
||||
getAttachedBoards(): Promise<{ boards: Board[] }>;
|
||||
getAvailablePorts(): Promise<{ ports: Port[] }>;
|
||||
}
|
||||
@@ -130,7 +136,7 @@ export namespace Port {
|
||||
}
|
||||
if (isOSX) {
|
||||
// Example: `/dev/cu.usbmodem14401`
|
||||
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
|
||||
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
|
||||
return [
|
||||
'/dev/cu.MALS',
|
||||
'/dev/cu.SOC',
|
||||
@@ -176,6 +182,18 @@ export interface Board {
|
||||
fqbn?: string
|
||||
}
|
||||
|
||||
export interface BoardDetails extends Board {
|
||||
fqbn: string;
|
||||
|
||||
requiredTools: Tool[];
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
readonly packager: string;
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
}
|
||||
|
||||
export namespace Board {
|
||||
|
||||
export function is(board: any): board is Board {
|
||||
@@ -209,6 +227,11 @@ export namespace Board {
|
||||
return !!board.fqbn;
|
||||
}
|
||||
|
||||
export function toString(board: Board, options: { useFqbn: boolean } = { useFqbn: true }): string {
|
||||
const fqbn = options && options.useFqbn && board.fqbn ? ` [${board.fqbn}]` : '';
|
||||
return `${board.name}${fqbn}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface AttachedSerialBoard extends Board {
|
||||
|
||||
@@ -14,6 +14,7 @@ export namespace CoreService {
|
||||
readonly uri: string;
|
||||
readonly board: Board;
|
||||
readonly port: string;
|
||||
readonly optimizeForDebug: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ export namespace CoreService {
|
||||
export interface Options {
|
||||
readonly uri: string;
|
||||
readonly board: Board;
|
||||
readonly optimizeForDebug: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
arduino-ide-extension/src/common/protocol/detailable.ts
Normal file
10
arduino-ide-extension/src/common/protocol/detailable.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
export interface Detailable<T> {
|
||||
detail(options: Detailable.Options): Promise<{ item?: T }>;
|
||||
}
|
||||
|
||||
export namespace Detailable {
|
||||
export interface Options {
|
||||
readonly id: string;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,23 @@
|
||||
export interface Installable<T> {
|
||||
install(item: T): Promise<void>;
|
||||
}
|
||||
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
|
||||
export interface Installable<T extends ArduinoComponent> {
|
||||
/**
|
||||
* If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`.
|
||||
*/
|
||||
install(options: { item: T, version?: Installable.Version }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Uninstalls the given component. It is a NOOP if not installed.
|
||||
*/
|
||||
uninstall(options: { item: T }): Promise<void>;
|
||||
}
|
||||
export namespace Installable {
|
||||
export type Version = string;
|
||||
export namespace Version {
|
||||
/**
|
||||
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
|
||||
*/
|
||||
export const COMPARATOR = (left: Version, right: Version) => naturalCompare(right, left);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,9 @@ import { ArduinoComponent } from './arduino-component';
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
export interface LibraryService extends Installable<Library>, Searchable<Library> {
|
||||
install(library: Library): Promise<void>;
|
||||
install(options: { item: Library, version?: Installable.Version }): Promise<void>;
|
||||
}
|
||||
|
||||
export interface Library extends ArduinoComponent {
|
||||
readonly builtIn?: boolean;
|
||||
}
|
||||
|
||||
export namespace Library {
|
||||
// TODO: figure out whether we need a dedicated `version` type.
|
||||
export type Version = string;
|
||||
}
|
||||
@@ -1,14 +1,52 @@
|
||||
import { JsonRpcServer } from '@theia/core';
|
||||
import { Board } from './boards-service';
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { Board, Port } from './boards-service';
|
||||
|
||||
export interface MonitorError {
|
||||
export interface Status { }
|
||||
export interface OK extends Status { }
|
||||
export interface ErrorStatus extends Status {
|
||||
readonly message: string;
|
||||
readonly code: number
|
||||
}
|
||||
export namespace Status {
|
||||
export function isOK(status: Status & { message?: string }): status is OK {
|
||||
return typeof status.message !== 'string';
|
||||
}
|
||||
export const OK: OK = {};
|
||||
export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
|
||||
export const ALREADY_CONNECTED: ErrorStatus = { message: 'Already connected.' };
|
||||
}
|
||||
|
||||
export interface MonitorReadEvent {
|
||||
readonly connectionId: string;
|
||||
readonly data: string;
|
||||
export const MonitorServicePath = '/services/serial-monitor';
|
||||
export const MonitorService = Symbol('MonitorService');
|
||||
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
|
||||
connect(config: MonitorConfig): Promise<Status>;
|
||||
disconnect(): Promise<Status>;
|
||||
send(data: string | Uint8Array): Promise<Status>;
|
||||
}
|
||||
|
||||
export interface MonitorConfig {
|
||||
readonly board: Board;
|
||||
readonly port: Port;
|
||||
/**
|
||||
* Defaults to [`SERIAL`](MonitorConfig#ConnectionType#SERIAL).
|
||||
*/
|
||||
readonly type?: MonitorConfig.ConnectionType;
|
||||
/**
|
||||
* Defaults to `9600`.
|
||||
*/
|
||||
readonly baudRate?: MonitorConfig.BaudRate;
|
||||
|
||||
}
|
||||
export namespace MonitorConfig {
|
||||
|
||||
export type BaudRate = 300 | 1200 | 2400 | 4800 | 9600 | 19200 | 38400 | 57600 | 115200;
|
||||
export namespace BaudRate {
|
||||
export const DEFAULT: BaudRate = 9600;
|
||||
}
|
||||
|
||||
export enum ConnectionType {
|
||||
SERIAL = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const MonitorServiceClient = Symbol('MonitorServiceClient');
|
||||
@@ -17,28 +55,31 @@ export interface MonitorServiceClient {
|
||||
notifyError(event: MonitorError): void;
|
||||
}
|
||||
|
||||
export const MonitorServicePath = '/services/serial-monitor';
|
||||
export const MonitorService = Symbol('MonitorService');
|
||||
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
|
||||
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
|
||||
disconnect(connectionId: string): Promise<boolean>;
|
||||
send(connectionId: string, data: string | Uint8Array): Promise<void>;
|
||||
getConnectionIds(): Promise<string[]>;
|
||||
export interface MonitorReadEvent {
|
||||
readonly data: string;
|
||||
}
|
||||
|
||||
export interface ConnectionConfig {
|
||||
readonly board: Board;
|
||||
readonly port: string;
|
||||
export interface MonitorError {
|
||||
readonly message: string;
|
||||
/**
|
||||
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
|
||||
* If no `code` is available, clients must reestablish the serial-monitor connection.
|
||||
*/
|
||||
readonly type?: ConnectionType;
|
||||
/**
|
||||
* Defaults to `9600`.
|
||||
*/
|
||||
readonly baudRate?: number;
|
||||
readonly code: number | undefined;
|
||||
readonly config: MonitorConfig;
|
||||
}
|
||||
|
||||
export enum ConnectionType {
|
||||
SERIAL = 0
|
||||
export namespace MonitorError {
|
||||
export namespace ErrorCodes {
|
||||
/**
|
||||
* The frontend has refreshed the browser, for instance.
|
||||
*/
|
||||
export const CLIENT_CANCEL = 1;
|
||||
/**
|
||||
* When detaching a physical device when the duplex channel is still opened.
|
||||
*/
|
||||
export const DEVICE_NOT_CONFIGURED = 2;
|
||||
/**
|
||||
* Another serial monitor was opened on this port. For another electron-instance, Java IDE.
|
||||
*/
|
||||
export const DEVICE_BUSY = 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ export interface SketchesService {
|
||||
getSketches(uri?: string): Promise<Sketch[]>
|
||||
getSketchFiles(uri: string): Promise<string[]>
|
||||
/**
|
||||
* Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified,
|
||||
* it falls back to the default `sketchDirUri` from the CLI.
|
||||
* Creates a new sketch folder in the `parentUri` location.
|
||||
* Normally, `parentUri` is the client's workspace root, or the default `sketchDirUri` from the CLI.
|
||||
* Note, `parentUri` and `sketchDirUri` can be the same.
|
||||
*/
|
||||
createNewSketch(parentUri?: string): Promise<Sketch>
|
||||
createNewSketch(parentUri: string): Promise<Sketch>
|
||||
isSketchFolder(uri: string): Promise<boolean>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import { injectable } from 'inversify';
|
||||
import * as electron from 'electron';
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||
import { EditorMode } from '../browser/editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ElectronArduinoMenuContribution extends ElectronMenuContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.editorMode.menuContentChanged.event(() => {
|
||||
const createdMenuBar = this.factory.createMenuBar();
|
||||
if (isOSX) {
|
||||
electron.remote.Menu.setApplicationMenu(createdMenuBar);
|
||||
} else {
|
||||
electron.remote.getCurrentWindow().setMenu(createdMenuBar);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected hideTopPanel(): void {
|
||||
// NOOP
|
||||
// We reuse the `div` for the Arduino toolbar.
|
||||
|
||||
@@ -38,10 +38,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ArduinoCliContribution).toSelf().inSingletonScope();
|
||||
bind(CliContribution).toService(ArduinoCliContribution);
|
||||
|
||||
// Provides the path of the Ardunio CLI.
|
||||
// Provides the path of the Arduino CLI.
|
||||
bind(ArduinoCli).toSelf().inSingletonScope();
|
||||
|
||||
// Shared daemonn
|
||||
// Shared daemon
|
||||
bind(ArduinoDaemon).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(ArduinoDaemon);
|
||||
|
||||
@@ -64,7 +64,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bindBackendService(SketchesServicePath, SketchesService);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule);
|
||||
|
||||
|
||||
// Config service
|
||||
bind(ConfigServiceImpl).toSelf().inSingletonScope();
|
||||
bind(ConfigService).toService(ConfigServiceImpl);
|
||||
@@ -161,8 +161,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Set up cpp extension
|
||||
if (!process.env.CPP_CLANGD_COMMAND) {
|
||||
const executable = os.platform() === 'win32' ? 'clangd.exe' : 'clangd';
|
||||
const clangdCommand = join(__dirname, '..', '..', 'build', executable);
|
||||
const segments = ['..', '..', 'build'];
|
||||
if (os.platform() === 'win32') {
|
||||
segments.push('clangd.exe');
|
||||
} else {
|
||||
segments.push('bin');
|
||||
segments.push('clangd');
|
||||
}
|
||||
const clangdCommand = join(__dirname, ...segments);
|
||||
if (fs.existsSync(clangdCommand)) {
|
||||
process.env.CPP_CLANGD_COMMAND = clangdCommand;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import * as os from 'os';
|
||||
import * as which from 'which';
|
||||
import { spawn } from 'child_process';
|
||||
import { join, delimiter } from 'path';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { Config } from '../common/protocol/config-service';
|
||||
import { spawnCommand, getExecPath } from './exec-util';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoCli {
|
||||
@@ -13,88 +10,40 @@ export class ArduinoCli {
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
private execPath: string | undefined;
|
||||
|
||||
async getExecPath(): Promise<string> {
|
||||
const build = join(__dirname, '..', '..', 'build');
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
which(`arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`, { path: `${process.env.PATH}${delimiter}${build}` }, (err, path) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(path);
|
||||
});
|
||||
});
|
||||
if (this.execPath) {
|
||||
return this.execPath;
|
||||
}
|
||||
const path = await getExecPath('arduino-cli', this.logger, 'version');
|
||||
this.execPath = path;
|
||||
return path;
|
||||
}
|
||||
|
||||
async getVersion(): Promise<string> {
|
||||
const execPath = await this.getExecPath();
|
||||
return this.spawn(`"${execPath}"`, ['version']);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const buffers: Buffer[] = [];
|
||||
const cp = spawn(`"${execPath}"`, ['version'], { windowsHide: true, shell: true });
|
||||
cp.stdout.on('data', (b: Buffer) => buffers.push(b));
|
||||
cp.on('error', error => reject(error));
|
||||
cp.on('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
const result = Buffer.concat(buffers).toString('utf8').trim()
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
if (signal) {
|
||||
reject(new Error(`Process exited with signal: ${signal}`));
|
||||
return;
|
||||
}
|
||||
if (code) {
|
||||
reject(new Error(`Process exited with exit code: ${code}`));
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
return spawnCommand(`"${execPath}"`, ['version'], this.logger);
|
||||
}
|
||||
|
||||
async getDefaultConfig(): Promise<Config> {
|
||||
const execPath = await this.getExecPath();
|
||||
const result = await this.spawn(`"${execPath}"`, ['config', 'dump', '--format', 'json']);
|
||||
const { sketchbook_path, arduino_data } = JSON.parse(result);
|
||||
if (!sketchbook_path) {
|
||||
throw new Error(`Could not parse config. 'sketchbook_path' was missing from: ${result}`);
|
||||
const result = await spawnCommand(`"${execPath}"`, ['config', 'dump', '--format', 'json'], this.logger);
|
||||
const { directories } = JSON.parse(result);
|
||||
if (!directories) {
|
||||
throw new Error(`Could not parse config. 'directories' was missing from: ${result}`);
|
||||
}
|
||||
if (!arduino_data) {
|
||||
throw new Error(`Could not parse config. 'arduino_data' was missing from: ${result}`);
|
||||
const { user, data } = directories;
|
||||
if (!user) {
|
||||
throw new Error(`Could not parse config. 'user' was missing from: ${result}`);
|
||||
}
|
||||
if (!data) {
|
||||
throw new Error(`Could not parse config. 'data' was missing from: ${result}`);
|
||||
}
|
||||
return {
|
||||
sketchDirUri: FileUri.create(sketchbook_path).toString(),
|
||||
dataDirUri: FileUri.create(arduino_data).toString()
|
||||
sketchDirUri: FileUri.create(user).toString(),
|
||||
dataDirUri: FileUri.create(data).toString()
|
||||
};
|
||||
}
|
||||
|
||||
private spawn(command: string, args?: string[]): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const buffers: Buffer[] = [];
|
||||
const cp = spawn(command, args, { windowsHide: true, shell: true });
|
||||
cp.stdout.on('data', (b: Buffer) => buffers.push(b));
|
||||
cp.on('error', error => {
|
||||
this.logger.error(`Error executing ${command} with args: ${JSON.stringify(args)}.`, error);
|
||||
reject(error);
|
||||
});
|
||||
cp.on('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
const result = Buffer.concat(buffers).toString('utf8').trim()
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
if (signal) {
|
||||
this.logger.error(`Unexpected signal '${signal}' when executing ${command} with args: ${JSON.stringify(args)}.`);
|
||||
reject(new Error(`Process exited with signal: ${signal}`));
|
||||
return;
|
||||
}
|
||||
if (code) {
|
||||
this.logger.error(`Unexpected exit code '${code}' when executing ${command} with args: ${JSON.stringify(args)}.`);
|
||||
reject(new Error(`Process exited with exit code: ${code}`));
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user