Merge pull request #58 from bcmi-labs/use-sketch-as-ws

Use sketch folder as workspace
This commit is contained in:
Jan Bicker 2019-09-03 16:32:08 +02:00 committed by GitHub
commit 206b65f138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 418 additions and 181 deletions

3
.gitignore vendored
View File

@ -6,7 +6,8 @@ build/
downloads/
!electron/build/
src-gen/
arduino-ide-*/webpack.config.js
browser-app/webpack.config.js
electron-app/webpack.config.js
.DS_Store
/workspace/static
# switching from `electron` to `browser` in dev mode.

View File

@ -8,7 +8,7 @@ ports:
tasks:
- init: >
yarn &&
yarn --cwd ./arduino-ide-browser start
yarn --cwd ./browser-app start
github:
prebuilds:

6
.vscode/launch.json vendored
View File

@ -14,7 +14,7 @@
"type": "node",
"request": "launch",
"name": "Launch Backend",
"program": "${workspaceRoot}/arduino-ide-browser/src-gen/backend/main.js",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
@ -26,8 +26,8 @@
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/arduino-ide-browser/src-gen/backend/*.js",
"${workspaceRoot}/arduino-ide-browser/lib/**/*.js",
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
],
"smartStep": true,

16
.vscode/tasks.json vendored
View File

@ -4,9 +4,9 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino-PoC - Start Browser Example",
"label": "Arduino Editor - Start Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser start",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
@ -15,7 +15,7 @@
}
},
{
"label": "Arduino-PoC - Watch Theia Extension",
"label": "Arduino Editor - Watch Theia Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@ -26,9 +26,9 @@
}
},
{
"label": "Arduino-PoC - Watch Browser Example",
"label": "Arduino Editor - Watch Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser watch",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
@ -37,11 +37,11 @@
}
},
{
"label": "Arduino-PoC - Watch All",
"label": "Arduino Editor - Watch All",
"type": "shell",
"dependsOn": [
"Arduino-PoC - Watch Theia Extension",
"Arduino-PoC - Watch Browser Example"
"Arduino Editor - Watch Theia Extension",
"Arduino Editor - Watch Browser Example"
]
}
]

View File

@ -17,7 +17,7 @@ git clone https://github.com/bcmi-labs/arduino-editor
cd arduino-editor
yarn
yarn rebuild:electron
yarn --cwd arduino-ide-electron start
yarn --cwd electron-app start
```
If you want to switch back to the browser-based example, execute the following in the repository root
@ -26,7 +26,7 @@ yarn rebuild:browser
```
Then you can start the browser example again:
```
yarn --cwd arduino-ide-browser start
yarn --cwd browser-app start
```
## Arduino-PoC Electron Application

View File

