ATL-302: Added built-in examples to the app.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2020-08-13 13:34:56 +02:00 committed by Akos Kitta
parent b5d7c3b45d
commit 1c9fcd0cdf
27 changed files with 728 additions and 101 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ node_modules/
lib/ lib/
downloads/ downloads/
build/ build/
Examples/
!electron/build/ !electron/build/
src-gen/ src-gen/
*webpack.config.js *webpack.config.js

View File

@ -4,10 +4,11 @@
"description": "An extension for Theia building the Arduino IDE", "description": "An extension for Theia building the Arduino IDE",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build", "prepare": "yarn download-cli && yarn download-ls && yarn clean && yarn download-examples && yarn build",
"clean": "rimraf lib", "clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js", "download-cli": "node ./scripts/download-cli.js",
"download-ls": "node ./scripts/download-ls.js", "download-ls": "node ./scripts/download-ls.js",
"download-examples": "node ./scripts/download-examples.js",
"generate-protocol": "node ./scripts/generate-protocol.js", "generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json", "lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint", "build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
@ -99,7 +100,8 @@
"lib", "lib",
"src", "src",
"build", "build",
"data" "data",
"examples"
], ],
"theiaExtensions": [ "theiaExtensions": [
{ {

View File

@ -0,0 +1,23 @@
// @ts-check
(async () => {
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const { v4 } = require('uuid');
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git clone https://github.com/arduino/arduino.git --depth 1 ${repository}`).code !== 0) {
shell.exit(1);
}
const destination = path.join(__dirname, '..', 'Examples');
shell.mkdir('-p', destination);
shell.cp('-fR', path.join(repository, 'build', 'shared', 'examples', '*'), destination);
})();

View File

@ -12,7 +12,7 @@ import { ArduinoLanguageClientContribution } from './language/arduino-language-c
import { LibraryListWidget } from './library/library-list-widget'; import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution'; import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution'; import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; import { LibraryServiceServer, LibraryServiceServerPath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
@ -118,6 +118,11 @@ import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget'; import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
import { OutputWidget } from './theia/output/output-widget'; import { OutputWidget } from './theia/output/output-widget';
import { BurnBootloader } from './contributions/burn-bootloader'; import { BurnBootloader } from './contributions/burn-bootloader';
import { ExamplesServicePath, ExamplesService } from '../common/protocol/examples-service';
import { Examples } from './contributions/examples';
import { LibraryServiceProvider } from './library/library-service-provider';
import { IncludeLibrary } from './contributions/include-library';
import { IncludeLibraryMenuUpdater } from './library/include-library-menu-updater';
const ElementQueries = require('css-element-queries/src/ElementQueries'); const ElementQueries = require('css-element-queries/src/ElementQueries');
@ -151,7 +156,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ListItemRenderer).toSelf().inSingletonScope(); bind(ListItemRenderer).toSelf().inSingletonScope();
// Library service // Library service
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope(); bind(LibraryServiceProvider).toSelf().inSingletonScope();
bind(LibraryServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServiceServerPath)).inSingletonScope();
bind(FrontendApplicationContribution).to(IncludeLibraryMenuUpdater).inSingletonScope();
// Library list widget // Library list widget
bind(LibraryListWidget).toSelf(); bind(LibraryListWidget).toSelf();
bindViewContribution(bind, LibraryListWidgetFrontendContribution); bindViewContribution(bind, LibraryListWidgetFrontendContribution);
@ -347,6 +355,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// File-system extension // File-system extension
bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope(); bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope();
// Examples service
bind(ExamplesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExamplesServicePath)).inSingletonScope();
Contribution.configure(bind, NewSketch); Contribution.configure(bind, NewSketch);
Contribution.configure(bind, OpenSketch); Contribution.configure(bind, OpenSketch);
Contribution.configure(bind, CloseSketch); Contribution.configure(bind, CloseSketch);
@ -360,4 +371,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, SketchControl); Contribution.configure(bind, SketchControl);
Contribution.configure(bind, Settings); Contribution.configure(bind, Settings);
Contribution.configure(bind, BurnBootloader); Contribution.configure(bind, BurnBootloader);
Contribution.configure(bind, Examples);
Contribution.configure(bind, IncludeLibrary);
}); });

View File

