Skip to content

Commit c64ac48

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
ATL-1064: Support for nested sketchbook structure
Signed-off-by: Akos Kitta <[email protected]>
1 parent ac50205 commit c64ac48

File tree

9 files changed

+188
-116
lines changed

9 files changed

+188
-116
lines changed

Diff for: arduino-ide-extension/src/browser/contributions/examples.ts

+32-17
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import * as PQueue from 'p-queue';
22
import { inject, injectable, postConstruct } from 'inversify';
3-
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
3+
import { MenuPath, CompositeMenuNode, SubMenuOptions } from '@theia/core/lib/common/menu';
44
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
55
import { OpenSketch } from './open-sketch';
66
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
77
import { MainMenuManager } from '../../common/main-menu-manager';
88
import { BoardsServiceProvider } from '../boards/boards-service-provider';
9-
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
9+
import { ExamplesService } from '../../common/protocol/examples-service';
1010
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
1111
import { NotificationCenter } from '../notification-center';
12-
import { Board } from '../../common/protocol';
12+
import { Board, Sketch, SketchContainer } from '../../common/protocol';
1313

1414
@injectable()
1515
export abstract class Examples extends SketchContribution {
@@ -59,18 +59,35 @@ export abstract class Examples extends SketchContribution {
5959
}
6060

6161
registerRecursively(
62-
exampleContainerOrPlaceholder: ExampleContainer | string,
62+
sketchContainerOrPlaceholder: SketchContainer | (Sketch | SketchContainer)[] | string,
6363
menuPath: MenuPath,
64-
pushToDispose: DisposableCollection = new DisposableCollection()): void {
64+
pushToDispose: DisposableCollection = new DisposableCollection(),
65+
subMenuOptions?: SubMenuOptions | undefined): void {
6566

66-
if (typeof exampleContainerOrPlaceholder === 'string') {
67-
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
67+
if (typeof sketchContainerOrPlaceholder === 'string') {
68+
const placeholder = new PlaceholderMenuNode(menuPath, sketchContainerOrPlaceholder);
6869
this.menuRegistry.registerMenuNode(menuPath, placeholder);
6970
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
7071
} else {
71-
const { label, sketches, children } = exampleContainerOrPlaceholder;
72-
const submenuPath = [...menuPath, label];
73-
this.menuRegistry.registerSubmenu(submenuPath, label);
72+
const sketches: Sketch[] = [];
73+
const children: SketchContainer[] = [];
74+
let submenuPath = menuPath;
75+
76+
if (SketchContainer.is(sketchContainerOrPlaceholder)) {
77+
const { label } = sketchContainerOrPlaceholder;
78+
submenuPath = [...menuPath, label];
79+
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
80+
sketches.push(...sketchContainerOrPlaceholder.sketches);
81+
children.push(...sketchContainerOrPlaceholder.children);
82+
} else {
83+
for (const sketchOrContainer of sketchContainerOrPlaceholder) {
84+
if (SketchContainer.is(sketchOrContainer)) {
85+
children.push(sketchOrContainer);
86+
} else {
87+
sketches.push(sketchOrContainer);
88+
}
89+
}
90+
}
7491
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
7592
for (const sketch of sketches) {
7693
const { uri } = sketch;
@@ -98,22 +115,20 @@ export class BuiltInExamples extends Examples {
98115
this.register(); // no `await`
99116
}
100117

101-
protected async register() {
102-
let exampleContainers: ExampleContainer[] | undefined;
118+
protected async register(): Promise<void> {
119+
let sketchContainers: SketchContainer[] | undefined;
103120
try {
104-
exampleContainers = await this.examplesService.builtIns();
121+
sketchContainers = await this.examplesService.builtIns();
105122
} catch (e) {
106123
console.error('Could not initialize built-in examples.', e);
107124
this.messageService.error('Could not initialize built-in examples.');
108125
return;
109126
}
110127
this.toDispose.dispose();
111-
for (const container of ['Built-in examples', ...exampleContainers]) {
128+
for (const container of ['Built-in examples', ...sketchContainers]) {
112129
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
113130
}
114131
this.menuManager.update();
115-
// TODO: remove
116-
console.log(typeof this.menuRegistry);
117132
}
118133

119134
}
@@ -136,7 +151,7 @@ export class LibraryExamples extends Examples {
136151
this.register(board);
137152
}
138153

139-
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
154+
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard): Promise<void> {
140155
return this.queue.add(async () => {
141156
this.toDispose.dispose();
142157
if (!board || !board.fqbn) {

Diff for: arduino-ide-extension/src/browser/contributions/open-sketch.ts

+12-16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
88
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
99
import { ExamplesService } from '../../common/protocol/examples-service';
1010
import { BuiltInExamples } from './examples';
11+
import { Sketchbook } from './sketchbook';
12+
import { SketchContainer } from '../../common/protocol';
1113

1214
@injectable()
1315
export class OpenSketch extends SketchContribution {
@@ -24,7 +26,10 @@ export class OpenSketch extends SketchContribution {
2426
@inject(ExamplesService)
2527
protected readonly examplesService: ExamplesService;
2628

27-
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
29+
@inject(Sketchbook)
30+
protected readonly sketchbook: Sketchbook;
31+
32+
protected readonly toDispose = new DisposableCollection();
2833

2934
registerCommands(registry: CommandRegistry): void {
3035
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
@@ -33,11 +38,11 @@ export class OpenSketch extends SketchContribution {
3338
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
3439
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
3540
execute: async (_: Widget, target: EventTarget) => {
36-
const sketches = await this.sketchService.getSketches();
37-
if (!sketches.length) {
41+
const container = await this.sketchService.getSketches({ exclude: ['**/hardware/**'] });
42+
if (SketchContainer.isEmpty(container)) {
3843
this.openSketch();
3944
} else {
40-
this.toDisposeBeforeCreateNewContextMenu.dispose();
45+
this.toDispose.dispose();
4146
if (!(target instanceof HTMLElement)) {
4247
return;
4348
}
@@ -50,21 +55,12 @@ export class OpenSketch extends SketchContribution {
5055
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
5156
label: 'Open...'
5257
});
53-
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
54-
for (const sketch of sketches) {
55-
const command = { id: `arduino-open-sketch--${sketch.uri}` };
56-
const handler = { execute: () => this.openSketch(sketch) };
57-
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
58-
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, {
59-
commandId: command.id,
60-
label: sketch.name
61-
});
62-
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
63-
}
58+
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
59+
this.sketchbook.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, this.toDispose);
6460
try {
6561
const containers = await this.examplesService.builtIns();
6662
for (const container of containers) {
67-
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu);
63+
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDispose);
6864
}
6965
} catch (e) {
7066
console.error('Error when collecting built-in examples.', e);
+14-37
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { inject, injectable } from 'inversify';
2-
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
3-
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
2+
import { CommandRegistry, MenuModelRegistry } from './contribution';
43
import { ArduinoMenus } from '../menu/arduino-menus';
54
import { MainMenuManager } from '../../common/main-menu-manager';
65
import { NotificationCenter } from '../notification-center';
7-
import { OpenSketch } from './open-sketch';
6+
import { Examples } from './examples';
7+
import { SketchContainer } from '../../common/protocol';
88

99
@injectable()
10-
export class Sketchbook extends SketchContribution {
10+
export class Sketchbook extends Examples {
1111

1212
@inject(CommandRegistry)
1313
protected readonly commandRegistry: CommandRegistry;
@@ -21,49 +21,26 @@ export class Sketchbook extends SketchContribution {
2121
@inject(NotificationCenter)
2222
protected readonly notificationCenter: NotificationCenter;
2323

24-
protected toDisposePerSketch = new Map<string, DisposableCollection>();
25-
2624
onStart(): void {
27-
this.sketchService.getSketches().then(sketches => {
28-
this.register(sketches);
25+
this.sketchService.getSketches({}).then(container => {
26+
this.register(container);
2927
this.mainMenuManager.update();
3028
});
31-
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => {
32-
this.unregister(removed);
33-
this.register(created);
34-
this.mainMenuManager.update();
29+
this.sketchServiceClient.onSketchbookDidChange(() => {
30+
this.sketchService.getSketches({}).then(container => {
31+
this.register(container);
32+
this.mainMenuManager.update();
33+
});
3534
});
3635
}
3736

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

42-
protected register(sketches: Sketch[]): void {
43-
for (const sketch of sketches) {
44-
const { uri } = sketch;
45-
const toDispose = this.toDisposePerSketch.get(uri);
46-
if (toDispose) {
47-
toDispose.dispose();
48-
}
49-
const command = { id: `arduino-sketchbook-open--${uri}` };
50-
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
51-
this.commandRegistry.registerCommand(command, handler);
52-
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
53-
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
54-
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
55-
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
56-
));
57-
}
58-
}
59-
60-
protected unregister(sketches: Sketch[]): void {
61-
for (const { uri } of sketches) {
62-
const toDispose = this.toDisposePerSketch.get(uri);
63-
if (toDispose) {
64-
toDispose.dispose();
65-
}
66-
}
41+
protected register(container: SketchContainer): void {
42+
this.toDispose.dispose();
43+
this.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, this.toDispose);
6744
}
6845

6946
}

Diff for: arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
88
import { FocusTracker, Widget } from '@theia/core/lib/browser';
99
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
1010
import { ConfigService } from '../../../common/protocol/config-service';
11-
import { SketchesService, Sketch } from '../../../common/protocol/sketches-service';
11+
import { SketchesService, Sketch, SketchContainer } from '../../../common/protocol/sketches-service';
1212
import { ArduinoWorkspaceRootResolver } from '../../arduino-workspace-resolver';
1313

1414
@injectable()
@@ -50,7 +50,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
5050
const hash = window.location.hash;
5151
const [recentWorkspaces, recentSketches] = await Promise.all([
5252
this.server.getRecentWorkspaces(),
53-
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
53+
this.sketchService.getSketches({}).then(container => SketchContainer.toArray(container).map(s => s.uri))
5454
]);
5555
const toOpen = await new ArduinoWorkspaceRootResolver({
5656
isValid: this.isValid.bind(this)
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import { Sketch } from './sketches-service';
1+
import { SketchContainer } from './sketches-service';
22

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

10-
export interface ExampleContainer {
11-
readonly label: string;
12-
readonly children: ExampleContainer[];
13-
readonly sketches: Sketch[];
14-
}
10+

Diff for: arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser';
99
import { ConfigService } from './config-service';
1010
import { DisposableCollection, Emitter } from '@theia/core';
1111
import { FileChangeType } from '@theia/filesystem/lib/browser';
12+
import { SketchContainer } from './sketches-service';
1213

1314
@injectable()
1415
export class SketchesServiceClientImpl implements FrontendApplicationContribution {
@@ -35,9 +36,9 @@ export class SketchesServiceClientImpl implements FrontendApplicationContributio
3536

3637
onStart(): void {
3738
this.configService.getConfiguration().then(({ sketchDirUri }) => {
38-
this.sketchService.getSketches(sketchDirUri).then(sketches => {
39+
this.sketchService.getSketches({ uri: sketchDirUri }).then(container => {
3940
const sketchbookUri = new URI(sketchDirUri);
40-
for (const sketch of sketches) {
41+
for (const sketch of SketchContainer.toArray(container)) {
4142
this.sketches.set(sketch.uri, sketch);
4243
}
4344
this.toDispose.push(this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [] }));

Diff for: arduino-ide-extension/src/common/protocol/sketches-service.ts

+52-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ export const SketchesService = Symbol('SketchesService');
55
export interface SketchesService {
66

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

1314
/**
1415
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
@@ -100,3 +101,51 @@ export namespace Sketch {
100101
return Extensions.MAIN.some(ext => arg.endsWith(ext));
101102
}
102103
}
104+
105+
export interface SketchContainer {
106+
readonly label: string;
107+
readonly children: SketchContainer[];
108+
readonly sketches: Sketch[];
109+
}
110+
export namespace SketchContainer {
111+
112+
export function is(arg: any): arg is SketchContainer {
113+
return !!arg
114+
&& 'label' in arg && typeof arg.label === 'string'
115+
&& 'children' in arg && Array.isArray(arg.children)
116+
&& 'sketches' in arg && Array.isArray(arg.sketches);
117+
}
118+
119+
/**
120+
* `false` if the `container` recursively contains at least one sketch. Otherwise, `true`.
121+
*/
122+
export function isEmpty(container: SketchContainer): boolean {
123+
const hasSketch = (parent: SketchContainer) => {
124+
if (parent.sketches.length || parent.children.some(child => hasSketch(child))) {
125+
return true;
126+
}
127+
return false;
128+
}
129+
return !hasSketch(container);
130+
}
131+
132+
export function prune<T extends SketchContainer>(container: T): T {
133+
for (let i = container.children.length - 1; i >= 0; i--) {
134+
if (isEmpty(container.children[i])) {
135+
container.children.splice(i, 1);
136+
}
137+
}
138+
return container;
139+
}
140+
141+
export function toArray(container: SketchContainer): Sketch[] {
142+
const visit = (parent: SketchContainer, toPushSketch: Sketch[]) => {
143+
toPushSketch.push(...parent.sketches);
144+
parent.children.map(child => visit(child, toPushSketch));
145+
}
146+
const sketches: Sketch[] = [];
147+
visit(container, sketches);
148+
return sketches;
149+
}
150+
151+
}

0 commit comments

Comments
 (0)