ATL-1064: Support for nested sketchbook structure

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2021-03-10 17:30:58 +01:00 committed by Akos Kitta
parent ac502053d7
commit c64ac48fe3
9 changed files with 188 additions and 116 deletions

View File

@ -1,15 +1,15 @@
import * as PQueue from 'p-queue'; import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify'; 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 { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; 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 { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { Board } from '../../common/protocol'; import { Board, Sketch, SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@ -59,18 +59,35 @@ export abstract class Examples extends SketchContribution {
} }
registerRecursively( registerRecursively(
exampleContainerOrPlaceholder: ExampleContainer | string, sketchContainerOrPlaceholder: SketchContainer | (Sketch | SketchContainer)[] | string,
menuPath: MenuPath, menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection()): void { pushToDispose: DisposableCollection = new DisposableCollection(),
subMenuOptions?: SubMenuOptions | undefined): void {
if (typeof exampleContainerOrPlaceholder === 'string') { if (typeof sketchContainerOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder); const placeholder = new PlaceholderMenuNode(menuPath, sketchContainerOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder); this.menuRegistry.registerMenuNode(menuPath, placeholder);
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id))); pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
} else { } else {
const { label, sketches, children } = exampleContainerOrPlaceholder; const sketches: Sketch[] = [];
const submenuPath = [...menuPath, label]; const children: SketchContainer[] = [];
this.menuRegistry.registerSubmenu(submenuPath, label); 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)); children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
for (const sketch of sketches) { for (const sketch of sketches) {
const { uri } = sketch; const { uri } = sketch;
@ -98,22 +115,20 @@ export class BuiltInExamples extends Examples {
this.register(); // no `await` this.register(); // no `await`
} }
protected async register() { protected async register(): Promise<void> {
let exampleContainers: ExampleContainer[] | undefined; let sketchContainers: SketchContainer[] | undefined;
try { try {
exampleContainers = await this.examplesService.builtIns(); sketchContainers = await this.examplesService.builtIns();
} catch (e) { } catch (e) {
console.error('Could not initialize built-in examples.', e); console.error('Could not initialize built-in examples.', e);
this.messageService.error('Could not initialize built-in examples.'); this.messageService.error('Could not initialize built-in examples.');
return; return;
} }
this.toDispose.dispose(); 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.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
} }
this.menuManager.update(); this.menuManager.update();
// TODO: remove
console.log(typeof this.menuRegistry);
} }
} }
@ -136,7 +151,7 @@ export class LibraryExamples extends Examples {
this.register(board); 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 () => { return this.queue.add(async () => {
this.toDispose.dispose(); this.toDispose.dispose();
if (!board || !board.fqbn) { if (!board || !board.fqbn) {

View File

@ -8,6 +8,8 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution'; import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service'; import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples'; import { BuiltInExamples } from './examples';
import { Sketchbook } from './sketchbook';
import { SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export class OpenSketch extends SketchContribution { export class OpenSketch extends SketchContribution {
@ -24,7 +26,10 @@ export class OpenSketch extends SketchContribution {
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: 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 { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
@ -33,11 +38,11 @@ export class OpenSketch extends SketchContribution {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (_: Widget, target: EventTarget) => { execute: async (_: Widget, target: EventTarget) => {
const sketches = await this.sketchService.getSketches(); const container = await this.sketchService.getSketches({ exclude: ['**/hardware/**'] });
if (!sketches.length) { if (SketchContainer.isEmpty(container)) {
this.openSketch(); this.openSketch();
} else { } else {
this.toDisposeBeforeCreateNewContextMenu.dispose(); this.toDispose.dispose();
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) {
return; return;
} }
@ -50,21 +55,12 @@ export class OpenSketch extends SketchContribution {
commandId: OpenSketch.Commands.OPEN_SKETCH.id, commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...' label: 'Open...'
}); });
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH))); this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
for (const sketch of sketches) { this.sketchbook.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, this.toDispose);
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)));
}
try { try {
const containers = await this.examplesService.builtIns(); const containers = await this.examplesService.builtIns();
for (const container of containers) { 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) { } catch (e) {
console.error('Error when collecting built-in examples.', e); console.error('Error when collecting built-in examples.', e);

View File

@ -1,13 +1,13 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { CommandRegistry, MenuModelRegistry } from './contribution';
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { OpenSketch } from './open-sketch'; import { Examples } from './examples';
import { SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export class Sketchbook extends SketchContribution { export class Sketchbook extends Examples {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@ -21,17 +21,16 @@ export class Sketchbook extends SketchContribution {
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected toDisposePerSketch = new Map<string, DisposableCollection>();
onStart(): void { onStart(): void {
this.sketchService.getSketches().then(sketches => { this.sketchService.getSketches({}).then(container => {
this.register(sketches); this.register(container);
this.mainMenuManager.update(); this.mainMenuManager.update();
}); });
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => { this.sketchServiceClient.onSketchbookDidChange(() => {
this.unregister(removed); this.sketchService.getSketches({}).then(container => {
this.register(created); this.register(container);
this.mainMenuManager.update(); this.mainMenuManager.update();
});
}); });
} }
@ -39,31 +38,9 @@ export class Sketchbook extends SketchContribution {
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' }); registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
} }
protected register(sketches: Sketch[]): void { protected register(container: SketchContainer): void {
for (const sketch of sketches) { this.toDispose.dispose();
const { uri } = sketch; this.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, this.toDispose);
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();
}
}
} }
} }

View File

@ -8,7 +8,7 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
import { FocusTracker, Widget } from '@theia/core/lib/browser'; import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../../../common/protocol/config-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'; import { ArduinoWorkspaceRootResolver } from '../../arduino-workspace-resolver';
@injectable() @injectable()
@ -50,7 +50,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
const hash = window.location.hash; const hash = window.location.hash;
const [recentWorkspaces, recentSketches] = await Promise.all([ const [recentWorkspaces, recentSketches] = await Promise.all([
this.server.getRecentWorkspaces(), 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({ const toOpen = await new ArduinoWorkspaceRootResolver({
isValid: this.isValid.bind(this) isValid: this.isValid.bind(this)

View File

@ -1,14 +1,10 @@
import { Sketch } from './sketches-service'; import { SketchContainer } from './sketches-service';
export const ExamplesServicePath = '/services/example-service'; export const ExamplesServicePath = '/services/example-service';
export const ExamplesService = Symbol('ExamplesService'); export const ExamplesService = Symbol('ExamplesService');
export interface ExamplesService { export interface ExamplesService {
builtIns(): Promise<ExampleContainer[]>; builtIns(): Promise<SketchContainer[]>;
installed(options: { fqbn: string }): Promise<{ user: ExampleContainer[], current: ExampleContainer[], any: ExampleContainer[] }>; installed(options: { fqbn: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }>;
} }
export interface ExampleContainer {
readonly label: string;
readonly children: ExampleContainer[];
readonly sketches: Sketch[];
}

View File

@ -9,6 +9,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ConfigService } from './config-service'; import { ConfigService } from './config-service';
import { DisposableCollection, Emitter } from '@theia/core'; import { DisposableCollection, Emitter } from '@theia/core';
import { FileChangeType } from '@theia/filesystem/lib/browser'; import { FileChangeType } from '@theia/filesystem/lib/browser';
import { SketchContainer } from './sketches-service';
@injectable() @injectable()
export class SketchesServiceClientImpl implements FrontendApplicationContribution { export class SketchesServiceClientImpl implements FrontendApplicationContribution {
@ -35,9 +36,9 @@ export class SketchesServiceClientImpl implements FrontendApplicationContributio
onStart(): void { onStart(): void {
this.configService.getConfiguration().then(({ sketchDirUri }) => { this.configService.getConfiguration().then(({ sketchDirUri }) => {
this.sketchService.getSketches(sketchDirUri).then(sketches => { this.sketchService.getSketches({ uri: sketchDirUri }).then(container => {
const sketchbookUri = new URI(sketchDirUri); const sketchbookUri = new URI(sketchDirUri);
for (const sketch of sketches) { for (const sketch of SketchContainer.toArray(container)) {
this.sketches.set(sketch.uri, sketch); this.sketches.set(sketch.uri, sketch);
} }
this.toDispose.push(this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [] })); this.toDispose.push(this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [] }));

View File

@ -5,10 +5,11 @@ export const SketchesService = Symbol('SketchesService');
export interface SketchesService { export interface SketchesService {
/** /**
* Returns with the direct sketch folders from the location of the `fileStat`. * Resolves to a sketch container representing the hierarchical structure of the sketches.
* The sketches returns with inverse-chronological order, the first item is the most recent one. * 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. * 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 {
return Extensions.MAIN.some(ext => arg.endsWith(ext)); 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;
}
}

View File

@ -4,9 +4,9 @@ import * as fs from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { FileUri } from '@theia/core/lib/node/file-uri'; import { FileUri } from '@theia/core/lib/node/file-uri';
import { notEmpty } from '@theia/core/lib/common/objects'; import { notEmpty } from '@theia/core/lib/common/objects';
import { Sketch } from '../common/protocol/sketches-service'; import { Sketch, SketchContainer } from '../common/protocol/sketches-service';
import { SketchesServiceImpl } from './sketches-service-impl'; import { SketchesServiceImpl } from './sketches-service-impl';
import { ExamplesService, ExampleContainer } from '../common/protocol/examples-service'; import { ExamplesService } from '../common/protocol/examples-service';
import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol'; import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol';
import { ConfigServiceImpl } from './config-service-impl'; import { ConfigServiceImpl } from './config-service-impl';
@ -22,14 +22,14 @@ export class ExamplesServiceImpl implements ExamplesService {
@inject(ConfigServiceImpl) @inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl; protected readonly configService: ConfigServiceImpl;
protected _all: ExampleContainer[] | undefined; protected _all: SketchContainer[] | undefined;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.builtIns(); this.builtIns();
} }
async builtIns(): Promise<ExampleContainer[]> { async builtIns(): Promise<SketchContainer[]> {
if (this._all) { if (this._all) {
return this._all; return this._all;
} }
@ -40,10 +40,10 @@ export class ExamplesServiceImpl implements ExamplesService {
} }
// TODO: decide whether it makes sense to cache them. Keys should be: `fqbn` + version of containing core/library. // TODO: decide whether it makes sense to cache them. Keys should be: `fqbn` + version of containing core/library.
async installed({ fqbn }: { fqbn: string }): Promise<{ user: ExampleContainer[], current: ExampleContainer[], any: ExampleContainer[] }> { async installed({ fqbn }: { fqbn: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }> {
const user: ExampleContainer[] = []; const user: SketchContainer[] = [];
const current: ExampleContainer[] = []; const current: SketchContainer[] = [];
const any: ExampleContainer[] = []; const any: SketchContainer[] = [];
if (fqbn) { if (fqbn) {
const packages: LibraryPackage[] = await this.libraryService.list({ fqbn }); const packages: LibraryPackage[] = await this.libraryService.list({ fqbn });
for (const pkg of packages) { for (const pkg of packages) {
@ -66,7 +66,7 @@ export class ExamplesServiceImpl implements ExamplesService {
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the * folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
* location of the examples. Otherwise it creates the example container from the direct examples FS paths. * location of the examples. Otherwise it creates the example container from the direct examples FS paths.
*/ */
protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise<ExampleContainer> { protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise<SketchContainer> {
const paths = exampleUris.map(uri => FileUri.fsPath(uri)); const paths = exampleUris.map(uri => FileUri.fsPath(uri));
if (installDirUri) { if (installDirUri) {
for (const example of ['example', 'Example', 'EXAMPLE', 'examples', 'Examples', 'EXAMPLES']) { for (const example of ['example', 'Example', 'EXAMPLE', 'examples', 'Examples', 'EXAMPLES']) {
@ -75,7 +75,7 @@ export class ExamplesServiceImpl implements ExamplesService {
const isDir = exists && (await promisify(fs.lstat)(examplesPath)).isDirectory(); const isDir = exists && (await promisify(fs.lstat)(examplesPath)).isDirectory();
if (isDir) { if (isDir) {
const fileNames = await promisify(fs.readdir)(examplesPath); const fileNames = await promisify(fs.readdir)(examplesPath);
const children: ExampleContainer[] = []; const children: SketchContainer[] = [];
const sketches: Sketch[] = []; const sketches: Sketch[] = [];
for (const fileName of fileNames) { for (const fileName of fileNames) {
const subPath = join(examplesPath, fileName); const subPath = join(examplesPath, fileName);
@ -109,7 +109,7 @@ export class ExamplesServiceImpl implements ExamplesService {
} }
// Built-ins are included inside the IDE. // Built-ins are included inside the IDE.
protected async load(path: string): Promise<ExampleContainer> { protected async load(path: string): Promise<SketchContainer> {
if (!await promisify(fs.exists)(path)) { if (!await promisify(fs.exists)(path)) {
throw new Error('Examples are not available'); throw new Error('Examples are not available');
} }
@ -119,7 +119,7 @@ export class ExamplesServiceImpl implements ExamplesService {
} }
const names = await promisify(fs.readdir)(path); const names = await promisify(fs.readdir)(path);
const sketches: Sketch[] = []; const sketches: Sketch[] = [];
const children: ExampleContainer[] = []; const children: SketchContainer[] = [];
for (const p of names.map(name => join(path, name))) { for (const p of names.map(name => join(path, name))) {
const stat = await promisify(fs.stat)(p); const stat = await promisify(fs.stat)(p);
if (stat.isDirectory()) { if (stat.isDirectory()) {

View File

@ -1,4 +1,5 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import * as minimatch from 'minimatch';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as temp from 'temp'; import * as temp from 'temp';
@ -10,7 +11,7 @@ import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/node'; import { FileUri } from '@theia/core/lib/node';
import { isWindows } from '@theia/core/lib/common/os'; import { isWindows } from '@theia/core/lib/common/os';
import { ConfigService } from '../common/protocol/config-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 { firstToLowerCase } from '../common/utils'; import { firstToLowerCase } from '../common/utils';
import { NotificationServiceServerImpl } from './notification-service-server'; import { NotificationServiceServerImpl } from './notification-service-server';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
@ -32,8 +33,8 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
@inject(EnvVariablesServer) @inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer; protected readonly envVariableServer: EnvVariablesServer;
async getSketches({ uri, exclude }: { uri?: string, exclude?: string[] }): Promise<SketchContainerWithDetails> {
async getSketches(uri?: string): Promise<SketchWithDetails[]> { const start = Date.now();
let sketchbookPath: undefined | string; let sketchbookPath: undefined | string;
if (!uri) { if (!uri) {
const { sketchDirUri } = await this.configService.getConfiguration(); const { sketchDirUri } = await this.configService.getConfiguration();
@ -44,33 +45,65 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ
} else { } else {
sketchbookPath = FileUri.fsPath(uri); sketchbookPath = FileUri.fsPath(uri);
} }
const container: SketchContainerWithDetails = {
label: uri ? path.basename(sketchbookPath) : 'Sketchbook',
sketches: [],
children: []
};
if (!await promisify(fs.exists)(sketchbookPath)) { if (!await promisify(fs.exists)(sketchbookPath)) {
return []; return container;
} }
const stat = await promisify(fs.stat)(sketchbookPath); const stat = await promisify(fs.stat)(sketchbookPath);
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
return []; return container;
} }
const sketches: Array<SketchWithDetails> = []; const recursivelyLoad = async (fsPath: string, containerToLoad: SketchContainerWithDetails) => {
const filenames = await promisify(fs.readdir)(sketchbookPath); const filenames = await promisify(fs.readdir)(fsPath);
for (const fileName of filenames) { for (const name of filenames) {
const filePath = path.join(sketchbookPath, fileName); const childFsPath = path.join(fsPath, name);
const sketch = await this._isSketchFolder(FileUri.create(filePath).toString()); let skip = false;
if (sketch) { for (const pattern of exclude || ['**/libraries/**', '**/hardware/**']) {
if (!skip && minimatch(childFsPath, pattern)) {
skip = true;
}
}
if (skip) {
continue;
}
try { try {
const stat = await promisify(fs.stat)(filePath); const stat = await promisify(fs.stat)(childFsPath);
sketches.push({ if (stat.isDirectory()) {
...sketch, const sketch = await this._isSketchFolder(FileUri.create(childFsPath).toString());
mtimeMs: stat.mtimeMs if (sketch) {
}); containerToLoad.sketches.push({
...sketch,
mtimeMs: stat.mtimeMs
});
} else {
const childContainer: SketchContainerWithDetails = {
label: name,
children: [],
sketches: []
};
await recursivelyLoad(childFsPath, childContainer);
if (!SketchContainer.isEmpty(childContainer)) {
containerToLoad.children.push(childContainer);
}
}
}
} catch { } catch {
console.warn(`Could not load sketch from ${filePath}.`); console.warn(`Could not load sketch from ${childFsPath}.`);
} }
} }
containerToLoad.sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
return containerToLoad;
} }
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
return sketches; await recursivelyLoad(sketchbookPath, container);
SketchContainer.prune(container);
console.debug(`Loading the sketches from ${sketchbookPath} took ${Date.now() - start} ms.`);
return container;
} }
async loadSketch(uri: string): Promise<SketchWithDetails> { async loadSketch(uri: string): Promise<SketchWithDetails> {
@ -363,3 +396,8 @@ void loop() {
interface SketchWithDetails extends Sketch { interface SketchWithDetails extends Sketch {
readonly mtimeMs: number; readonly mtimeMs: number;
} }
interface SketchContainerWithDetails extends SketchContainer {
readonly label: string;
readonly children: SketchContainerWithDetails[];
readonly sketches: SketchWithDetails[];
}