@ -27,13 +27,13 @@ export class BoardsDataStore implements FrontendApplicationContribution {
protected readonly onChangedEmitter = new Emitter<void>(); protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void { onStart(): void {
this.boardsServiceClient.onBoardsPackageInstalled(async ({ pkg }) => { this.boardsServiceClient.onBoardsPackageInstalled(async ({ item }) => {
const { installedVersion: version } = pkg; const { installedVersion: version } = item;
if (!version) { if (!version) {
return; return;
} }
let shouldFireChanged = false; let shouldFireChanged = false;
for (const fqbn of pkg.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) { for (const fqbn of item.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
const key = this.getStorageKey(fqbn, version); const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<ConfigOption[] | undefined>(key); let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
if (!data || !data.length) { if (!data || !data.length) {

View File

@ -1,11 +1,20 @@
import { injectable, inject, optional } from 'inversify'; import { injectable, inject } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { StorageService } from '@theia/core/lib/browser/storage-service'; import { StorageService } from '@theia/core/lib/browser/storage-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types'; import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, Board, Port, BoardUninstalledEvent, BoardsService } from '../../common/protocol'; import {
Port,
Board,
BoardsService,
BoardsPackage,
InstalledEvent,
UninstalledEvent,
BoardsServiceClient,
AttachedBoardsChangeEvent
} from '../../common/protocol';
import { BoardsConfig } from './boards-config'; import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils'; import { naturalCompare } from '../../common/utils';
import { compareAnything } from '../theia/monaco/comparers'; import { compareAnything } from '../theia/monaco/comparers';
@ -21,15 +30,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
@inject(ILogger) @inject(ILogger)
protected logger: ILogger; protected logger: ILogger;
@optional()
@inject(MessageService) @inject(MessageService)
protected messageService: MessageService; protected messageService: MessageService;
@inject(StorageService) @inject(StorageService)
protected storageService: StorageService; protected storageService: StorageService;
protected readonly onBoardsPackageInstalledEmitter = new Emitter<BoardInstalledEvent>(); protected readonly onBoardsPackageInstalledEmitter = new Emitter<InstalledEvent<BoardsPackage>>();
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<BoardUninstalledEvent>(); protected readonly onBoardsPackageUninstalledEmitter = new Emitter<UninstalledEvent<BoardsPackage>>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>(); protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>(); protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>(); protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
@ -119,13 +127,13 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
return false; return false;
} }
notifyBoardInstalled(event: BoardInstalledEvent): void { notifyInstalled(event: InstalledEvent<BoardsPackage>): void {
this.logger.info('Board installed: ', JSON.stringify(event)); this.logger.info('Boards package installed: ', JSON.stringify(event));
this.onBoardsPackageInstalledEmitter.fire(event); this.onBoardsPackageInstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig; const { selectedBoard } = this.boardsConfig;
const { installedVersion, id } = event.pkg; const { installedVersion, id } = event.item;
if (selectedBoard) { if (selectedBoard) {
const installedBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name); const installedBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) { if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`); this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
this.boardsConfig = { this.boardsConfig = {
@ -136,14 +144,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
} }
} }
notifyBoardUninstalled(event: BoardUninstalledEvent): void { notifyUninstalled(event: UninstalledEvent<BoardsPackage>): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event)); this.logger.info('Boards package uninstalled: ', JSON.stringify(event));
this.onBoardsPackageUninstalledEmitter.fire(event); this.onBoardsPackageUninstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig; const { selectedBoard } = this.boardsConfig;
if (selectedBoard && selectedBoard.fqbn) { if (selectedBoard && selectedBoard.fqbn) {
const uninstalledBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name); const uninstalledBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) { if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
this.logger.info(`Board package ${event.pkg.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`); this.logger.info(`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
const selectedBoardWithoutFqbn = { const selectedBoardWithoutFqbn = {
name: selectedBoard.name name: selectedBoard.name
// No FQBN // No FQBN
@ -219,7 +227,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
} }
if (!config.selectedBoard) { if (!config.selectedBoard) {
if (!options.silent && this.messageService) { if (!options.silent) {
this.messageService.warn('No boards selected.', { timeout: 3000 }); this.messageService.warn('No boards selected.', { timeout: 3000 });
} }
return false; return false;
@ -241,14 +249,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
const { name } = config.selectedBoard; const { name } = config.selectedBoard;
if (!config.selectedPort) { if (!config.selectedPort) {
if (!options.silent && this.messageService) { if (!options.silent) {
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 }); this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
} }
return false; return false;
} }
if (!config.selectedBoard.fqbn) { if (!config.selectedBoard.fqbn) {
if (!options.silent && this.messageService) { 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 }); 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 false;

View File

@ -2,6 +2,7 @@ import { inject, injectable, interfaces } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { FileSystem } from '@theia/filesystem/lib/common'; import { FileSystem } from '@theia/filesystem/lib/common';
import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
@ -13,11 +14,12 @@ import { Command, CommandRegistry, CommandContribution, CommandService } from '@
import { EditorMode } from '../editor-mode'; import { EditorMode } from '../editor-mode';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol'; import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open }; export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
@injectable() @injectable()
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution { export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, FrontendApplicationContribution {
@inject(ILogger) @inject(ILogger)
protected readonly logger: ILogger; protected readonly logger: ILogger;
@ -37,6 +39,9 @@ export abstract class Contribution implements CommandContribution, MenuContribut
@inject(LabelProvider) @inject(LabelProvider)
protected readonly labelProvider: LabelProvider; protected readonly labelProvider: LabelProvider;
onStart(app: FrontendApplication): MaybePromise<void> {
}
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
} }
@ -75,11 +80,12 @@ export abstract class SketchContribution extends Contribution {
} }
export namespace Contribution { export namespace Contribution {
export function configure<T>(bind: interfaces.Bind, serviceIdentifier: interfaces.ServiceIdentifier<T>): void { export function configure<T>(bind: interfaces.Bind, serviceIdentifier: typeof Contribution): void {
bind(serviceIdentifier).toSelf().inSingletonScope(); bind(serviceIdentifier).toSelf().inSingletonScope();
bind(CommandContribution).toService(serviceIdentifier); bind(CommandContribution).toService(serviceIdentifier);
bind(MenuContribution).toService(serviceIdentifier); bind(MenuContribution).toService(serviceIdentifier);
bind(KeybindingContribution).toService(serviceIdentifier); bind(KeybindingContribution).toService(serviceIdentifier);
bind(TabBarToolbarContribution).toService(serviceIdentifier); bind(TabBarToolbarContribution).toService(serviceIdentifier);
bind(FrontendApplicationContribution).toService(serviceIdentifier);
} }
} }

View File

@ -0,0 +1,74 @@
import { inject, injectable } from 'inversify';
import { MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
@injectable()
export class Examples extends SketchContribution {
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
protected readonly toDisposeBeforeRegister = new DisposableCollection();
onStart(): void {
this.registerExamples(); // no `await`
}
protected async registerExamples() {
let exampleContainer: ExampleContainer | undefined;
try {
exampleContainer = await this.examplesService.all();
} catch (e) {
console.error('Could not initialize built-in examples.', e);
}
if (!exampleContainer) {
this.messageService.error('Could not initialize built-in examples.');
return;
}
this.toDisposeBeforeRegister.dispose();
this.registerRecursively(exampleContainer, ArduinoMenus.FILE__SKETCH_GROUP, this.toDisposeBeforeRegister, { order: '4' });
this.menuManager.update();
}
registerRecursively(
exampleContainer: ExampleContainer,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
options?: SubMenuOptions): void {
const { label, sketches, children } = exampleContainer;
const submenuPath = [...menuPath, label];
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
this.menuRegistry.registerSubmenu(submenuPath, label, options);
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
for (const sketch of sketches) {
const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
const command = { id: commandId };
const handler = {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
}
};
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
}
}

View File

@ -0,0 +1,36 @@
import { /*inject,*/ injectable } from 'inversify';
// import { remote } from 'electron';
// import { ArduinoMenus } from '../menu/arduino-menus';
import { SketchContribution, Command, CommandRegistry } from './contribution';
import { LibraryPackage } from '../../common/protocol';
// import { SaveAsSketch } from './save-as-sketch';
// import { EditorManager } from '@theia/editor/lib/browser';
// import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
@injectable()
export class IncludeLibrary extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async arg => {
if (LibraryPackage.is(arg)) {
this.includeLibrary(arg);
}
}
});
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
// Always include to the main sketch file unless a c, cpp, or h file is the active one.
console.log('INCLUDE', library);
}
}
export namespace IncludeLibrary {
export namespace Commands {
export const INCLUDE_LIBRARY: Command = {
id: 'arduino-include-library'
};
}
}

View File

@ -6,6 +6,8 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service';
import { Examples } from './examples';
@injectable() @injectable()
export class OpenSketch extends SketchContribution { export class OpenSketch extends SketchContribution {
@ -16,6 +18,12 @@ export class OpenSketch extends SketchContribution {
@inject(ContextMenuRenderer) @inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(Examples)
protected readonly examples: Examples;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection(); protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
@ -53,6 +61,14 @@ export class OpenSketch extends SketchContribution {
}); });
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))); this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
} }
try {
const { children } = await this.examplesService.all();
for (const child of children) {
this.examples.registerRecursively(child, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
}
const options = { const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT, menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: { anchor: {

View File

@ -0,0 +1,90 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ArduinoMenus } from '../menu/arduino-menus';
import { LibraryPackage } from '../../common/protocol';
import { IncludeLibrary } from '../contributions/include-library';
import { MainMenuManager } from '../../common/main-menu-manager';
import { LibraryListWidget } from './library-list-widget';
import { LibraryServiceProvider } from './library-service-provider';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
@injectable()
export class IncludeLibraryMenuUpdater implements FrontendApplicationContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(LibraryServiceProvider)
protected readonly libraryServiceProvider: LibraryServiceProvider;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection();
async onStart(): Promise<void> {
this.updateMenuActions();
this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions())
this.libraryServiceProvider.onLibraryPackageInstalled(() => this.updateMenuActions());
this.libraryServiceProvider.onLibraryPackageUninstalled(() => this.updateMenuActions());
}
protected async updateMenuActions(): Promise<void> {
return this.queue.add(async () => {
this.toDispose.dispose();
this.mainMenuManager.update();
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
const libraries = await this.libraryServiceProvider.list({ fqbn });
// `Include Library` submenu
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
this.menuRegistry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
// `Manage Libraries...` group.
this.menuRegistry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...'
});
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction({ commandId: `${LibraryListWidget.WIDGET_ID}:toggle` })));
// `Add .ZIP Library...`
// TODO: implement it
// `Arduino libraries`
const arduinoLibsMenuPath = [...includeLibMenuPath, '2_arduino'];
for (const library of libraries.filter(({ author }) => author.toLowerCase() === 'arduino')) {
this.toDispose.push(this.registerLibrary(library, arduinoLibsMenuPath));
}
const contributedLibsMenuPath = [...includeLibMenuPath, '3_contributed'];
for (const library of libraries.filter(({ author }) => author.toLowerCase() !== 'arduino')) {
this.toDispose.push(this.registerLibrary(library, contributedLibsMenuPath));
}
this.mainMenuManager.update();
});
}
protected registerLibrary(library: LibraryPackage, menuPath: MenuPath): Disposable {
const commandId = `arduino-include-library--${library.name}:${library.author}`;
const command = { id: commandId };
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, library) };
const menuAction = { commandId, label: library.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(menuAction)),
);
}
}

