Skip to content

ATL-1064: Support for nested sketchbook structure #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 32 additions & 17 deletions arduino-ide-extension/src/browser/contributions/examples.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify';
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
import { MenuPath, CompositeMenuNode, SubMenuOptions } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
import { ExamplesService } from '../../common/protocol/examples-service';
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board } from '../../common/protocol';
import { Board, Sketch, SketchContainer } from '../../common/protocol';

@injectable()
export abstract class Examples extends SketchContribution {
Expand Down Expand Up @@ -59,18 +59,35 @@ export abstract class Examples extends SketchContribution {
}

registerRecursively(
exampleContainerOrPlaceholder: ExampleContainer | string,
sketchContainerOrPlaceholder: SketchContainer | (Sketch | SketchContainer)[] | string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection()): void {
pushToDispose: DisposableCollection = new DisposableCollection(),
subMenuOptions?: SubMenuOptions | undefined): void {

if (typeof exampleContainerOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
if (typeof sketchContainerOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, sketchContainerOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
} else {
const { label, sketches, children } = exampleContainerOrPlaceholder;
const submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label);
const sketches: Sketch[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;

if (SketchContainer.is(sketchContainerOrPlaceholder)) {
const { label } = sketchContainerOrPlaceholder;
submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
sketches.push(...sketchContainerOrPlaceholder.sketches);
children.push(...sketchContainerOrPlaceholder.children);
} else {
for (const sketchOrContainer of sketchContainerOrPlaceholder) {
if (SketchContainer.is(sketchOrContainer)) {
children.push(sketchOrContainer);
} else {
sketches.push(sketchOrContainer);
}
}
}
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
for (const sketch of sketches) {
const { uri } = sketch;
Expand Down Expand Up @@ -98,22 +115,20 @@ export class BuiltInExamples extends Examples {
this.register(); // no `await`
}

protected async register() {
let exampleContainers: ExampleContainer[] | undefined;
protected async register(): Promise<void> {
let sketchContainers: SketchContainer[] | undefined;
try {
exampleContainers = await this.examplesService.builtIns();
sketchContainers = await this.examplesService.builtIns();
} catch (e) {
console.error('Could not initialize built-in examples.', e);
this.messageService.error('Could not initialize built-in examples.');
return;
}
this.toDispose.dispose();
for (const container of ['Built-in examples', ...exampleContainers]) {
for (const container of ['Built-in examples', ...sketchContainers]) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
}
this.menuManager.update();
// TODO: remove
console.log(typeof this.menuRegistry);
}

}
Expand All @@ -136,7 +151,7 @@ export class LibraryExamples extends Examples {
this.register(board);
}

protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard): Promise<void> {
return this.queue.add(async () => {
this.toDispose.dispose();
if (!board || !board.fqbn) {
Expand Down
28 changes: 12 additions & 16 deletions arduino-ide-extension/src/browser/contributions/open-sketch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ 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 { BuiltInExamples } from './examples';
import { Sketchbook } from './sketchbook';
import { SketchContainer } from '../../common/protocol';

@injectable()
export class OpenSketch extends SketchContribution {
Expand All @@ -24,7 +26,10 @@ export class OpenSketch extends SketchContribution {
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;

protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
@inject(Sketchbook)
protected readonly sketchbook: Sketchbook;

protected readonly toDispose = new DisposableCollection();

registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
Expand All @@ -33,11 +38,11 @@ export class OpenSketch extends SketchContribution {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (_: Widget, target: EventTarget) => {
const sketches = await this.sketchService.getSketches();
if (!sketches.length) {
const container = await this.sketchService.getSketches({ exclude: ['**/hardware/**'] });
if (SketchContainer.isEmpty(container)) {
this.openSketch();
} else {
this.toDisposeBeforeCreateNewContextMenu.dispose();
this.toDispose.dispose();
if (!(target instanceof HTMLElement)) {
return;
}
Expand All @@ -50,21 +55,12 @@ export class OpenSketch extends SketchContribution {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...'
});
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
for (const sketch of sketches) {
const command = { id: `arduino-open-sketch--${sketch.uri}` };
const handler = { execute: () => this.openSketch(sketch) };
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, {
commandId: command.id,
label: sketch.name
});
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
this.sketchbook.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, this.toDispose);
try {
const containers = await this.examplesService.builtIns();
for (const container of containers) {
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu);
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDispose);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
Expand Down
51 changes: 14 additions & 37 deletions arduino-ide-extension/src/browser/contributions/sketchbook.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { inject, injectable } from 'inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
import { CommandRegistry, MenuModelRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center';
import { OpenSketch } from './open-sketch';
import { Examples } from './examples';
import { SketchContainer } from '../../common/protocol';

@injectable()
export class Sketchbook extends SketchContribution {
export class Sketchbook extends Examples {

@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
Expand All @@ -21,49 +21,26 @@ export class Sketchbook extends SketchContribution {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;

protected toDisposePerSketch = new Map<string, DisposableCollection>();

onStart(): void {
this.sketchService.getSketches().then(sketches => {
this.register(sketches);
this.sketchService.getSketches({}).then(container => {
this.register(container);
this.mainMenuManager.update();
});
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => {
this.unregister(removed);
this.register(created);
this.mainMenuManager.update();
this.sketchServiceClient.onSketchbookDidChange(() => {
this.sketchService.getSketches({}).then(container => {
this.register(container);
this.mainMenuManager.update();
});
});
}

registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
}

protected register(sketches: Sketch[]): void {
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposePerSketch.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-sketchbook-open--${uri}` };
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
));
}
}

protected unregister(sketches: Sketch[]): void {
for (const { uri } of sketches) {
const toDispose = this.toDisposePerSketch.get(uri);
if (toDispose) {
toDispose.dispose();
}
}
protected register(container: SketchContainer): void {
this.toDispose.dispose();
this.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, this.toDispose);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../../../common/protocol/config-service';
import { SketchesService, Sketch } from '../../../common/protocol/sketches-service';
import { SketchesService, Sketch, SketchContainer } from '../../../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from '../../arduino-workspace-resolver';

@injectable()
Expand Down Expand Up @@ -50,7 +50,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
const hash = window.location.hash;
const [recentWorkspaces, recentSketches] = await Promise.all([
this.server.getRecentWorkspaces(),
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
this.sketchService.getSketches({}).then(container => SketchContainer.toArray(container).map(s => s.uri))
]);
const toOpen = await new ArduinoWorkspaceRootResolver({
isValid: this.isValid.bind(this)
Expand Down
12 changes: 4 additions & 8 deletions arduino-ide-extension/src/common/protocol/examples-service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Sketch } from './sketches-service';
import { SketchContainer } from './sketches-service';

export const ExamplesServicePath = '/services/example-service';
export const ExamplesService = Symbol('ExamplesService');
export interface ExamplesService {
builtIns(): Promise<ExampleContainer[]>;
installed(options: { fqbn: string }): Promise<{ user: ExampleContainer[], current: ExampleContainer[], any: ExampleContainer[] }>;
builtIns(): Promise<SketchContainer[]>;
installed(options: { fqbn: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }>;
}

export interface ExampleContainer {
readonly label: string;
readonly children: ExampleContainer[];
readonly sketches: Sketch[];
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ConfigService } from './config-service';
import { DisposableCollection, Emitter } from '@theia/core';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { SketchContainer } from './sketches-service';

@injectable()
export class SketchesServiceClientImpl implements FrontendApplicationContribution {
Expand All @@ -35,9 +36,9 @@ export class SketchesServiceClientImpl implements FrontendApplicationContributio

onStart(): void {
this.configService.getConfiguration().then(({ sketchDirUri }) => {
this.sketchService.getSketches(sketchDirUri).then(sketches => {
this.sketchService.getSketches({ uri: sketchDirUri }).then(container => {
const sketchbookUri = new URI(sketchDirUri);
for (const sketch of sketches) {
for (const sketch of SketchContainer.toArray(container)) {
this.sketches.set(sketch.uri, sketch);
}
this.toDispose.push(this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [] }));
Expand Down
55 changes: 52 additions & 3 deletions arduino-ide-extension/src/common/protocol/sketches-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ export const SketchesService = Symbol('SketchesService');
export interface SketchesService {

/**
* Returns with the direct sketch folders from the location of the `fileStat`.
* The sketches returns with inverse-chronological order, the first item is the most recent one.
* Resolves to a sketch container representing the hierarchical structure of the sketches.
* If `uri` is not given, `directories.user` will be user instead. Specify `exclude` global patterns to filter folders from the sketch container.
* If `exclude` is not set `['**\/libraries\/**', '**\/hardware\/**']` will be used instead.
*/
getSketches(uri?: string): Promise<Sketch[]>;
getSketches({ uri, exclude }: { uri?: string, exclude?: string[] }): Promise<SketchContainer>;

/**
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
Expand Down Expand Up @@ -100,3 +101,51 @@ export namespace Sketch {
return Extensions.MAIN.some(ext => arg.endsWith(ext));
}
}

export interface SketchContainer {
readonly label: string;
readonly children: SketchContainer[];
readonly sketches: Sketch[];
}
export namespace SketchContainer {

export function is(arg: any): arg is SketchContainer {
return !!arg
&& 'label' in arg && typeof arg.label === 'string'
&& 'children' in arg && Array.isArray(arg.children)
&& 'sketches' in arg && Array.isArray(arg.sketches);
}

/**
* `false` if the `container` recursively contains at least one sketch. Otherwise, `true`.
*/
export function isEmpty(container: SketchContainer): boolean {
const hasSketch = (parent: SketchContainer) => {
if (parent.sketches.length || parent.children.some(child => hasSketch(child))) {
return true;
}
return false;
}
return !hasSketch(container);
}

export function prune<T extends SketchContainer>(container: T): T {
for (let i = container.children.length - 1; i >= 0; i--) {
if (isEmpty(container.children[i])) {
container.children.splice(i, 1);
}
}
return container;
}

export function toArray(container: SketchContainer): Sketch[] {
const visit = (parent: SketchContainer, toPushSketch: Sketch[]) => {
toPushSketch.push(...parent.sketches);
parent.children.map(child => visit(child, toPushSketch));
}
const sketches: Sketch[] = [];
visit(container, sketches);
return sketches;
}

}
Loading