@ -12,6 +12,7 @@
"@theia/core": "next",
"@theia/editor": "next",
"@theia/filesystem": "next",
"@theia/git": "next",
"@theia/languages": "next",
"@theia/markers": "next",
"@theia/monaco": "next",
@ -19,7 +20,6 @@
"@theia/workspace": "next",
"@theia/navigator": "next",
"@theia/terminal": "next",
"@theia/git": "next",
"@theia/search-in-workspace": "next",
"@types/ps-tree": "^1.1.0",
"@types/which": "^1.3.1",
@ -40,11 +40,12 @@
},
"devDependencies": {
"decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"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",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"shelljs": "^0.8.3",

View File

@ -1,11 +1,16 @@
// @ts-check
// The links to the downloads as of today (11.08.) are the followings:
// - https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli-nightly-latest-${FILE_NAME}
// - https://downloads.arduino.cc/arduino-cli/arduino-cli-latest-${FILE_NAME}
// The links to the downloads as of today (02.09.) are the followings:
// In order to get the latest nightly build for your platform use the following links replacing <DATE> with the current date, using the format YYYYMMDD (i.e for 2019/Aug/06 use 20190806 )
// Linux 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_64bit.tar.gz
// Linux ARM 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_ARM64.tar.gz
// Windows 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Windows_64bit.zip
// Mac OSX: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_macOS_64bit.tar.gz
// [...]
// redirecting to latest generated builds by replacing latest with the latest available build date, using the format YYYYMMDD (i.e for 2019/Aug/06 latest is replaced with 20190806
(async () => {
const DEFAULT_VERSION = 'nightly';
const DEFAULT_VERSION = 'latest'; // require('moment')().format('YYYYMMDD');
const os = require('os');
const fs = require('fs');
@ -14,7 +19,7 @@
const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untarbz = require('decompress-tarbz2');
const untargz = require('decompress-targz');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
@ -31,11 +36,7 @@
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
choices: [
// 'latest', // TODO: How do we get the source for `latest`. Currently, `latest` is the `0.3.7-alpha.preview`.
'nightly'
],
describe: `The version of the 'arduino-cli' to download. Defaults to ${DEFAULT_VERSION}.`
describe: `The version of the 'arduino-cli' to download with the YYYYMMDD format, or 'latest'. Defaults to ${DEFAULT_VERSION}.`
})
.option('force-download', {
alias: 'fd',
@ -68,13 +69,12 @@
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macosx.zip';
case 'win32': return 'windows.zip';
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm64': return 'linuxarm.tar.bz2';
case 'x32': return 'linux32.tar.bz2';
case 'x64': return 'linux64.tar.bz2';
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
@ -86,7 +86,7 @@
shell.exit(1);
}
const url = `https://downloads.arduino.cc/arduino-cli/${version === 'nightly' ? 'nightly/' : ''}arduino-cli-${version}-latest-${suffix}`;
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
shell.echo(`>>> Downloading 'arduino-cli' from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
@ -94,16 +94,21 @@
const files = await decompress(data, downloads, {
plugins: [
unzip(),
untarbz()
untargz()
]
});
shell.echo('<<< Decompressing succeeded.');
if (files.length !== 1) {
if (files.length === 0) {
shell.echo('Error ocurred when decompressing the CLI.');
shell.exit(1);
}
if (shell.mv('-f', path.join(downloads, files[0].path), cli).code !== 0) {
const cliIndex = files.findIndex(f => f.path.startsWith('arduino-cli'));
if (cliIndex === -1) {
shell.echo('The downloaded artifact does not contains the CLI.');
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (shell.mv('-f', path.join(downloads, files[cliIndex].path), cli).code !== 0) {
shell.echo(`Could not move file to ${cli}.`);
shell.exit(1);
}

View File

@ -46,6 +46,7 @@ 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';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@ -133,21 +134,19 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ConfigService)
protected readonly configService: ConfigService;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
constructor(@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) {
this.workspaceService.onWorkspaceChanged(() => {
if (this.workspaceService.workspace) {
this.registerSketchesInMenu(this.menuRegistry);
}
})
}
@postConstruct()
protected async init(): Promise<void> {
// This is a hack. Otherwise, the backend services won't bind.
@ -161,6 +160,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClient.boardsConfig);
this.registerSketchesInMenu(this.menuRegistry);
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@ -453,7 +454,13 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
protected async getWorkspaceSketches(): Promise<Sketch[]> {
const sketches = this.sketches.getSketches(this.workspaceService.workspace);
let sketches: Sketch[] = [];
const config = await this.configService.getConfiguration();
const stat = await this.fileSystem.getFileStat(config.sketchDirUri);
if (!!stat) {
sketches = await this.sketches.getSketches(stat);
}
return sketches;
}
@ -461,13 +468,18 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
const url = new URL(window.location.href);
const currentSketch = url.searchParams.get('sketch');
// Nothing to do if we want to open the same sketch which is already opened.
if (!!currentSketch && new URI(currentSketch).toString() === new URI(uri).toString()) {
this.messageService.info(`The '${this.labelProvider.getLongName(new URI(uri))}' is already opened.`);
const sketchUri = new URI(uri);
if (!!currentSketch && new URI(currentSketch).toString() === sketchUri.toString()) {
this.messageService.info(`The '${this.labelProvider.getLongName(sketchUri)}' is already opened.`);
// NOOP.
return;
}
// Preserve the current window if the `sketch` is not in the `searchParams`.
url.searchParams.set('sketch', uri);
const hash = await this.fileSystem.getFsPath(sketchUri.toString());
if (hash) {
url.hash = hash;
}
if (!currentSketch) {
setTimeout(() => window.location.href = url.toString(), 100);
return;
@ -543,25 +555,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return undefined;
}
// private async onNoBoardsInstalled() {
// const action = await this.messageService.info("You have no boards installed. Use the boards manager to install one.", "Open Boards Manager");
// if (!action) {
// return;
// }
// this.boardsListWidgetFrontendContribution.openView({ reveal: true });
// }
// private async onUnknownBoard() {
// const action = await this.messageService.warn("There's a board connected for which you need to install software." +
// " If this were not a PoC we would offer you the right package now.", "Open Boards Manager");
// if (!action) {
// return;
// }
// this.boardsListWidgetFrontendContribution.openView({ reveal: true });
// }
private isArduinoToolbar(maybeToolbarWidget: any): boolean {
if (maybeToolbarWidget instanceof ArduinoToolbar) {
return true;

View File

@ -56,6 +56,7 @@ 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';
const ElementQueries = require('css-element-queries/src/ElementQueries');
if (!ARDUINO_PRO_MODE) {
@ -96,6 +97,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
// Boards service
bind(BoardsService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);

View File

@ -4,6 +4,7 @@ import { WorkspaceServer } from "@theia/workspace/lib/common";
import { FileSystem, FileStat } from "@theia/filesystem/lib/common";
import URI from "@theia/core/lib/common/uri";
import { SketchFactory } from "./sketch-factory";
import { ConfigService } from "../common/protocol/config-service";
/**
* This is workaround to have custom frontend binding for the default workspace, although we
@ -21,16 +22,14 @@ export class AWorkspaceService extends WorkspaceService {
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(ConfigService)
protected readonly configService: ConfigService;
protected async getDefaultWorkspacePath(): Promise<string | undefined> {
let result = await super.getDefaultWorkspacePath();
if (!result) {
const userHome = await this.fileSystem.getCurrentUserHome();
if (!userHome) {
return;
}
// The backend has created this location if it was missing.
result = new URI(userHome.uri).resolve('Arduino-PoC').resolve('Sketches').toString();
const config = await this.configService.getConfiguration();
result = config.sketchDirUri;
}
const stat = await this.fileSystem.getFileStat(result);

View File

@ -38,7 +38,8 @@ export class SketchFactory {
const sketchDir = parent.resolve(sketchName);
const sketchFile = sketchDir.resolve(`${sketchName}.ino`);
this.fileSystem.createFolder(sketchDir.toString());
this.fileSystem.createFile(sketchFile.toString(), { content: `
this.fileSystem.createFile(sketchFile.toString(), {
content: `
void setup() {
// put your setup code here, to run once:
@ -50,7 +51,11 @@ void loop() {
}
` });
const location = new URL(window.location.href);
location.searchParams.set('sketch', sketchFile.toString());
location.searchParams.set('sketch', sketchDir.toString());
const hash = await this.fileSystem.getFsPath(sketchDir.toString());
if (hash) {
location.hash = hash;
}
this.windowService.openNewWindow(location.toString());
} catch (e) {
throw new Error("Cannot create new sketch: " + e);

View File

@ -0,0 +1,11 @@
export const ConfigServicePath = '/services/config-service';
export const ConfigService = Symbol('ConfigService');
export interface ConfigService {
getConfiguration(): Promise<Config>;
}
export interface Config {
sketchDirUri: string;
dataDirUri: string;
}

View File

@ -19,11 +19,24 @@ import { DefaultWorkspaceServerExt } from './default-workspace-server-ext';
import { WorkspaceServer } from '@theia/workspace/lib/common';
import { SketchesServiceImpl } from './sketches-service-impl';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
import { MonitorClientProvider } from './monitor/monitor-client-provider';
import { ArduinoCli } from './arduino-cli';
import { ArduinoCliContribution } from './arduino-cli-contribution';
import { CliContribution } from '@theia/core/lib/node';
import { ConfigServiceImpl } from './config-service-impl';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Theia backend CLI contribution.
bind(ArduinoCliContribution).toSelf().inSingletonScope();
bind(CliContribution).toService(ArduinoCliContribution);
// Provides the path of the Ardunio CLI.
bind(ArduinoCli).toSelf().inSingletonScope();
// Shared daemonn
bind(ArduinoDaemon).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(ArduinoDaemon);
@ -42,6 +55,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bindBackendService(SketchesServicePath, SketchesService);
});
bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule);
bind(ConfigServiceImpl).toSelf().inSingletonScope();
bind(ConfigService).toService(ConfigServiceImpl);
// Config service
const configServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bindBackendService(ConfigServicePath, ConfigService);
});
bind(ConnectionContainerModule).toConstantValue(configServiceConnectionModule);
// Boards service
const boardsServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {

View File

@ -0,0 +1,27 @@
import { injectable } from 'inversify';
import { Argv, Arguments } from 'yargs';
import { CliContribution } from '@theia/core/lib/node';
@injectable()
export class ArduinoCliContribution implements CliContribution {
protected _debugCli = false
configure(conf: Argv): void {
conf.option('debug-cli', {
description: 'Can be specified if the CLI daemon process was started externally.',
type: 'boolean',
default: false,
nargs: 1
});
}
setArguments(args: Arguments): void {
this._debugCli = args['debug-cli'];
}
get debugCli(): boolean {
return this._debugCli;
}
}

View File

@ -0,0 +1,66 @@
import * as os from 'os';
import * as which from 'which';
import * as cp 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';
@injectable()
export class ArduinoCli {
@inject(ILogger)
protected logger: ILogger;
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);
});
});
}
async getDefaultConfig(): Promise<Config> {
const command = await this.getExecPath();
return new Promise<Config>((resolve, reject) => {
cp.execFile(
command,
['config', 'dump', '--format', 'json'],
{ encoding: 'utf8' },
(error, stdout, stderr) => {
if (error) {
throw error;
}
if (stderr) {
throw new Error(stderr);
}
const { sketchbook_path, arduino_data } = JSON.parse(stdout.trim());
if (!sketchbook_path) {
reject(new Error(`Could not parse config. 'sketchbook_path' was missing from: ${stdout}`));
return;
}
if (!arduino_data) {
reject(new Error(`Could not parse config. 'arduino_data' was missing from: ${stdout}`));
return;
}
resolve({
sketchDirUri: FileUri.create(sketchbook_path).toString(),
dataDirUri: FileUri.create(arduino_data).toString()
});
});
});
}
}