View File

@ -1,17 +1,18 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service'; import { LibraryPackage } from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget'; import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
import { LibraryServiceProvider } from './library-service-provider';
@injectable() @injectable()
export class LibraryListWidget extends ListWidget<Library> { export class LibraryListWidget extends ListWidget<LibraryPackage> {
static WIDGET_ID = 'library-list-widget'; static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager'; static WIDGET_LABEL = 'Library Manager';
constructor( constructor(
@inject(LibraryService) protected service: LibraryService, @inject(LibraryServiceProvider) protected service: LibraryServiceProvider,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) { @inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<LibraryPackage>) {
super({ super({
id: LibraryListWidget.WIDGET_ID, id: LibraryListWidget.WIDGET_ID,
@ -19,7 +20,7 @@ export class LibraryListWidget extends ListWidget<Library> {
iconClass: 'library-tab-icon', iconClass: 'library-tab-icon',
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: Library) => item.name, itemLabel: (item: LibraryPackage) => item.name,
itemRenderer itemRenderer
}); });
} }

View File

@ -0,0 +1,61 @@
import { inject, injectable, postConstruct } from 'inversify';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Searchable, InstalledEvent, UninstalledEvent } from '../../common/protocol';
import { LibraryPackage, LibraryServiceServer, LibraryService } from '../../common/protocol/library-service';
@injectable()
export class LibraryServiceProvider implements Required<LibraryService> {
@inject(LibraryServiceServer)
protected readonly server: JsonRpcProxy<LibraryServiceServer>;
protected readonly onLibraryPackageInstalledEmitter = new Emitter<InstalledEvent<LibraryPackage>>();
protected readonly onLibraryPackageUninstalledEmitter = new Emitter<UninstalledEvent<LibraryPackage>>();
protected readonly toDispose = new DisposableCollection(
this.onLibraryPackageInstalledEmitter,
this.onLibraryPackageUninstalledEmitter
);
@postConstruct()
protected init(): void {
this.server.setClient({
notifyInstalled: event => this.onLibraryPackageInstalledEmitter.fire(event),
notifyUninstalled: event => this.onLibraryPackageUninstalledEmitter.fire(event)
});
}
get onLibraryPackageInstalled(): Event<InstalledEvent<LibraryPackage>> {
return this.onLibraryPackageInstalledEmitter.event;
}
get onLibraryPackageUninstalled(): Event<InstalledEvent<LibraryPackage>> {
return this.onLibraryPackageUninstalledEmitter.event;
}
// #region remote library service API
async install(options: { item: LibraryPackage; version?: string | undefined; }): Promise<void> {
return this.server.install(options);
}
async list(options: LibraryService.List.Options): Promise<LibraryPackage[]> {
return this.server.list(options);
}
async uninstall(options: { item: LibraryPackage; }): Promise<void> {
return this.server.uninstall(options);
}
async search(options: Searchable.Options): Promise<LibraryPackage[]> {
return this.server.search(options);
}
// #endregion remote API
dispose(): void {
this.toDispose.dispose();
}
}

View File

@ -12,3 +12,15 @@ export interface ArduinoComponent {
readonly installedVersion?: Installable.Version; readonly installedVersion?: Installable.Version;
} }
export namespace ArduinoComponent {
export function is(arg: any): arg is ArduinoComponent {
return !!arg
&& 'name' in arg && typeof arg['name'] === 'string'
&& 'author' in arg && typeof arg['author'] === 'string'
&& 'summary' in arg && typeof arg['summary'] === 'string'
&& 'description' in arg && typeof arg['description'] === 'string'
&& 'installable' in arg && typeof arg['installable'] === 'boolean';
}
}

