mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-10 04:46:33 +00:00
ATL-302: Added built-in examples to the app.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
b5d7c3b45d
commit
1c9fcd0cdf
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ node_modules/
|
||||
lib/
|
||||
downloads/
|
||||
build/
|
||||
Examples/
|
||||
!electron/build/
|
||||
src-gen/
|
||||
*webpack.config.js
|
||||
|
@ -4,10 +4,11 @@
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
"download-cli": "node ./scripts/download-cli.js",
|
||||
"download-ls": "node ./scripts/download-ls.js",
|
||||
"download-examples": "node ./scripts/download-examples.js",
|
||||
"generate-protocol": "node ./scripts/generate-protocol.js",
|
||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
@ -99,7 +100,8 @@
|
||||
"lib",
|
||||
"src",
|
||||
"build",
|
||||
"data"
|
||||
"data",
|
||||
"examples"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
|
23
arduino-ide-extension/scripts/download-examples.js
Normal file
23
arduino-ide-extension/scripts/download-examples.js
Normal 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);
|
||||
|
||||
})();
|
@ -12,7 +12,7 @@ import { ArduinoLanguageClientContribution } from './language/arduino-language-c
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-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 { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
||||
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 } from './theia/output/output-widget';
|
||||
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');
|
||||
|
||||
@ -151,7 +156,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// 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
|
||||
bind(LibraryListWidget).toSelf();
|
||||
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
||||
@ -347,6 +355,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// File-system extension
|
||||
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, OpenSketch);
|
||||
Contribution.configure(bind, CloseSketch);
|
||||
@ -360,4 +371,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, SketchControl);
|
||||
Contribution.configure(bind, Settings);
|
||||
Contribution.configure(bind, BurnBootloader);
|
||||
Contribution.configure(bind, Examples);
|
||||
Contribution.configure(bind, IncludeLibrary);
|
||||
});
|
||||
|
@ -27,13 +27,13 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
|
||||
onStart(): void {
|
||||
this.boardsServiceClient.onBoardsPackageInstalled(async ({ pkg }) => {
|
||||
const { installedVersion: version } = pkg;
|
||||
this.boardsServiceClient.onBoardsPackageInstalled(async ({ item }) => {
|
||||
const { installedVersion: version } = item;
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
|
||||
if (!data || !data.length) {
|
||||
|
@ -1,11 +1,20 @@
|
||||
import { injectable, inject, optional } from 'inversify';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { StorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, 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 { naturalCompare } from '../../common/utils';
|
||||
import { compareAnything } from '../theia/monaco/comparers';
|
||||
@ -21,15 +30,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@optional()
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(StorageService)
|
||||
protected storageService: StorageService;
|
||||
|
||||
protected readonly onBoardsPackageInstalledEmitter = new Emitter<BoardInstalledEvent>();
|
||||
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
|
||||
protected readonly onBoardsPackageInstalledEmitter = new Emitter<InstalledEvent<BoardsPackage>>();
|
||||
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<UninstalledEvent<BoardsPackage>>();
|
||||
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
|
||||
@ -119,13 +127,13 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
return false;
|
||||
}
|
||||
|
||||
notifyBoardInstalled(event: BoardInstalledEvent): void {
|
||||
this.logger.info('Board installed: ', JSON.stringify(event));
|
||||
notifyInstalled(event: InstalledEvent<BoardsPackage>): void {
|
||||
this.logger.info('Boards package installed: ', JSON.stringify(event));
|
||||
this.onBoardsPackageInstalledEmitter.fire(event);
|
||||
const { selectedBoard } = this.boardsConfig;
|
||||
const { installedVersion, id } = event.pkg;
|
||||
const { installedVersion, id } = event.item;
|
||||
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)) {
|
||||
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
|
||||
this.boardsConfig = {
|
||||
@ -136,14 +144,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
}
|
||||
}
|
||||
|
||||
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
|
||||
this.logger.info('Board uninstalled: ', JSON.stringify(event));
|
||||
notifyUninstalled(event: UninstalledEvent<BoardsPackage>): void {
|
||||
this.logger.info('Boards package uninstalled: ', JSON.stringify(event));
|
||||
this.onBoardsPackageUninstalledEmitter.fire(event);
|
||||
const { selectedBoard } = this.boardsConfig;
|
||||
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) {
|
||||
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 = {
|
||||
name: selectedBoard.name
|
||||
// No FQBN
|
||||
@ -219,7 +227,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
}
|
||||
|
||||
if (!config.selectedBoard) {
|
||||
if (!options.silent && this.messageService) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn('No boards selected.', { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
@ -241,14 +249,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
|
||||
const { name } = config.selectedBoard;
|
||||
if (!config.selectedPort) {
|
||||
if (!options.silent && this.messageService) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard.fqbn) {
|
||||
if (!options.silent && this.messageService) {
|
||||
if (!options.silent) {
|
||||
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
|
@ -2,6 +2,7 @@ import { inject, injectable, interfaces } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
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 { MessageService } from '@theia/core/lib/common/message-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 { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
|
||||
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 };
|
||||
|
||||
@injectable()
|
||||
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution {
|
||||
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
@ -37,6 +39,9 @@ export abstract class Contribution implements CommandContribution, MenuContribut
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
}
|
||||
|
||||
@ -75,11 +80,12 @@ export abstract class SketchContribution extends 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(CommandContribution).toService(serviceIdentifier);
|
||||
bind(MenuContribution).toService(serviceIdentifier);
|
||||
bind(KeybindingContribution).toService(serviceIdentifier);
|
||||
bind(TabBarToolbarContribution).toService(serviceIdentifier);
|
||||
bind(FrontendApplicationContribution).toService(serviceIdentifier);
|
||||
}
|
||||
}
|
||||
|
74
arduino-ide-extension/src/browser/contributions/examples.ts
Normal file
74
arduino-ide-extension/src/browser/contributions/examples.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { Examples } from './examples';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
@ -16,6 +18,12 @@ export class OpenSketch extends SketchContribution {
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(Examples)
|
||||
protected readonly examples: Examples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
@ -53,6 +61,14 @@ export class OpenSketch extends SketchContribution {
|
||||
});
|
||||
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 = {
|
||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||
anchor: {
|
||||
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
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 { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { LibraryServiceProvider } from './library-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidget extends ListWidget<Library> {
|
||||
export class LibraryListWidget extends ListWidget<LibraryPackage> {
|
||||
|
||||
static WIDGET_ID = 'library-list-widget';
|
||||
static WIDGET_LABEL = 'Library Manager';
|
||||
|
||||
constructor(
|
||||
@inject(LibraryService) protected service: LibraryService,
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
|
||||
@inject(LibraryServiceProvider) protected service: LibraryServiceProvider,
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<LibraryPackage>) {
|
||||
|
||||
super({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
@ -19,7 +20,7 @@ export class LibraryListWidget extends ListWidget<Library> {
|
||||
iconClass: 'library-tab-icon',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: Library) => item.name,
|
||||
itemLabel: (item: LibraryPackage) => item.name,
|
||||
itemRenderer
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -12,3 +12,15 @@ export interface ArduinoComponent {
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os';
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { naturalCompare } from './../utils';
|
||||
import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { Installable, InstallableClient } from './installable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
|
||||
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 interface BoardsServiceClient {
|
||||
export interface BoardsServiceClient extends InstallableClient<BoardsPackage> {
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
|
||||
notifyBoardInstalled(event: BoardInstalledEvent): void
|
||||
notifyBoardUninstalled(event: BoardUninstalledEvent): void
|
||||
}
|
||||
|
||||
export const BoardsServicePath = '/services/boards-service';
|
||||
@ -194,6 +184,11 @@ export interface BoardsPackage extends ArduinoComponent {
|
||||
readonly id: string;
|
||||
readonly boards: Board[];
|
||||
}
|
||||
export namespace BoardsPackage {
|
||||
export function equals(left: BoardsPackage, right: BoardsPackage): boolean {
|
||||
return left.id === right.id;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Board {
|
||||
readonly name: string;
|
||||
|
@ -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[];
|
||||
}
|
@ -1,6 +1,17 @@
|
||||
import { naturalCompare } from './../utils';
|
||||
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> {
|
||||
/**
|
||||
* If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`.
|
||||
|
@ -1,13 +1,46 @@
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
import { Installable, InstallableClient } from './installable';
|
||||
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
export interface LibraryService extends Installable<Library>, Searchable<Library> {
|
||||
install(options: { item: Library, version?: Installable.Version }): Promise<void>;
|
||||
export interface LibraryService extends Installable<LibraryPackage>, Searchable<LibraryPackage> {
|
||||
install(options: { item: LibraryPackage, version?: Installable.Version }): Promise<void>;
|
||||
list(options: LibraryService.List.Options): Promise<LibraryPackage[]>;
|
||||
}
|
||||
|
||||
export interface Library extends ArduinoComponent {
|
||||
readonly builtIn?: boolean;
|
||||
export const LibraryServiceClient = Symbol('LibraryServiceClient');
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,11 @@ export interface SketchesService {
|
||||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
|
@ -7,3 +7,7 @@ export function notEmpty(arg: string | undefined | null): arg is string {
|
||||
export function firstToLowerCase(what: string): string {
|
||||
return what.charAt(0).toLowerCase() + what.slice(1);
|
||||
}
|
||||
|
||||
export function firstToUpperCase(what: string): string {
|
||||
return what.charAt(0).toUpperCase() + what.slice(1);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import { LanguageServerContribution } from '@theia/languages/lib/node';
|
||||
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 { LibraryServiceImpl } from './library-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 { NodeFileSystemExt } from './node-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) => {
|
||||
rebind(EnvVariablesServer).to(ArduinoEnvVariablesServer).inSingletonScope();
|
||||
@ -66,25 +68,31 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
})
|
||||
).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
|
||||
bind(ArduinoLanguageServerContribution).toSelf().inSingletonScope();
|
||||
bind(LanguageServerContribution).toService(ArduinoLanguageServerContribution);
|
||||
|
||||
// Library service
|
||||
const libraryServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(LibraryServiceImpl).toSelf().inSingletonScope();
|
||||
bind(LibraryService).toService(LibraryServiceImpl);
|
||||
bindBackendService(LibraryServicePath, LibraryService);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(libraryServiceConnectionModule);
|
||||
bind(LibraryServiceImpl).toSelf().inSingletonScope();
|
||||
bind(LibraryServiceServer).toService(LibraryServiceImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context =>
|
||||
new JsonRpcConnectionHandler<LibraryServiceClient>(LibraryServiceServerPath, client => {
|
||||
const server = context.container.get<LibraryServiceImpl>(LibraryServiceImpl);
|
||||
server.setClient(client);
|
||||
client.onDidCloseConnection(() => server.dispose());
|
||||
return server;
|
||||
})
|
||||
).inSingletonScope();
|
||||
|
||||
// Sketches service
|
||||
const sketchesServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(SketchesServiceImpl).toSelf().inSingletonScope();
|
||||
bind(SketchesService).toService(SketchesServiceImpl);
|
||||
bindBackendService(SketchesServicePath, SketchesService);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule);
|
||||
// Shred sketches service
|
||||
bind(SketchesServiceImpl).toSelf().inSingletonScope();
|
||||
bind(SketchesService).toService(SketchesServiceImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
|
||||
|
||||
// Boards service
|
||||
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
|
||||
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();
|
||||
});
|
||||
|
@ -112,8 +112,8 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
if (this.discoveryTimer !== undefined) {
|
||||
clearInterval(this.discoveryTimer);
|
||||
}
|
||||
this.logger.info('<<< Disposed boards service.');
|
||||
this.client = undefined;
|
||||
this.logger.info('<<< Disposed boards service.');
|
||||
}
|
||||
|
||||
async getAttachedBoards(): Promise<Board[]> {
|
||||
@ -370,15 +370,15 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
}
|
||||
|
||||
async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> {
|
||||
const pkg = options.item;
|
||||
const version = !!options.version ? options.version : pkg.availableVersions[0];
|
||||
const item = options.item;
|
||||
const version = !!options.version ? options.version : item.availableVersions[0];
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
return;
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
const [platform, architecture] = pkg.id.split(':');
|
||||
const [platform, architecture] = item.id.split(':');
|
||||
|
||||
const req = new PlatformInstallReq();
|
||||
req.setInstance(instance);
|
||||
@ -386,7 +386,7 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
req.setPlatformPackage(platform);
|
||||
req.setVersion(version);
|
||||
|
||||
console.info('Starting board installation', pkg);
|
||||
console.info('>>> Starting boards package installation...', item);
|
||||
const resp = client.platformInstall(req);
|
||||
resp.on('data', (r: PlatformInstallResp) => {
|
||||
const prog = r.getProgress();
|
||||
@ -399,34 +399,34 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
resp.on('error', reject);
|
||||
});
|
||||
if (this.client) {
|
||||
const packages = await this.search({});
|
||||
const updatedPackage = packages.find(({ id }) => id === pkg.id) || pkg;
|
||||
this.client.notifyBoardInstalled({ pkg: updatedPackage });
|
||||
const items = await this.search({});
|
||||
const updated = items.find(other => BoardsPackage.equals(other, item)) || item;
|
||||
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> {
|
||||
const pkg = options.item;
|
||||
const item = options.item;
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
return;
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
const [platform, architecture] = pkg.id.split(':');
|
||||
const [platform, architecture] = item.id.split(':');
|
||||
|
||||
const req = new PlatformUninstallReq();
|
||||
req.setInstance(instance);
|
||||
req.setArchitecture(architecture);
|
||||
req.setPlatformPackage(platform);
|
||||
|
||||
console.info('Starting board uninstallation', pkg);
|
||||
console.info('>>> Starting boards package uninstallation...', item);
|
||||
let logged = false;
|
||||
const resp = client.platformUninstall(req);
|
||||
resp.on('data', (_: PlatformUninstallResp) => {
|
||||
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;
|
||||
}
|
||||
})
|
||||
@ -435,10 +435,10 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
resp.on('error', reject);
|
||||
});
|
||||
if (this.client) {
|
||||
// Here, unlike at `install` we send out the argument `pkg`. Otherwise, we would not know about the board FQBN.
|
||||
this.client.notifyBoardUninstalled({ pkg });
|
||||
// Here, unlike at `install` we send out the argument `item`. Otherwise, we would not know about the board FQBN.
|
||||
this.client.notifyUninstalled({ item });
|
||||
}
|
||||
console.info('Board uninstallation done', pkg);
|
||||
console.info('<<< Boards package uninstallation done.', item);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,11 +15,16 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
protected readonly toolOutputService: ToolOutputServiceServer;
|
||||
|
||||
protected readonly onIndexUpdatedEmitter = new Emitter<void>();
|
||||
protected readonly onClientReadyEmitter = new Emitter<void>();
|
||||
|
||||
get onIndexUpdated(): Event<void> {
|
||||
return this.onIndexUpdatedEmitter.event;
|
||||
}
|
||||
|
||||
get onClientReady(): Event<void> {
|
||||
return this.onClientReadyEmitter.event;
|
||||
}
|
||||
|
||||
close(client: CoreClientProvider.Client): void {
|
||||
client.client.close();
|
||||
}
|
||||
@ -28,10 +33,12 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
|
||||
if (port && port === this._port) {
|
||||
// No need to create a new gRPC client, but we have to update the indexes.
|
||||
if (this._client) {
|
||||
this.updateIndexes(this._client);
|
||||
await this.updateIndexes(this._client);
|
||||
this.onClientReadyEmitter.fire();
|
||||
}
|
||||
} else {
|
||||
return super.reconcileClient(port);
|
||||
await super.reconcileClient(port);
|
||||
this.onClientReadyEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
|
79
arduino-ide-extension/src/node/examples-service-impl.ts
Normal file
79
arduino-ide-extension/src/node/examples-service-impl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,7 @@ import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
export const constants = fs.constants;
|
||||
export type Stats = fs.Stats;
|
||||
|
||||
export const existsSync = fs.existsSync;
|
||||
export const lstatSync = fs.lstatSync;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Library, LibraryService } from '../common/protocol/library-service';
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { LibraryPackage, LibraryService, LibraryServiceClient } from '../common/protocol/library-service';
|
||||
import { CoreClientProvider } from './core-client-provider';
|
||||
import {
|
||||
LibrarySearchReq,
|
||||
@ -15,17 +15,37 @@ import {
|
||||
} from './cli-protocol/commands/lib_pb';
|
||||
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
|
||||
import { Installable } from '../common/protocol/installable';
|
||||
import { ILogger, notEmpty } from '@theia/core';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
|
||||
@injectable()
|
||||
export class LibraryServiceImpl implements LibraryService {
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(CoreClientProvider)
|
||||
protected readonly coreClientProvider: CoreClientProvider;
|
||||
|
||||
@inject(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();
|
||||
if (!coreClient) {
|
||||
return [];
|
||||
@ -71,9 +91,74 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
return items;
|
||||
}
|
||||
|
||||
async install(options: { item: Library, version?: Installable.Version }): Promise<void> {
|
||||
const library = options.item;
|
||||
const version = !!options.version ? options.version : library.availableVersions[0];
|
||||
async list({ fqbn }: { fqbn?: string | undefined }): Promise<LibraryPackage[]> {
|
||||
await this.ready.promise;
|
||||
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();
|
||||
if (!coreClient) {
|
||||
return;
|
||||
@ -82,9 +167,10 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
|
||||
const req = new LibraryInstallReq();
|
||||
req.setInstance(instance);
|
||||
req.setName(library.name);
|
||||
req.setName(item.name);
|
||||
req.setVersion(version);
|
||||
|
||||
console.info('>>> Starting library package installation...', item);
|
||||
const resp = client.libraryInstall(req);
|
||||
resp.on('data', (r: LibraryInstallResp) => {
|
||||
const prog = r.getProgress();
|
||||
@ -96,10 +182,18 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
resp.on('end', resolve);
|
||||
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> {
|
||||
const library = options.item;
|
||||
async uninstall(options: { item: LibraryPackage }): Promise<void> {
|
||||
const item = options.item;
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
return;
|
||||
@ -108,14 +202,15 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
|
||||
const req = new LibraryUninstallReq();
|
||||
req.setInstance(instance);
|
||||
req.setName(library.name);
|
||||
req.setVersion(library.installedVersion!);
|
||||
req.setName(item.name);
|
||||
req.setVersion(item.installedVersion!);
|
||||
|
||||
console.info('>>> Starting library package uninstallation...', item);
|
||||
let logged = false;
|
||||
const resp = client.libraryUninstall(req);
|
||||
resp.on('data', (_: LibraryUninstallResp) => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -123,11 +218,25 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
resp.on('end', resolve);
|
||||
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 {
|
||||
name: '',
|
||||
installable: false,
|
||||
@ -135,6 +244,7 @@ function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVers
|
||||
|
||||
author: release.getAuthor(),
|
||||
availableVersions,
|
||||
includes: release.getProvidesIncludesList(),
|
||||
description: release.getSentence(),
|
||||
moreInfoLink: release.getWebsite(),
|
||||
summary: release.getParagraph()
|
||||
|
@ -19,6 +19,8 @@ const MAX_FILESYSTEM_DEPTH = 40;
|
||||
|
||||
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
|
||||
|
||||
const prefix = '.arduinoProIDE-unsaved';
|
||||
|
||||
// TODO: `fs`: use async API
|
||||
@injectable()
|
||||
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(
|
||||
root: string,
|
||||
maxDepth: number,
|
||||
@ -258,15 +276,15 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
|
||||
async createNewSketch(): Promise<Sketch> {
|
||||
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
||||
const today = new Date();
|
||||
const parent = await new Promise<string>((resolve, reject) => {
|
||||
this.temp.mkdir({ prefix: '.arduinoProIDE-unsaved' }, (err, dirPath) => {
|
||||
const parentPath = await new Promise<string>((resolve, reject) => {
|
||||
this.temp.mkdir({ prefix }, (err, dirPath) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(dirPath);
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
|
||||
const config = await this.configService.getConfiguration();
|
||||
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');
|
||||
}
|
||||
|
||||
const sketchDir = path.join(parent, sketchName)
|
||||
const sketchDir = path.join(parentPath, sketchName)
|
||||
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
|
||||
await fs.mkdirp(sketchDir);
|
||||
await fs.writeFile(sketchFile, `void setup() {
|
||||
@ -346,7 +364,7 @@ void loop() {
|
||||
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> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user