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 7c72a3c4..96efce4e 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -142,6 +142,7 @@ import { AddFile } from './contributions/add-file'; import { ArchiveSketch } from './contributions/archive-sketch'; import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution'; import { OutputToolbarContribution } from './theia/output/output-toolbar-contribution'; +import { AddZipLibrary } from './contributions/add-zip-library'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -354,6 +355,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, Help); Contribution.configure(bind, AddFile); Contribution.configure(bind, ArchiveSketch); + Contribution.configure(bind, AddZipLibrary); bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => { WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService); diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts new file mode 100644 index 00000000..508a02d4 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -0,0 +1,73 @@ +import { inject, injectable } from 'inversify'; +import { remote } from 'electron'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import URI from '@theia/core/lib/common/uri'; +import { InstallationProgressDialog } from '../widgets/progress-dialog'; +import { LibraryService } from '../../common/protocol'; + +@injectable() +export class AddZipLibrary extends SketchContribution { + + @inject(EnvVariablesServer) + protected readonly envVariableServer: EnvVariablesServer; + + @inject(LibraryService) + protected readonly libraryService: LibraryService; + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, { + execute: () => this.addZipLibrary() + }); + } + + registerMenus(registry: MenuModelRegistry): void { + const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include']; + // TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt. + registry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' }); + registry.registerMenuAction([...includeLibMenuPath, '1_install'], { + commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id, + label: 'Add .ZIP Library...', + order: '1' + }); + } + + async addZipLibrary(): Promise { + const homeUri = await this.envVariableServer.getHomeDirUri(); + const defaultPath = await this.fileService.fsPath(new URI(homeUri)); + const { canceled, filePaths } = await remote.dialog.showOpenDialog({ + title: "Select a zip file containing the library you'd like to add", + defaultPath, + properties: ['openFile'], + filters: [ + { + name: 'Library', + extensions: ['zip'] + } + ] + }); + if (!canceled && filePaths.length) { + const zipUri = await this.fileSystemExt.getUri(filePaths[0]); + const dialog = new InstallationProgressDialog('Installing library', 'zip'); + try { + this.outputChannelManager.getChannel('Arduino').clear(); + dialog.open(); + await this.libraryService.installZip({ zipUri }); + } catch (e) { + this.messageService.error(e.toString()); + } finally { + dialog.close(); + } + } + } + +} + +export namespace AddZipLibrary { + export namespace Commands { + export const ADD_ZIP_LIBRARY: Command = { + id: 'arduino-add-zip-library' + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/contribution.ts b/arduino-ide-extension/src/browser/contributions/contribution.ts index 06d3335a..af4a5d47 100644 --- a/arduino-ide-extension/src/browser/contributions/contribution.ts +++ b/arduino-ide-extension/src/browser/contributions/contribution.ts @@ -9,6 +9,7 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { MessageService } from '@theia/core/lib/common/message-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; +import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu'; import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; @@ -90,6 +91,9 @@ export abstract class SketchContribution extends Contribution { @inject(EditorManager) protected readonly editorManager: EditorManager; + @inject(OutputChannelManager) + protected readonly outputChannelManager: OutputChannelManager; + protected async sourceOverride(): Promise> { const override: Record = {}; const sketch = await this.sketchServiceClient.currentSketch(); diff --git a/arduino-ide-extension/src/browser/contributions/include-library.ts b/arduino-ide-extension/src/browser/contributions/include-library.ts index 079e6b2a..df42e3f5 100644 --- a/arduino-ide-extension/src/browser/contributions/include-library.ts +++ b/arduino-ide-extension/src/browser/contributions/include-library.ts @@ -47,6 +47,17 @@ export class IncludeLibrary extends SketchContribution { this.notificationCenter.onLibraryUninstalled(() => this.updateMenuActions()); } + registerMenus(registry: MenuModelRegistry): void { + // `Include Library` submenu + const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include']; + registry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' }); + // `Manage Libraries...` group. + registry.registerMenuAction([...includeLibMenuPath, '0_manage'], { + commandId: `${LibraryListWidget.WIDGET_ID}:toggle`, + label: 'Manage Libraries...' + }); + } + registerCommands(registry: CommandRegistry): void { registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, { execute: async arg => { @@ -68,16 +79,7 @@ export class IncludeLibrary extends SketchContribution { libraries.push(...await this.libraryService.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 diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index fdd85f94..4f0109aa 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -1,5 +1,4 @@ import { inject, injectable } from 'inversify'; -import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; @@ -23,9 +22,6 @@ export class UploadSketch extends SketchContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClientImpl: BoardsServiceProvider; - @inject(OutputChannelManager) - protected readonly outputChannelManager: OutputChannelManager; - registerCommands(registry: CommandRegistry): void { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { execute: () => this.uploadSketch() diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index 7522d594..fe9b3e07 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -1,5 +1,4 @@ import { inject, injectable } from 'inversify'; -import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; @@ -19,9 +18,6 @@ export class VerifySketch extends SketchContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClientImpl: BoardsServiceProvider; - @inject(OutputChannelManager) - protected readonly outputChannelManager: OutputChannelManager; - registerCommands(registry: CommandRegistry): void { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { execute: () => this.verifySketch() diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index 0e37c52e..aaf5444f 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -10,6 +10,7 @@ export interface LibraryService extends Installable, Searchable< * When `installDependencies` is not set, it is `true` by default. If you want to skip the installation of required dependencies, set it to `false`. */ install(options: { item: LibraryPackage, version?: Installable.Version, installDependencies?: boolean }): Promise; + installZip(options: { zipUri: string }): Promise; /** * Set `filterSelf` to `true` if you want to avoid having `item` in the result set. * Note: as of today (22.02.2021), the CLI works like this: `./arduino-cli lib deps Adaino@0.1.0 ✕ Adaino 0.1.0 must be installed.`. diff --git a/arduino-ide-extension/src/node/library-service-server-impl.ts b/arduino-ide-extension/src/node/library-service-server-impl.ts index bdafaf9f..da8a6f53 100644 --- a/arduino-ide-extension/src/node/library-service-server-impl.ts +++ b/arduino-ide-extension/src/node/library-service-server-impl.ts @@ -1,4 +1,5 @@ import { injectable, inject } from 'inversify'; +import * as path from 'path'; import { LibraryDependency, LibraryPackage, LibraryService } from '../common/protocol/library-service'; import { CoreClientAware } from './core-client-provider'; import { @@ -13,13 +14,16 @@ import { LibraryUninstallReq, LibraryUninstallResp, Library, - LibraryResolveDependenciesReq + LibraryResolveDependenciesReq, + ZipLibraryInstallReq, + ZipLibraryInstallResp } from './cli-protocol/commands/lib_pb'; import { Installable } from '../common/protocol/installable'; import { ILogger, notEmpty } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; import { OutputService, NotificationServiceServer } from '../common/protocol'; + @injectable() export class LibraryServiceImpl extends CoreClientAware implements LibraryService { @@ -188,6 +192,37 @@ export class LibraryServiceImpl extends CoreClientAware implements LibraryServic console.info('<<< Library package installation done.', item); } + async installZip({ zipUri }: { zipUri: string }): Promise { + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; + const req = new ZipLibraryInstallReq(); + req.setPath(FileUri.fsPath(zipUri)); + req.setInstance(instance); + const resp = client.zipLibraryInstall(req); + resp.on('data', (r: ZipLibraryInstallResp) => { + const task = r.getTaskProgress(); + if (task && task.getMessage()) { + this.outputService.append({ chunk: task.getMessage() }); + } + }); + await new Promise((resolve, reject) => { + resp.on('end', resolve); + resp.on('error', error => { + // This is a hack to have better error messages for the user. We try to get the name of the library from this: + // Request installZip failed with error: 2 UNKNOWN: copying library: destination /path/to/lib already exists + const match = error.message.match(/destination (.*?) already exists/); + if (match && match.length >= 2) { + const name = path.basename(match[1].trim()); + if (name) { + reject(new Error(`A library named ${name} already exists.`)); + return; + } + } + reject(error); + }); + }); + } + async uninstall(options: { item: LibraryPackage }): Promise { const item = options.item; const coreClient = await this.coreClient();