View File

@ -2,7 +2,7 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { naturalCompare } from './../utils'; import { naturalCompare } from './../utils';
import { Searchable } from './searchable'; import { Searchable } from './searchable';
import { Installable } from './installable'; import { Installable, InstallableClient } from './installable';
import { ArduinoComponent } from './arduino-component'; import { ArduinoComponent } from './arduino-component';
export interface AttachedBoardsChangeEvent { export interface AttachedBoardsChangeEvent {
@ -45,19 +45,9 @@ export namespace AttachedBoardsChangeEvent {
} }
export interface BoardInstalledEvent {
readonly pkg: Readonly<BoardsPackage>;
}
export interface BoardUninstalledEvent {
readonly pkg: Readonly<BoardsPackage>;
}
export const BoardsServiceClient = Symbol('BoardsServiceClient'); export const BoardsServiceClient = Symbol('BoardsServiceClient');
export interface BoardsServiceClient { export interface BoardsServiceClient extends InstallableClient<BoardsPackage> {
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void; notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
notifyBoardInstalled(event: BoardInstalledEvent): void
notifyBoardUninstalled(event: BoardUninstalledEvent): void
} }
export const BoardsServicePath = '/services/boards-service'; export const BoardsServicePath = '/services/boards-service';
@ -194,6 +184,11 @@ export interface BoardsPackage extends ArduinoComponent {
readonly id: string; readonly id: string;
readonly boards: Board[]; readonly boards: Board[];
} }
export namespace BoardsPackage {
export function equals(left: BoardsPackage, right: BoardsPackage): boolean {
return left.id === right.id;
}
}
export interface Board { export interface Board {
readonly name: string; readonly name: string;

View File

@ -0,0 +1,13 @@
import { Sketch } from './sketches-service';
export const ExamplesServicePath = '/services/example-service';
export const ExamplesService = Symbol('ExamplesService');
export interface ExamplesService {
all(): Promise<ExampleContainer>;
}
export interface ExampleContainer {
readonly label: string;
readonly children: ExampleContainer[];
readonly sketches: Sketch[];
}

View File

@ -1,6 +1,17 @@
import { naturalCompare } from './../utils'; import { naturalCompare } from './../utils';
import { ArduinoComponent } from './arduino-component'; import { ArduinoComponent } from './arduino-component';
export interface InstalledEvent<T extends ArduinoComponent> {
readonly item: Readonly<T>;
}
export interface UninstalledEvent<T extends ArduinoComponent> {
readonly item: Readonly<T>;
}
export interface InstallableClient<T extends ArduinoComponent> {
notifyInstalled(event: InstalledEvent<T>): void
notifyUninstalled(event: UninstalledEvent<T>): void
}
export interface Installable<T extends ArduinoComponent> { export interface Installable<T extends ArduinoComponent> {
/** /**
* If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`. * If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`.

View File

@ -1,13 +1,46 @@
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Searchable } from './searchable'; import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component'; import { ArduinoComponent } from './arduino-component';
import { Installable, InstallableClient } from './installable';
export const LibraryServicePath = '/services/library-service'; export interface LibraryService extends Installable<LibraryPackage>, Searchable<LibraryPackage> {
export const LibraryService = Symbol('LibraryService'); install(options: { item: LibraryPackage, version?: Installable.Version }): Promise<void>;
export interface LibraryService extends Installable<Library>, Searchable<Library> { list(options: LibraryService.List.Options): Promise<LibraryPackage[]>;
install(options: { item: Library, version?: Installable.Version }): Promise<void>;
} }
export interface Library extends ArduinoComponent { export const LibraryServiceClient = Symbol('LibraryServiceClient');
readonly builtIn?: boolean; export interface LibraryServiceClient extends InstallableClient<LibraryPackage> {
}
export const LibraryServiceServerPath = '/services/library-service-server';
export const LibraryServiceServer = Symbol('LibraryServiceServer');
export interface LibraryServiceServer extends LibraryService, JsonRpcServer<LibraryServiceClient> {
}
export namespace LibraryService {
export namespace List {
export interface Options {
readonly fqbn?: string | undefined;
}
}
}
export interface LibraryPackage extends ArduinoComponent {
/**
* An array of string that should be included into the `ino` file if this library is used.
* For example, including `SD` will prepend `#include <SD.h>` to the `ino` file. While including `Bridge`
* requires multiple `#include` declarations: `YunClient`, `YunServer`, `Bridge`, etc.
*/
readonly includes: string[];
}
export namespace LibraryPackage {
export function is(arg: any): arg is LibraryPackage {
return ArduinoComponent.is(arg) && 'includes' in arg && Array.isArray(arg['includes']);
}
export function equals(left: LibraryPackage, right: LibraryPackage): boolean {
return left.name === right.name && left.author === right.author;
}
} }

View File

@ -22,6 +22,11 @@ export interface SketchesService {
*/ */
createNewSketch(): Promise<Sketch>; createNewSketch(): Promise<Sketch>;
/**
* Creates a new sketch with existing content. Rejects if `uri` is not pointing to a valid sketch folder.
*/
cloneExample(uri: string): Promise<Sketch>;
isSketchFolder(uri: string): Promise<boolean>; isSketchFolder(uri: string): Promise<boolean>;
/** /**

View File

@ -7,3 +7,7 @@ export function notEmpty(arg: string | undefined | null): arg is string {
export function firstToLowerCase(what: string): string { export function firstToLowerCase(what: string): string {
return what.charAt(0).toLowerCase() + what.slice(1); return what.charAt(0).toLowerCase() + what.slice(1);
} }
export function firstToUpperCase(what: string): string {
return what.charAt(0).toUpperCase() + what.slice(1);
}

View File

@ -7,7 +7,7 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { LanguageServerContribution } from '@theia/languages/lib/node'; import { LanguageServerContribution } from '@theia/languages/lib/node';
import { ArduinoLanguageServerContribution } from './language/arduino-language-server-contribution'; import { ArduinoLanguageServerContribution } from './language/arduino-language-server-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; import { LibraryServiceServerPath, LibraryServiceServer, LibraryServiceClient } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { LibraryServiceImpl } from './library-service-impl'; import { LibraryServiceImpl } from './library-service-impl';
import { BoardsServiceImpl } from './boards-service-impl'; import { BoardsServiceImpl } from './boards-service-impl';
@ -35,6 +35,8 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoEnvVariablesServer } from './arduino-env-variables-server'; import { ArduinoEnvVariablesServer } from './arduino-env-variables-server';
import { NodeFileSystemExt } from './node-filesystem-ext'; import { NodeFileSystemExt } from './node-filesystem-ext';
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext'; import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
import { ExamplesServiceImpl } from './examples-service-impl';
import { ExamplesService, ExamplesServicePath } from '../common/protocol/examples-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(EnvVariablesServer).to(ArduinoEnvVariablesServer).inSingletonScope(); rebind(EnvVariablesServer).to(ArduinoEnvVariablesServer).inSingletonScope();
@ -66,25 +68,31 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
}) })
).inSingletonScope(); ).inSingletonScope();
// Shared examples service
bind(ExamplesServiceImpl).toSelf().inSingletonScope();
bind(ExamplesService).toService(ExamplesServiceImpl);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(ExamplesServicePath, () => context.container.get(ExamplesService))).inSingletonScope();
// Language server // Language server
bind(ArduinoLanguageServerContribution).toSelf().inSingletonScope(); bind(ArduinoLanguageServerContribution).toSelf().inSingletonScope();
bind(LanguageServerContribution).toService(ArduinoLanguageServerContribution); bind(LanguageServerContribution).toService(ArduinoLanguageServerContribution);
// Library service // Library service
const libraryServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryServiceImpl).toSelf().inSingletonScope(); bind(LibraryServiceServer).toService(LibraryServiceImpl);
bind(LibraryService).toService(LibraryServiceImpl); bind(ConnectionHandler).toDynamicValue(context =>
bindBackendService(LibraryServicePath, LibraryService); new JsonRpcConnectionHandler<LibraryServiceClient>(LibraryServiceServerPath, client => {
}); const server = context.container.get<LibraryServiceImpl>(LibraryServiceImpl);
bind(ConnectionContainerModule).toConstantValue(libraryServiceConnectionModule); server.setClient(client);
client.onDidCloseConnection(() => server.dispose());
return server;
})
).inSingletonScope();
// Sketches service // Shred sketches service
const sketchesServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesServiceImpl).toSelf().inSingletonScope(); bind(SketchesService).toService(SketchesServiceImpl);
bind(SketchesService).toService(SketchesServiceImpl); bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
bindBackendService(SketchesServicePath, SketchesService);
});
bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule);
// Boards service // Boards service
const boardsServiceConnectionModule = ConnectionContainerModule.create(async ({ bind, bindBackendService }) => { const boardsServiceConnectionModule = ConnectionContainerModule.create(async ({ bind, bindBackendService }) => {
@ -190,6 +198,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// File-system extension for mapping paths to URIs // File-system extension for mapping paths to URIs
bind(NodeFileSystemExt).toSelf().inSingletonScope(); bind(NodeFileSystemExt).toSelf().inSingletonScope();
bind(FileSystemExt).toDynamicValue(context => context.container.get(NodeFileSystemExt)); bind(FileSystemExt).toService(NodeFileSystemExt);
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope(); bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope();
}); });

View File

@ -112,8 +112,8 @@ export class BoardsServiceImpl implements BoardsService {
if (this.discoveryTimer !== undefined) { if (this.discoveryTimer !== undefined) {
clearInterval(this.discoveryTimer); clearInterval(this.discoveryTimer);
} }
this.logger.info('<<< Disposed boards service.');
this.client = undefined; this.client = undefined;
this.logger.info('<<< Disposed boards service.');
} }
async getAttachedBoards(): Promise<Board[]> { async getAttachedBoards(): Promise<Board[]> {
@ -370,15 +370,15 @@ export class BoardsServiceImpl implements BoardsService {
} }
async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> { async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> {
const pkg = options.item; const item = options.item;
const version = !!options.version ? options.version : pkg.availableVersions[0]; const version = !!options.version ? options.version : item.availableVersions[0];
const coreClient = await this.coreClientProvider.client(); const coreClient = await this.coreClientProvider.client();
if (!coreClient) { if (!coreClient) {
return; return;
} }
const { client, instance } = coreClient; const { client, instance } = coreClient;
const [platform, architecture] = pkg.id.split(':'); const [platform, architecture] = item.id.split(':');
const req = new PlatformInstallReq(); const req = new PlatformInstallReq();
req.setInstance(instance); req.setInstance(instance);
@ -386,7 +386,7 @@ export class BoardsServiceImpl implements BoardsService {
req.setPlatformPackage(platform); req.setPlatformPackage(platform);
req.setVersion(version); req.setVersion(version);
console.info('Starting board installation', pkg); console.info('>>> Starting boards package installation...', item);
const resp = client.platformInstall(req); const resp = client.platformInstall(req);
resp.on('data', (r: PlatformInstallResp) => { resp.on('data', (r: PlatformInstallResp) => {
const prog = r.getProgress(); const prog = r.getProgress();
@ -399,34 +399,34 @@ export class BoardsServiceImpl implements BoardsService {
resp.on('error', reject); resp.on('error', reject);
}); });
if (this.client) { if (this.client) {
const packages = await this.search({}); const items = await this.search({});
const updatedPackage = packages.find(({ id }) => id === pkg.id) || pkg; const updated = items.find(other => BoardsPackage.equals(other, item)) || item;
this.client.notifyBoardInstalled({ pkg: updatedPackage }); this.client.notifyInstalled({ item: updated });
} }
console.info('Board installation done', pkg); console.info('<<< Boards package installation done.', item);
} }
async uninstall(options: { item: BoardsPackage }): Promise<void> { async uninstall(options: { item: BoardsPackage }): Promise<void> {
const pkg = options.item; const item = options.item;
const coreClient = await this.coreClientProvider.client(); const coreClient = await this.coreClientProvider.client();
if (!coreClient) { if (!coreClient) {
return; return;
} }
const { client, instance } = coreClient; const { client, instance } = coreClient;
const [platform, architecture] = pkg.id.split(':'); const [platform, architecture] = item.id.split(':');
const req = new PlatformUninstallReq(); const req = new PlatformUninstallReq();
req.setInstance(instance); req.setInstance(instance);
req.setArchitecture(architecture); req.setArchitecture(architecture);
req.setPlatformPackage(platform); req.setPlatformPackage(platform);
console.info('Starting board uninstallation', pkg); console.info('>>> Starting boards package uninstallation...', item);
let logged = false; let logged = false;
const resp = client.platformUninstall(req); const resp = client.platformUninstall(req);
resp.on('data', (_: PlatformUninstallResp) => { resp.on('data', (_: PlatformUninstallResp) => {
if (!logged) { if (!logged) {
this.toolOutputService.append({ tool: 'board uninstall', chunk: `uninstalling ${pkg.id}\n` }); this.toolOutputService.append({ tool: 'board uninstall', chunk: `uninstalling ${item.id}\n` });
logged = true; logged = true;
} }
}) })
@ -435,10 +435,10 @@ export class BoardsServiceImpl implements BoardsService {
resp.on('error', reject); resp.on('error', reject);
}); });
if (this.client) { if (this.client) {
// Here, unlike at `install` we send out the argument `pkg`. Otherwise, we would not know about the board FQBN. // Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN.
this.client.notifyBoardUninstalled({ pkg }); this.client.notifyUninstalled({ item });
} }
console.info('Board uninstallation done', pkg); console.info('<<< Boards package uninstallation done.', item);
} }
} }

View File

@ -15,11 +15,16 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
protected readonly toolOutputService: ToolOutputServiceServer; protected readonly toolOutputService: ToolOutputServiceServer;
protected readonly onIndexUpdatedEmitter = new Emitter<void>(); protected readonly onIndexUpdatedEmitter = new Emitter<void>();
protected readonly onClientReadyEmitter = new Emitter<void>();
get onIndexUpdated(): Event<void> { get onIndexUpdated(): Event<void> {
return this.onIndexUpdatedEmitter.event; return this.onIndexUpdatedEmitter.event;
} }
get onClientReady(): Event<void> {
return this.onClientReadyEmitter.event;
}
close(client: CoreClientProvider.Client): void { close(client: CoreClientProvider.Client): void {
client.client.close(); client.client.close();
} }
@ -28,10 +33,12 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
if (port && port === this._port) { if (port && port === this._port) {
// No need to create a new gRPC client, but we have to update the indexes. // No need to create a new gRPC client, but we have to update the indexes.
if (this._client) { if (this._client) {
this.updateIndexes(this._client); await this.updateIndexes(this._client);
this.onClientReadyEmitter.fire();
} }
} else { } else {
return super.reconcileClient(port); await super.reconcileClient(port);
this.onClientReadyEmitter.fire();
} }
} }

View File

@ -0,0 +1,79 @@
import { inject, injectable, postConstruct } from 'inversify';
import { join, basename } from 'path';
import * as fs from './fs-extra';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Sketch } from '../common/protocol/sketches-service';
import { SketchesServiceImpl } from './sketches-service-impl';
import { ExamplesService, ExampleContainer } from '../common/protocol/examples-service';
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
protected _all: ExampleContainer | undefined;
@postConstruct()
protected init(): void {
this.all();
}
async all(): Promise<ExampleContainer> {
if (this._all) {
return this._all;
}
this._all = await this.load();
return this._all;
}
protected async load(path: string = join(__dirname, '..', '..', 'Examples')): Promise<ExampleContainer> {
if (!await fs.exists(path)) {
throw new Error('Examples are not available');
}
const stat = await fs.stat(path);
if (!stat.isDirectory) {
throw new Error(`${path} is not a directory.`);
}
const names = await fs.readdir(path);
const sketches: Sketch[] = [];
const children: ExampleContainer[] = [];
for (const p of names.map(name => join(path, name))) {
const stat = await fs.stat(p);
if (stat.isDirectory()) {
const sketch = await this.tryLoadSketch(p);
if (sketch) {
sketches.push(sketch);
} else {
const child = await this.load(p);
children.push(child);
}
}
}
const label = basename(path);
return {
label,
children,
sketches
};
}
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
const map = new Map<string, fs.Stats>();
for (const path of paths) {
const stat = await fs.stat(path);
map.set(path, stat);
}
return map;
}
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(FileUri.create(path).toString());
return sketch;
} catch {
return undefined;
}
}
}

View File

@ -2,6 +2,7 @@ import * as fs from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
export const constants = fs.constants; export const constants = fs.constants;
export type Stats = fs.Stats;
export const existsSync = fs.existsSync; export const existsSync = fs.existsSync;
export const lstatSync = fs.lstatSync; export const lstatSync = fs.lstatSync;

View File

@ -1,5 +1,5 @@
import { injectable, inject } from 'inversify'; import { injectable, inject, postConstruct } from 'inversify';
import { Library, LibraryService } from '../common/protocol/library-service'; import { LibraryPackage, LibraryService, LibraryServiceClient } from '../common/protocol/library-service';
import { CoreClientProvider } from './core-client-provider'; import { CoreClientProvider } from './core-client-provider';
import { import {
LibrarySearchReq, LibrarySearchReq,
@ -15,17 +15,37 @@ import {
} from './cli-protocol/commands/lib_pb'; } from './cli-protocol/commands/lib_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Installable } from '../common/protocol/installable'; import { Installable } from '../common/protocol/installable';
import { ILogger, notEmpty } from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
@injectable() @injectable()
export class LibraryServiceImpl implements LibraryService { export class LibraryServiceImpl implements LibraryService {
@inject(ILogger)
protected logger: ILogger;
@inject(CoreClientProvider) @inject(CoreClientProvider)
protected readonly coreClientProvider: CoreClientProvider; protected readonly coreClientProvider: CoreClientProvider;
@inject(ToolOutputServiceServer) @inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer; protected readonly toolOutputService: ToolOutputServiceServer;
async search(options: { query?: string }): Promise<Library[]> { protected ready = new Deferred<void>();
protected client: LibraryServiceClient | undefined;
@postConstruct()
protected init(): void {
this.coreClientProvider.client().then(client => {
if (client) {
this.ready.resolve();
} else {
this.coreClientProvider.onClientReady(() => this.ready.resolve());
}
})
}
async search(options: { query?: string }): Promise<LibraryPackage[]> {
await this.ready.promise;
const coreClient = await this.coreClientProvider.client(); const coreClient = await this.coreClientProvider.client();
if (!coreClient) { if (!coreClient) {
return []; return [];
@ -71,9 +91,74 @@ export class LibraryServiceImpl implements LibraryService {
return items; return items;
} }
async install(options: { item: Library, version?: Installable.Version }): Promise<void> { async list({ fqbn }: { fqbn?: string | undefined }): Promise<LibraryPackage[]> {
const library = options.item; await this.ready.promise;
const version = !!options.version ? options.version : library.availableVersions[0]; const coreClient = await this.coreClientProvider.client();
if (!coreClient) {
return [];
}
const { client, instance } = coreClient;
const req = new LibraryListReq();
req.setInstance(instance);
req.setAll(true);
const resp = await new Promise<LibraryListResp>((resolve, reject) => client.libraryList(req, ((error, resp) => !!error ? reject(error) : resolve(resp))));
const x = resp.getInstalledLibraryList().map(item => {
const release = item.getRelease();
const library = item.getLibrary();
if (!release || !library) {
return undefined;
}
// https://arduino.github.io/arduino-cli/latest/rpc/commands/#librarylocation
// 0: In the libraries subdirectory of the Arduino IDE installation. (`ide_builtin`)
// 1: In the libraries subdirectory of the user directory (sketchbook). (`user`)
// 2: In the libraries subdirectory of a platform. (`platform_builtin`)
// 3: When LibraryLocation is used in a context where a board is specified, this indicates the library is
// in the libraries subdirectory of a platform referenced by the board's platform. (`referenced_platform_builtin`)
// If 0, we ignore it.
// If 1, we include always.
// If 2, we include iff `fqbn` is specified and the platform matches.
// if 3, TODO
const location = library.getLocation();
if (location === 0) {
return undefined;
}
if (location === 2) {
if (!fqbn) {
return undefined;
}
const architectures = library.getArchitecturesList();
const [platform] = library.getContainerPlatform().split(':');
if (!platform) {
return undefined;
}
const [boardPlatform, boardArchitecture] = fqbn.split(':');
if (boardPlatform !== platform || architectures.indexOf(boardArchitecture) === -1) {
return undefined;
}
}
const installedVersion = library.getVersion();
return toLibrary({
name: library.getName(),
installedVersion,
installable: true,
description: library.getSentence(),
summary: library.getParagraph(),
includes: release.getProvidesIncludesList(),
moreInfoLink: library.getWebsite()
}, release, [library.getVersion()]);
}).filter(notEmpty);
console.log(x);
return x;
}
async install(options: { item: LibraryPackage, version?: Installable.Version }): Promise<void> {
await this.ready.promise;
const item = options.item;
const version = !!options.version ? options.version : item.availableVersions[0];
const coreClient = await this.coreClientProvider.client(); const coreClient = await this.coreClientProvider.client();
if (!coreClient) { if (!coreClient) {
return; return;
@ -82,9 +167,10 @@ export class LibraryServiceImpl implements LibraryService {
const req = new LibraryInstallReq(); const req = new LibraryInstallReq();
req.setInstance(instance); req.setInstance(instance);
req.setName(library.name); req.setName(item.name);
req.setVersion(version); req.setVersion(version);
console.info('>>> Starting library package installation...', item);
const resp = client.libraryInstall(req); const resp = client.libraryInstall(req);
resp.on('data', (r: LibraryInstallResp) => { resp.on('data', (r: LibraryInstallResp) => {
const prog = r.getProgress(); const prog = r.getProgress();
@ -96,10 +182,18 @@ export class LibraryServiceImpl implements LibraryService {
resp.on('end', resolve); resp.on('end', resolve);
resp.on('error', reject); resp.on('error', reject);
}); });
if (this.client) {
const items = await this.search({});
const updated = items.find(other => LibraryPackage.equals(other, item)) || item;
this.client.notifyInstalled({ item: updated });
}
console.info('<<< Library package installation done.', item);
} }
async uninstall(options: { item: Library }): Promise<void> { async uninstall(options: { item: LibraryPackage }): Promise<void> {
const library = options.item; const item = options.item;
const coreClient = await this.coreClientProvider.client(); const coreClient = await this.coreClientProvider.client();
if (!coreClient) { if (!coreClient) {
return; return;
@ -108,14 +202,15 @@ export class LibraryServiceImpl implements LibraryService {
const req = new LibraryUninstallReq(); const req = new LibraryUninstallReq();
req.setInstance(instance); req.setInstance(instance);
req.setName(library.name); req.setName(item.name);
req.setVersion(library.installedVersion!); req.setVersion(item.installedVersion!);
console.info('>>> Starting library package uninstallation...', item);
let logged = false; let logged = false;
const resp = client.libraryUninstall(req); const resp = client.libraryUninstall(req);
resp.on('data', (_: LibraryUninstallResp) => { resp.on('data', (_: LibraryUninstallResp) => {
if (!logged) { if (!logged) {
this.toolOutputService.append({ tool: 'library', chunk: `uninstalling ${library.name}:${library.installedVersion}%\n` }); this.toolOutputService.append({ tool: 'library', chunk: `uninstalling ${item.name}:${item.installedVersion}%\n` });
logged = true; logged = true;
} }
}); });
@ -123,11 +218,25 @@ export class LibraryServiceImpl implements LibraryService {
resp.on('end', resolve); resp.on('end', resolve);
resp.on('error', reject); resp.on('error', reject);
}); });
if (this.client) {
this.client.notifyUninstalled({ item });
}
console.info('<<< Library package uninstallation done.', item);
}
setClient(client: LibraryServiceClient | undefined): void {
this.client = client;
}
dispose(): void {
this.logger.info('>>> Disposing library service...');
this.client = undefined;
this.logger.info('<<< Disposed library service.');
} }
} }
function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVersions: string[]): Library { function toLibrary(tpl: Partial<LibraryPackage>, release: LibraryRelease, availableVersions: string[]): LibraryPackage {
return { return {
name: '', name: '',
installable: false, installable: false,
@ -135,6 +244,7 @@ function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVers
author: release.getAuthor(), author: release.getAuthor(),
availableVersions, availableVersions,
includes: release.getProvidesIncludesList(),
description: release.getSentence(), description: release.getSentence(),
moreInfoLink: release.getWebsite(), moreInfoLink: release.getWebsite(),
summary: release.getParagraph() summary: release.getParagraph()

View File

@ -19,6 +19,8 @@ const MAX_FILESYSTEM_DEPTH = 40;
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/; const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
const prefix = '.arduinoProIDE-unsaved';
// TODO: `fs`: use async API // TODO: `fs`: use async API
@injectable() @injectable()
export class SketchesServiceImpl implements SketchesService, BackendApplicationContribution { export class SketchesServiceImpl implements SketchesService, BackendApplicationContribution {
@ -205,6 +207,22 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
} }
} }
async cloneExample(uri: string): Promise<Sketch> {
const sketch = await this.loadSketch(uri);
const parentPath = await new Promise<string>((resolve, reject) => {
this.temp.mkdir({ prefix }, (err, dirPath) => {
if (err) {
reject(err);
return;
}
resolve(dirPath);
})
});
const destinationUri = FileUri.create(path.join(parentPath, sketch.name)).toString();
const copiedSketchUri = await this.copy(sketch, { destinationUri });
return this.loadSketch(copiedSketchUri);
}
protected async simpleLocalWalk( protected async simpleLocalWalk(
root: string, root: string,
maxDepth: number, maxDepth: number,
@ -258,15 +276,15 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
async createNewSketch(): Promise<Sketch> { async createNewSketch(): Promise<Sketch> {
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const today = new Date(); const today = new Date();
const parent = await new Promise<string>((resolve, reject) => { const parentPath = await new Promise<string>((resolve, reject) => {
this.temp.mkdir({ prefix: '.arduinoProIDE-unsaved' }, (err, dirPath) => { this.temp.mkdir({ prefix }, (err, dirPath) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
} }
resolve(dirPath); resolve(dirPath);
}) });
}) });
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`; const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
const config = await this.configService.getConfiguration(); const config = await this.configService.getConfiguration();
const user = FileUri.fsPath(config.sketchDirUri); const user = FileUri.fsPath(config.sketchDirUri);
@ -286,7 +304,7 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
throw new Error('Cannot create a unique sketch name'); throw new Error('Cannot create a unique sketch name');
} }
const sketchDir = path.join(parent, sketchName) const sketchDir = path.join(parentPath, sketchName)
const sketchFile = path.join(sketchDir, `${sketchName}.ino`); const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
await fs.mkdirp(sketchDir); await fs.mkdirp(sketchDir);
await fs.writeFile(sketchFile, `void setup() { await fs.writeFile(sketchFile, `void setup() {
@ -346,7 +364,7 @@ void loop() {
temp = firstToLowerCase(temp); temp = firstToLowerCase(temp);
} }
} }
return sketchPath.indexOf('.arduinoProIDE-unsaved') !== -1 && sketchPath.startsWith(temp); return sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(temp);
} }
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> { async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {