From 1c9fcd0cdfaee24c67846fe75f7e03d374cf3a8d Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 13 Aug 2020 13:34:56 +0200 Subject: [PATCH] ATL-302: Added built-in examples to the app. Signed-off-by: Akos Kitta --- .gitignore | 1 + arduino-ide-extension/package.json | 6 +- .../scripts/download-examples.js | 23 +++ .../browser/arduino-ide-frontend-module.ts | 17 ++- .../src/browser/boards/boards-data-store.ts | 6 +- .../boards/boards-service-client-impl.ts | 40 +++--- .../src/browser/contributions/contribution.ts | 10 +- .../src/browser/contributions/examples.ts | 74 ++++++++++ .../browser/contributions/include-library.ts | 36 +++++ .../src/browser/contributions/open-sketch.ts | 16 +++ .../library/include-library-menu-updater.ts | 90 ++++++++++++ .../browser/library/library-list-widget.ts | 11 +- .../library/library-service-provider.ts | 61 ++++++++ .../src/common/protocol/arduino-component.ts | 12 ++ .../src/common/protocol/boards-service.ts | 19 +-- .../src/common/protocol/examples-service.ts | 13 ++ .../src/common/protocol/installable.ts | 11 ++ .../src/common/protocol/library-service.ts | 47 +++++- .../src/common/protocol/sketches-service.ts | 5 + arduino-ide-extension/src/common/utils.ts | 4 + .../src/node/arduino-ide-backend-module.ts | 38 +++-- .../src/node/boards-service-impl.ts | 32 ++--- .../src/node/core-client-provider.ts | 11 +- .../src/node/examples-service-impl.ts | 79 ++++++++++ arduino-ide-extension/src/node/fs-extra.ts | 1 + .../src/node/library-service-impl.ts | 136 ++++++++++++++++-- .../src/node/sketches-service-impl.ts | 30 +++- 27 files changed, 728 insertions(+), 101 deletions(-) create mode 100644 arduino-ide-extension/scripts/download-examples.js create mode 100644 arduino-ide-extension/src/browser/contributions/examples.ts create mode 100644 arduino-ide-extension/src/browser/contributions/include-library.ts create mode 100644 arduino-ide-extension/src/browser/library/include-library-menu-updater.ts create mode 100644 arduino-ide-extension/src/browser/library/library-service-provider.ts create mode 100644 arduino-ide-extension/src/common/protocol/examples-service.ts create mode 100644 arduino-ide-extension/src/node/examples-service-impl.ts diff --git a/.gitignore b/.gitignore index 0f3b9a37..afa5a5d4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ lib/ downloads/ build/ +Examples/ !electron/build/ src-gen/ *webpack.config.js diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 534215f4..e353cde5 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -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": [ { diff --git a/arduino-ide-extension/scripts/download-examples.js b/arduino-ide-extension/scripts/download-examples.js new file mode 100644 index 00000000..b58877ad --- /dev/null +++ b/arduino-ide-extension/scripts/download-examples.js @@ -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); + +})(); diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index d660d7af..2e4f1b87 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -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); }); diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index 155a2169..47163355 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -27,13 +27,13 @@ export class BoardsDataStore implements FrontendApplicationContribution { protected readonly onChangedEmitter = new Emitter(); 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(key); if (!data || !data.length) { diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts index e565fd92..679c1270 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -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(); - protected readonly onBoardsPackageUninstalledEmitter = new Emitter(); + protected readonly onBoardsPackageInstalledEmitter = new Emitter>(); + protected readonly onBoardsPackageUninstalledEmitter = new Emitter>(); protected readonly onAttachedBoardsChangedEmitter = new Emitter(); protected readonly onBoardsConfigChangedEmitter = new Emitter(); protected readonly onAvailableBoardsChangedEmitter = new Emitter(); @@ -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): 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): 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; diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 39ac0950..b247798e 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -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 { + } + registerCommands(registry: CommandRegistry): void { } @@ -75,11 +80,12 @@ export abstract class SketchContribution extends Contribution { } export namespace Contribution { - export function configure(bind: interfaces.Bind, serviceIdentifier: interfaces.ServiceIdentifier): void { + export function configure(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); } } diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts new file mode 100644 index 00000000..e294a889 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -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))); + } + } + +} diff --git a/arduino-ide-extension/src/browser/contributions/include-library.ts b/arduino-ide-extension/src/browser/contributions/include-library.ts new file mode 100644 index 00000000..d4bdbc94 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/include-library.ts @@ -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 { + // 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' + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-sketch.ts index 7d5b378a..f6d4fe9b 100644 --- a/arduino-ide-extension/src/browser/contributions/open-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-sketch.ts @@ -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: { diff --git a/arduino-ide-extension/src/browser/library/include-library-menu-updater.ts b/arduino-ide-extension/src/browser/library/include-library-menu-updater.ts new file mode 100644 index 00000000..74a92e41 --- /dev/null +++ b/arduino-ide-extension/src/browser/library/include-library-menu-updater.ts @@ -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 { + this.updateMenuActions(); + this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions()) + this.libraryServiceProvider.onLibraryPackageInstalled(() => this.updateMenuActions()); + this.libraryServiceProvider.onLibraryPackageUninstalled(() => this.updateMenuActions()); + } + + protected async updateMenuActions(): Promise { + 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)), + ); + } + +} diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index bb7f03c8..0a6b5851 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -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 { +export class LibraryListWidget extends ListWidget { static WIDGET_ID = 'library-list-widget'; static WIDGET_LABEL = 'Library Manager'; constructor( - @inject(LibraryService) protected service: LibraryService, - @inject(ListItemRenderer) protected itemRenderer: ListItemRenderer) { + @inject(LibraryServiceProvider) protected service: LibraryServiceProvider, + @inject(ListItemRenderer) protected itemRenderer: ListItemRenderer) { super({ id: LibraryListWidget.WIDGET_ID, @@ -19,7 +20,7 @@ export class LibraryListWidget extends ListWidget { iconClass: 'library-tab-icon', searchable: service, installable: service, - itemLabel: (item: Library) => item.name, + itemLabel: (item: LibraryPackage) => item.name, itemRenderer }); } diff --git a/arduino-ide-extension/src/browser/library/library-service-provider.ts b/arduino-ide-extension/src/browser/library/library-service-provider.ts new file mode 100644 index 00000000..c3e918c6 --- /dev/null +++ b/arduino-ide-extension/src/browser/library/library-service-provider.ts @@ -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 { + + @inject(LibraryServiceServer) + protected readonly server: JsonRpcProxy; + + protected readonly onLibraryPackageInstalledEmitter = new Emitter>(); + protected readonly onLibraryPackageUninstalledEmitter = new Emitter>(); + 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> { + return this.onLibraryPackageInstalledEmitter.event; + } + + get onLibraryPackageUninstalled(): Event> { + return this.onLibraryPackageUninstalledEmitter.event; + } + + // #region remote library service API + + async install(options: { item: LibraryPackage; version?: string | undefined; }): Promise { + return this.server.install(options); + } + + async list(options: LibraryService.List.Options): Promise { + return this.server.list(options); + } + + async uninstall(options: { item: LibraryPackage; }): Promise { + return this.server.uninstall(options); + } + + async search(options: Searchable.Options): Promise { + return this.server.search(options); + } + + // #endregion remote API + + dispose(): void { + this.toDispose.dispose(); + } + +} diff --git a/arduino-ide-extension/src/common/protocol/arduino-component.ts b/arduino-ide-extension/src/common/protocol/arduino-component.ts index ca6b37f1..583d061d 100644 --- a/arduino-ide-extension/src/common/protocol/arduino-component.ts +++ b/arduino-ide-extension/src/common/protocol/arduino-component.ts @@ -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'; + } + +} diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 0abbe29f..4d787284 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -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; -} - -export interface BoardUninstalledEvent { - readonly pkg: Readonly; -} - export const BoardsServiceClient = Symbol('BoardsServiceClient'); -export interface BoardsServiceClient { +export interface BoardsServiceClient extends InstallableClient { 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; diff --git a/arduino-ide-extension/src/common/protocol/examples-service.ts b/arduino-ide-extension/src/common/protocol/examples-service.ts new file mode 100644 index 00000000..dfc7ae9f --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/examples-service.ts @@ -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; +} + +export interface ExampleContainer { + readonly label: string; + readonly children: ExampleContainer[]; + readonly sketches: Sketch[]; +} diff --git a/arduino-ide-extension/src/common/protocol/installable.ts b/arduino-ide-extension/src/common/protocol/installable.ts index 4bf90759..c7f3dee8 100644 --- a/arduino-ide-extension/src/common/protocol/installable.ts +++ b/arduino-ide-extension/src/common/protocol/installable.ts @@ -1,6 +1,17 @@ import { naturalCompare } from './../utils'; import { ArduinoComponent } from './arduino-component'; +export interface InstalledEvent { + readonly item: Readonly; +} +export interface UninstalledEvent { + readonly item: Readonly; +} +export interface InstallableClient { + notifyInstalled(event: InstalledEvent): void + notifyUninstalled(event: UninstalledEvent): void +} + export interface Installable { /** * If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`. diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index a89705b4..69a0a849 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -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, Searchable { - install(options: { item: Library, version?: Installable.Version }): Promise; +export interface LibraryService extends Installable, Searchable { + install(options: { item: LibraryPackage, version?: Installable.Version }): Promise; + list(options: LibraryService.List.Options): Promise; } -export interface Library extends ArduinoComponent { - readonly builtIn?: boolean; +export const LibraryServiceClient = Symbol('LibraryServiceClient'); +export interface LibraryServiceClient extends InstallableClient { +} + +export const LibraryServiceServerPath = '/services/library-service-server'; +export const LibraryServiceServer = Symbol('LibraryServiceServer'); +export interface LibraryServiceServer extends LibraryService, JsonRpcServer { +} + +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 ` 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; + } + } diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 0631bf19..a045f646 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -22,6 +22,11 @@ export interface SketchesService { */ createNewSketch(): Promise; + /** + * Creates a new sketch with existing content. Rejects if `uri` is not pointing to a valid sketch folder. + */ + cloneExample(uri: string): Promise; + isSketchFolder(uri: string): Promise; /** diff --git a/arduino-ide-extension/src/common/utils.ts b/arduino-ide-extension/src/common/utils.ts index 5e355dad..1162b7b2 100644 --- a/arduino-ide-extension/src/common/utils.ts +++ b/arduino-ide-extension/src/common/utils.ts @@ -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); +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 22f1f052..04e3e7cc 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -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(LibraryServiceServerPath, client => { + const server = context.container.get(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(); }); diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 717d32a1..7248ce6a 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -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 { @@ -370,15 +370,15 @@ export class BoardsServiceImpl implements BoardsService { } async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise { - 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 { - 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); } } diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index 64582fb1..65ca90ac 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -15,11 +15,16 @@ export class CoreClientProvider extends GrpcClientProvider(); + protected readonly onClientReadyEmitter = new Emitter(); get onIndexUpdated(): Event { return this.onIndexUpdatedEmitter.event; } + get onClientReady(): Event { + return this.onClientReadyEmitter.event; + } + close(client: CoreClientProvider.Client): void { client.client.close(); } @@ -28,10 +33,12 @@ export class CoreClientProvider extends GrpcClientProvider { + if (this._all) { + return this._all; + } + this._all = await this.load(); + return this._all; + } + + protected async load(path: string = join(__dirname, '..', '..', 'Examples')): Promise { + 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> { + const map = new Map(); + for (const path of paths) { + const stat = await fs.stat(path); + map.set(path, stat); + } + return map; + } + + protected async tryLoadSketch(path: string): Promise { + try { + const sketch = await this.sketchesService.loadSketch(FileUri.create(path).toString()); + return sketch; + } catch { + return undefined; + } + } + +} diff --git a/arduino-ide-extension/src/node/fs-extra.ts b/arduino-ide-extension/src/node/fs-extra.ts index dbbf8bcb..2893eace 100644 --- a/arduino-ide-extension/src/node/fs-extra.ts +++ b/arduino-ide-extension/src/node/fs-extra.ts @@ -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; diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index 8cf62698..644e8b49 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -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 { + protected ready = new Deferred(); + 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 { + 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 { - const library = options.item; - const version = !!options.version ? options.version : library.availableVersions[0]; + async list({ fqbn }: { fqbn?: string | undefined }): Promise { + 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((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 { + 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 { - const library = options.item; + async uninstall(options: { item: LibraryPackage }): Promise { + 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, release: LibraryRelease, availableVersions: string[]): Library { +function toLibrary(tpl: Partial, release: LibraryRelease, availableVersions: string[]): LibraryPackage { return { name: '', installable: false, @@ -135,6 +244,7 @@ function toLibrary(tpl: Partial, release: LibraryRelease, availableVers author: release.getAuthor(), availableVersions, + includes: release.getProvidesIncludesList(), description: release.getSentence(), moreInfoLink: release.getWebsite(), summary: release.getParagraph() diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 643f96ed..c33c623f 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -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 { + const sketch = await this.loadSketch(uri); + const parentPath = await new Promise((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 { const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const today = new Date(); - const parent = await new Promise((resolve, reject) => { - this.temp.mkdir({ prefix: '.arduinoProIDE-unsaved' }, (err, dirPath) => { + const parentPath = await new Promise((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 {