View File

@ -1,6 +1,4 @@
import * as which from 'which';
import * as os from 'os';
import { join, delimiter } from 'path';
import { join } from 'path';
import { exec, spawn, SpawnOptions } from 'child_process';
import { inject, injectable, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
@ -9,17 +7,22 @@ import { Deferred } from '@theia/core/lib/common/promise-util';
import { environment } from '@theia/application-package/lib/environment';
import { DaemonLog } from './daemon-log';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { ArduinoCliContribution } from './arduino-cli-contribution';
import { ArduinoCli } from './arduino-cli';
@injectable()
export class ArduinoDaemon implements BackendApplicationContribution {
// Set this to `true` if you want to connect to a CLI running in debug mode.
static DEBUG_CLI = false;
@inject(ILogger)
@named('daemon')
protected readonly logger: ILogger
@inject(ArduinoCli)
protected readonly cli: ArduinoCli;
@inject(ArduinoCliContribution)
protected readonly cliContribution: ArduinoCliContribution;
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
@ -27,19 +30,10 @@ export class ArduinoDaemon implements BackendApplicationContribution {
async onStart() {
try {
if (!ArduinoDaemon.DEBUG_CLI) {
const build = join(__dirname, '..', '..', 'build');
const executable = await 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.cliContribution.debugCli) {
const executable = await this.cli.getExecPath();
this.logger.info(`>>> Starting 'arduino-cli' daemon... [${executable}]`);
const daemon = exec(`${executable} --debug daemon`, (err, stdout, stderr) => {
const daemon = exec(`${executable} daemon -v --log-level info --format json --log-format json`, (err, stdout, stderr) => {
if (err || stderr) {
console.log(err || new Error(stderr));
return;
@ -74,7 +68,7 @@ export class ArduinoDaemon implements BackendApplicationContribution {
await new Promise(resolve => setTimeout(resolve, 2000));
this.isReady.resolve();
if (!ArduinoDaemon.DEBUG_CLI) {
if (!this.cliContribution.debugCli) {
this.logger.info(`<<< The 'arduino-cli' daemon is up an running.`);
} else {
this.logger.info(`Assuming the 'arduino-cli' already runs in debug mode.`);

View File

@ -51,6 +51,9 @@ export class CompileReq extends jspb.Message {
getExportfile(): string;
setExportfile(value: string): void;
getJobs(): number;
setJobs(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CompileReq.AsObject;
@ -77,6 +80,7 @@ export namespace CompileReq {
quiet: boolean,
vidpid: string,
exportfile: string,
jobs: number,
}
}

View File

@ -81,7 +81,8 @@ proto.cc.arduino.cli.commands.CompileReq.toObject = function(includeInstance, ms
verbose: jspb.Message.getFieldWithDefault(msg, 10, false),
quiet: jspb.Message.getFieldWithDefault(msg, 11, false),
vidpid: jspb.Message.getFieldWithDefault(msg, 12, ""),
exportfile: jspb.Message.getFieldWithDefault(msg, 13, "")
exportfile: jspb.Message.getFieldWithDefault(msg, 13, ""),
jobs: jspb.Message.getFieldWithDefault(msg, 14, 0)
};
if (includeInstance) {
@ -171,6 +172,10 @@ proto.cc.arduino.cli.commands.CompileReq.deserializeBinaryFromReader = function(
var value = /** @type {string} */ (reader.readString());
msg.setExportfile(value);
break;
case 14:
var value = /** @type {number} */ (reader.readInt32());
msg.setJobs(value);
break;
default:
reader.skipField();
break;
@ -292,6 +297,13 @@ proto.cc.arduino.cli.commands.CompileReq.serializeBinaryToWriter = function(mess
f
);
}
f = message.getJobs();
if (f !== 0) {
writer.writeInt32(
14,
f
);
}
};
@ -527,6 +539,21 @@ proto.cc.arduino.cli.commands.CompileReq.prototype.setExportfile = function(valu
};
/**
* optional int32 jobs = 14;
* @return {number}
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.getJobs = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 14, 0));
};
/** @param {number} value */
proto.cc.arduino.cli.commands.CompileReq.prototype.setJobs = function(value) {
jspb.Message.setProto3IntField(this, 14, value);
};
/**
* Generated by JsPbCodeGenerator.

View File

@ -0,0 +1,14 @@
import { injectable, inject } from "inversify";
import { ConfigService, Config } from "../common/protocol/config-service";
import { ArduinoCli } from "./arduino-cli";
@injectable()
export class ConfigServiceImpl implements ConfigService {
@inject(ArduinoCli)
protected readonly cli: ArduinoCli;
async getConfiguration(): Promise<Config> {
return this.cli.getDefaultConfig();
}
}

View File

@ -1,5 +1,12 @@
import { inject, injectable } from 'inversify';
import * as fs from 'fs';
import * as path from 'path';
import * as grpc from '@grpc/grpc-js';
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { FileSystem } from '@theia/filesystem/lib/common';
import { WorkspaceServiceExt } from '../browser/workspace-service-ext';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { ArduinoCoreClient } from './cli-protocol/commands/commands_grpc_pb';
import {
InitResp,
@ -10,16 +17,10 @@ import {
UpdateLibrariesIndexReq,
UpdateLibrariesIndexResp
} from './cli-protocol/commands/commands_pb';
import { WorkspaceServiceExt } from '../browser/workspace-service-ext';
import { FileSystem } from '@theia/filesystem/lib/common';
import URI from '@theia/core/lib/common/uri';
import { CoreClientProvider, Client } from './core-client-provider';
import * as PQueue from 'p-queue';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { ArduinoCli } from './arduino-cli';
import { Instance } from './cli-protocol/commands/common_pb';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as os from 'os';
import { CoreClientProvider, Client } from './core-client-provider';
import { FileUri } from '@theia/core/lib/node';
@injectable()
export class CoreClientProviderImpl implements CoreClientProvider {
@ -36,6 +37,9 @@ export class CoreClientProviderImpl implements CoreClientProvider {
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
@inject(ArduinoCli)
protected readonly cli: ArduinoCli;
async getClient(workspaceRootOrResourceUri?: string): Promise<Client | undefined> {
return this.clientRequestQueue.add(() => new Promise<Client | undefined>(async resolve => {
const roots = await this.workspaceServiceExt.roots();
@ -76,19 +80,26 @@ export class CoreClientProviderImpl implements CoreClientProvider {
throw new Error(`Could not resolve filesystem path of URI: ${rootUri}.`);
}
const defaultDownloadsDirPath = path.resolve(os.homedir(), 'Arduino-PoC', 'downloads');
if (!fs.existsSync(defaultDownloadsDirPath)) {
fs.mkdirpSync(defaultDownloadsDirPath);
const { dataDirUri, sketchDirUri } = await this.cli.getDefaultConfig();
const dataDirPath = FileUri.fsPath(dataDirUri);
const sketchDirPath = FileUri.fsPath(sketchDirUri);
if (!fs.existsSync(dataDirPath)) {
fs.mkdirSync(dataDirPath);
}
const defaultDataDirPath = path.resolve(os.homedir(), 'Arduino-PoC', 'data')
if (!fs.existsSync(defaultDataDirPath)) {
fs.mkdirpSync(defaultDataDirPath);
if (!fs.existsSync(sketchDirPath)) {
fs.mkdirSync(sketchDirPath);
}
config.setSketchbookdir(rootPath);
config.setDatadir(defaultDataDirPath);
config.setDownloadsdir(defaultDownloadsDirPath);
const downloadDir = path.join(dataDirPath, 'staging');
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir);
}
config.setSketchbookdir(sketchDirPath);
config.setDatadir(dataDirPath);
config.setDownloadsdir(downloadDir);
config.setBoardmanageradditionalurlsList(['https://downloads.arduino.cc/packages/package_index.json']);
const initReq = new InitReq();

View File

@ -8,7 +8,58 @@ export interface DaemonLog {
export namespace DaemonLog {
export type Level = 'info' | 'debug' | 'warning' | 'error';
export interface Url {
readonly Scheme: string;
readonly Host: string;
readonly Path: string;
}
export namespace Url {
export function is(arg: any | undefined): arg is Url {
return !!arg
&& typeof arg.Scheme === 'string'
&& typeof arg.Host === 'string'
&& typeof arg.Path === 'string';
}
export function toString(url: Url): string {
const { Scheme, Host, Path } = url;
return `${Scheme}://${Host}${Path}`;
}
}
export interface System {
readonly os: string;
// readonly Resource: Resource;
}
export namespace System {
export function toString(system: System): string {
return `OS: ${system.os}`
}
}
export interface Tool {
readonly version: string;
readonly systems: System[];
}
export namespace Tool {
export function is(arg: any | undefined): arg is Tool {
return !!arg && typeof arg.version === 'string' && 'systems' in arg;
}
export function toString(tool: Tool): string {
const { version, systems } = tool;
return `Version: ${version}${!!systems ? ` Systems: [${tool.systems.map(System.toString).join(', ')}]` : ''}`;
}
}
export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error';
export function is(arg: any | undefined): arg is DaemonLog {
return !!arg
@ -20,61 +71,62 @@ export namespace DaemonLog {
export function toLogLevel(log: DaemonLog): LogLevel {
const { level } = log;
switch (level) {
case 'info': return LogLevel.INFO;
case 'trace': return LogLevel.TRACE;
case 'debug': return LogLevel.DEBUG;
case 'error': return LogLevel.ERROR;
case 'info': return LogLevel.INFO;
case 'warning': return LogLevel.WARN;
case 'error': return LogLevel.ERROR;
default: return LogLevel.INFO;
}
}
export function log(logger: ILogger, toLog: string): void {
const segments = toLog.split('time').filter(s => s.trim().length > 0);
for (const segment of segments) {
const maybeDaemonLog = parse(`time${segment}`.trim());
for (const logMsg of maybeDaemonLog) {
logger.log(toLogLevel(logMsg), logMsg.msg);
}
export function log(logger: ILogger, logMessages: string): void {
const parsed = parse(logMessages);
for (const log of parsed) {
logger.log(toLogLevel(log), toMessage(log));
}
}
// Super naive.
function parse(toLog: string): DaemonLog[] {
const messages = toLog.split('\ntime=');
const messages = toLog.trim().split('\n');
const result: DaemonLog[] = [];
for (let i = 0; i < messages.length; i++) {
const msg = (i > 0 ? 'time=' : '') + messages[i];
const rawSegments = msg.split(/(\s+)/)
.map(segment => segment.replace(/['"]+/g, ''))
.map(segment => segment.trim())
.filter(segment => segment.length > 0);
const timeIndex = rawSegments.findIndex(segment => segment.startsWith('time='));
const levelIndex = rawSegments.findIndex(segment => segment.startsWith('level='));
const msgIndex = rawSegments.findIndex(segment => segment.startsWith('msg='));
if (rawSegments.length > 2
&& timeIndex !== -1
&& levelIndex !== -1
&& msgIndex !== -1) {
result.push({
time: rawSegments[timeIndex].split('=')[1],
level: rawSegments[levelIndex].split('=')[1] as Level,
msg: [rawSegments[msgIndex].split('=')[1], ...rawSegments.slice(msgIndex + 1)].join(' ')
});
} else {
try {
const maybeDaemonLog = JSON.parse(messages[i]);
if (DaemonLog.is(maybeDaemonLog)) {
result.push(maybeDaemonLog);
continue;
}
} catch { /* NOOP */ }
result.push({
time: new Date().toString(),
level: 'info',
msg: msg
time: new Date().toString(),
level: 'info',
msg: messages[i]
});
}
}
// Otherwise, log the string as is.
return result;
}
export function toPrettyString(logMessage: string): string {
const parsed = parse(logMessage);
return parsed.map(msg => `[${msg.level.toUpperCase() || 'INFO'}] ${msg.msg}\n`).join('');
export function toPrettyString(logMessages: string): string {
const parsed = parse(logMessages);
return parsed.map(toMessage).join('\n');
}
function toMessage(log: DaemonLog): string {
const details = Object.keys(log).filter(key => key !== 'msg' && key !== 'level' && key !== 'time').map(key => toDetails(log, key)).join(', ');
return `[${log.level.toUpperCase()}] ${log.msg}${!!details ? ` [${details}]` : ''}`
}
function toDetails(log: DaemonLog, key: string): string {
let value = (log as any)[key];
if (DaemonLog.Url.is(value)) {
value = DaemonLog.Url.toString(value);
} else if (DaemonLog.Tool.is(value)) {
value = DaemonLog.Tool.toString(value);
} else if (typeof value === 'object') {
value = JSON.stringify(value).replace(/\"([^(\")"]+)\":/g, '$1:'); // Remove the quotes from the property keys.
}
return `${key.toLowerCase()}: ${value}`;
}
}

View File

@ -1,14 +1,15 @@
import * as os from 'os';
import * as path from 'path';
import { injectable } from 'inversify';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { injectable, inject } from 'inversify';
import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server';
import { ConfigService } from '../common/protocol/config-service';
@injectable()
export class DefaultWorkspaceServerExt extends DefaultWorkspaceServer {
@inject(ConfigService) protected readonly configService: ConfigService;
protected async getWorkspaceURIFromCli(): Promise<string | undefined> {
return FileUri.create(path.join(os.homedir(), 'Arduino-PoC', 'Sketches')).toString();
const config = await this.configService.getConfiguration();
return config.sketchDirUri;
}
}

View File

@ -1,6 +1,6 @@
{
"private": true,
"name": "arduino-ide-browser",
"name": "browser-app",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
@ -30,7 +30,7 @@
"theia": {
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"applicationName": "Arduino Editor",
"defaultTheme": "arduino-theme",
"preferences": {
"editor.autoSave": "on"

View File

@ -1,6 +1,6 @@
{
"private": true,
"name": "arduino-ide-electron",
"name": "electron-app",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
@ -33,7 +33,7 @@
"target": "electron",
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"applicationName": "Arduino Editor",
"defaultTheme": "arduino-theme",
"preferences": {
"editor.autoSave": "on"

View File

@ -14,7 +14,7 @@
const workingCopy = 'working-copy';
/**
* Relative path from the `__dirname` to the root where the `arduino-ide-extension` and the `arduino-ide-electron` folders are.
* Relative path from the `__dirname` to the root where the `arduino-ide-extension` and the `electron-app` folders are.
* This could come handy when moving the location of the `electron/packager`.
*/
const rootPath = join('..', '..');
@ -42,18 +42,18 @@
// Copy the following items into the `working-copy` folder. Make sure to reuse the `yarn.lock`. |
//----------------------------------------------------------------------------------------------+
mkdir('-p', path('..', workingCopy));
for (const name of ['arduino-ide-extension', 'arduino-ide-electron', 'yarn.lock', 'package.json', 'lerna.json']) {
for (const name of ['arduino-ide-extension', 'electron-app', 'yarn.lock', 'package.json', 'lerna.json']) {
cp('-rf', path(rootPath, name), path('..', workingCopy));
}
//-----------------------------------------------------+
// No need to build the `arduino-ide-browser` example. |
// No need to build the `browser-app` example. |
//-----------------------------------------------------+
//@ts-ignore
let pkg = require('../working-copy/package.json');
const workspaces = pkg.workspaces;
// We cannot remove the `arduino-ide-electron`. Otherwise, there is not way to collect the unused dependencies.
const dependenciesToRemove = ['arduino-ide-browser'];
// We cannot remove the `electron-app`. Otherwise, there is not way to collect the unused dependencies.
const dependenciesToRemove = ['browser-app'];
for (const dependencyToRemove of dependenciesToRemove) {
const index = workspaces.indexOf(dependencyToRemove);
if (index !== -1) {
@ -70,13 +70,13 @@
// Collect all unused dependencies by the backend. We have to remove them from the electron app.
// The `bundle.js` already contains everything we need for the frontend.
// We have to do it before changing the dependencies to `local-path`.
const unusedDependencies = await utils.collectUnusedDependencies('../working-copy/arduino-ide-electron/');
const unusedDependencies = await utils.collectUnusedDependencies('../working-copy/electron-app/');
//------------------------------------------------------------------------------------+
// Merge the `working-copy/package.json` with `electron/build/template-package.json`. |
//------------------------------------------------------------------------------------+
// @ts-ignore
pkg = require('../working-copy/arduino-ide-electron/package.json');
pkg = require('../working-copy/electron-app/package.json');
// @ts-ignore
const template = require('../build/template-package.json');
template.build.files = [ ...template.build.files, ...unusedDependencies.map(name => `!node_modules/${name}`) ];

View File

@ -1,7 +1,7 @@
{
"name": "arduino-poc",
"name": "arduino-editor",
"version": "0.0.1",
"description": "A PoC demonstrating an Arduino IDE built using Eclipse Theia",
"description": "Arduino IDE built using Eclipse Theia",
"main": "index.js",
"repository": "https://github.com/bcmi-labs/arduino-editor.git",
"author": "Christian Weichel <christian.weichel@typefox.io>",
@ -14,12 +14,12 @@
"prepare": "lerna run prepare",
"rebuild:browser": "theia rebuild:browser",
"rebuild:electron": "theia rebuild:electron",
"start": "cd arduino-ide-browser && yarn start",
"start": "yarn --cwd ./browser-app start",
"watch": "lerna run watch --parallel"
},
"workspaces": [
"arduino-ide-electron",
"arduino-ide-browser",
"arduino-ide-extension"
"arduino-ide-extension",
"electron-app",
"browser-app"
]
}

View File

@ -4473,7 +4473,7 @@ decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
is-stream "^1.1.0"
tar-stream "^1.5.2"
decompress-tarbz2@^4.0.0, decompress-tarbz2@^4.1.1:
decompress-tarbz2@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b"
integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==
@ -4484,7 +4484,7 @@ decompress-tarbz2@^4.0.0, decompress-tarbz2@^4.1.1:
seek-bzip "^1.0.5"
unbzip2-stream "^1.0.9"
decompress-targz@^4.0.0:
decompress-targz@^4.0.0, decompress-targz@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee"
integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==
@ -8098,7 +8098,7 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment@^2.10.6, moment@^2.18.1, moment@^2.21.0:
moment@^2.10.6, moment@^2.18.1, moment@^2.21.0, moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==