diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index 4b1d5f1a5..4875818b6 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -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 { @@ -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; @@ -98,22 +115,20 @@ export class BuiltInExamples extends Examples { this.register(); // no `await` } - protected async register() { - let exampleContainers: ExampleContainer[] | undefined; + protected async register(): Promise { + 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); } } @@ -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 { return this.queue.add(async () => { this.toDispose.dispose(); if (!board || !board.fqbn) { diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-sketch.ts index a04f882f3..5d6f74123 100644 --- a/arduino-ide-extension/src/browser/contributions/open-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-sketch.ts @@ -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 { @@ -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, { @@ -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; } @@ -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); diff --git a/arduino-ide-extension/src/browser/contributions/sketchbook.ts b/arduino-ide-extension/src/browser/contributions/sketchbook.ts index 55efb77d7..9264bed59 100644 --- a/arduino-ide-extension/src/browser/contributions/sketchbook.ts +++ b/arduino-ide-extension/src/browser/contributions/sketchbook.ts @@ -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; @@ -21,17 +21,16 @@ export class Sketchbook extends SketchContribution { @inject(NotificationCenter) protected readonly notificationCenter: NotificationCenter; - protected toDisposePerSketch = new Map(); - 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(); + }); }); } @@ -39,31 +38,9 @@ export class Sketchbook extends SketchContribution { 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); } } diff --git a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts index bbf9f8a31..3db16c732 100644 --- a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts +++ b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts @@ -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() @@ -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) diff --git a/arduino-ide-extension/src/common/protocol/examples-service.ts b/arduino-ide-extension/src/common/protocol/examples-service.ts index 783af370f..2d511c0f2 100644 --- a/arduino-ide-extension/src/common/protocol/examples-service.ts +++ b/arduino-ide-extension/src/common/protocol/examples-service.ts @@ -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; - installed(options: { fqbn: string }): Promise<{ user: ExampleContainer[], current: ExampleContainer[], any: ExampleContainer[] }>; + builtIns(): Promise; + installed(options: { fqbn: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }>; } -export interface ExampleContainer { - readonly label: string; - readonly children: ExampleContainer[]; - readonly sketches: Sketch[]; -} + diff --git a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts index 3ad64af3b..dba9a497d 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts @@ -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 { @@ -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: [] })); diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 4855a02aa..f521a205f 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -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; + getSketches({ uri, exclude }: { uri?: string, exclude?: string[] }): Promise; /** * 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)); } } + +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(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; + } + +} diff --git a/arduino-ide-extension/src/node/examples-service-impl.ts b/arduino-ide-extension/src/node/examples-service-impl.ts index d519d1fea..5b16adad9 100644 --- a/arduino-ide-extension/src/node/examples-service-impl.ts +++ b/arduino-ide-extension/src/node/examples-service-impl.ts @@ -4,9 +4,9 @@ import * as fs from 'fs'; import { promisify } from 'util'; import { FileUri } from '@theia/core/lib/node/file-uri'; 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 { ExamplesService, ExampleContainer } from '../common/protocol/examples-service'; +import { ExamplesService } from '../common/protocol/examples-service'; import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol'; import { ConfigServiceImpl } from './config-service-impl'; @@ -22,14 +22,14 @@ export class ExamplesServiceImpl implements ExamplesService { @inject(ConfigServiceImpl) protected readonly configService: ConfigServiceImpl; - protected _all: ExampleContainer[] | undefined; + protected _all: SketchContainer[] | undefined; @postConstruct() protected init(): void { this.builtIns(); } - async builtIns(): Promise { + async builtIns(): Promise { if (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. - async installed({ fqbn }: { fqbn: string }): Promise<{ user: ExampleContainer[], current: ExampleContainer[], any: ExampleContainer[] }> { - const user: ExampleContainer[] = []; - const current: ExampleContainer[] = []; - const any: ExampleContainer[] = []; + async installed({ fqbn }: { fqbn: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }> { + const user: SketchContainer[] = []; + const current: SketchContainer[] = []; + const any: SketchContainer[] = []; if (fqbn) { const packages: LibraryPackage[] = await this.libraryService.list({ fqbn }); 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 * location of the examples. Otherwise it creates the example container from the direct examples FS paths. */ - protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise { + protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise { const paths = exampleUris.map(uri => FileUri.fsPath(uri)); if (installDirUri) { 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(); if (isDir) { const fileNames = await promisify(fs.readdir)(examplesPath); - const children: ExampleContainer[] = []; + const children: SketchContainer[] = []; const sketches: Sketch[] = []; for (const fileName of fileNames) { const subPath = join(examplesPath, fileName); @@ -109,7 +109,7 @@ export class ExamplesServiceImpl implements ExamplesService { } // Built-ins are included inside the IDE. - protected async load(path: string): Promise { + protected async load(path: string): Promise { if (!await promisify(fs.exists)(path)) { throw new Error('Examples are not available'); } @@ -119,7 +119,7 @@ export class ExamplesServiceImpl implements ExamplesService { } const names = await promisify(fs.readdir)(path); const sketches: Sketch[] = []; - const children: ExampleContainer[] = []; + const children: SketchContainer[] = []; for (const p of names.map(name => join(path, name))) { const stat = await promisify(fs.stat)(p); if (stat.isDirectory()) { diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index a2d6321af..49f08378f 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -1,4 +1,5 @@ import { injectable, inject } from 'inversify'; +import * as minimatch from 'minimatch'; import * as fs from 'fs'; import * as os from 'os'; 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 { isWindows } from '@theia/core/lib/common/os'; 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 { NotificationServiceServerImpl } from './notification-service-server'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; @@ -32,8 +33,8 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ @inject(EnvVariablesServer) protected readonly envVariableServer: EnvVariablesServer; - - async getSketches(uri?: string): Promise { + async getSketches({ uri, exclude }: { uri?: string, exclude?: string[] }): Promise { + const start = Date.now(); let sketchbookPath: undefined | string; if (!uri) { const { sketchDirUri } = await this.configService.getConfiguration(); @@ -44,33 +45,65 @@ export class SketchesServiceImpl extends CoreClientAware implements SketchesServ } else { sketchbookPath = FileUri.fsPath(uri); } + const container: SketchContainerWithDetails = { + label: uri ? path.basename(sketchbookPath) : 'Sketchbook', + sketches: [], + children: [] + }; if (!await promisify(fs.exists)(sketchbookPath)) { - return []; + return container; } const stat = await promisify(fs.stat)(sketchbookPath); if (!stat.isDirectory()) { - return []; + return container; } - const sketches: Array = []; - const filenames = await promisify(fs.readdir)(sketchbookPath); - for (const fileName of filenames) { - const filePath = path.join(sketchbookPath, fileName); - const sketch = await this._isSketchFolder(FileUri.create(filePath).toString()); - if (sketch) { + const recursivelyLoad = async (fsPath: string, containerToLoad: SketchContainerWithDetails) => { + const filenames = await promisify(fs.readdir)(fsPath); + for (const name of filenames) { + const childFsPath = path.join(fsPath, name); + let skip = false; + for (const pattern of exclude || ['**/libraries/**', '**/hardware/**']) { + if (!skip && minimatch(childFsPath, pattern)) { + skip = true; + } + } + if (skip) { + continue; + } try { - const stat = await promisify(fs.stat)(filePath); - sketches.push({ - ...sketch, - mtimeMs: stat.mtimeMs - }); + const stat = await promisify(fs.stat)(childFsPath); + if (stat.isDirectory()) { + const sketch = await this._isSketchFolder(FileUri.create(childFsPath).toString()); + 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 { - 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 { @@ -363,3 +396,8 @@ void loop() { interface SketchWithDetails extends Sketch { readonly mtimeMs: number; } +interface SketchContainerWithDetails extends SketchContainer { + readonly label: string; + readonly children: SketchContainerWithDetails[]; + readonly sketches: SketchWithDetails[]; +}