From 527a34dd27652b008f42f65bdd6f2166e113ab31 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Fri, 9 Jul 2021 09:22:55 +0200 Subject: [PATCH] wip --- .../filterable-list-container.tsx | 1 - .../sketchbook/sketchbook-tree-model.ts | 227 +++++++++++++++++- .../widgets/sketchbook/sketchbook-tree.ts | 88 +++++-- .../sketchbook-widget-contribution.ts | 30 +++ .../widgets/sketchbook/sketchbook-widget.tsx | 4 + 5 files changed, 319 insertions(+), 31 deletions(-) diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 9048d3e1d..c7b7c33c3 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -94,7 +94,6 @@ export class FilterableListContainer< } protected sort(items: T[]): T[] { - // debugger; const { itemLabel, itemDeprecated } = this.props; return items.sort((left, right) => { // always put deprecated items at the bottom of the list diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-model.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-model.ts index 90b6bcfe2..a4c3facce 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-model.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-model.ts @@ -1,15 +1,29 @@ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { ConfigService } from '../../../common/protocol'; import { SketchbookTree } from './sketchbook-tree'; import { ArduinoPreferences } from '../../arduino-preferences'; -import { SelectableTreeNode, TreeNode } from '@theia/core/lib/browser/tree'; +import { + CompositeTreeNode, + ExpandableTreeNode, + SelectableTreeNode, + TreeNode, +} from '@theia/core/lib/browser/tree'; import { SketchbookCommands } from './sketchbook-commands'; import { OpenerService, open } from '@theia/core/lib/browser'; import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; import { CommandRegistry } from '@theia/core/lib/common/command'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { ProgressService } from '@theia/core/lib/common/progress-service'; +import { + WorkspaceNode, + WorkspaceRootNode, +} from '@theia/navigator/lib/browser/navigator-tree'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Disposable } from '@theia/core/lib/common/disposable'; @injectable() export class SketchbookTreeModel extends FileTreeModel { @@ -31,14 +45,209 @@ export class SketchbookTreeModel extends FileTreeModel { @inject(SketchesServiceClientImpl) protected readonly sketchServiceClient: SketchesServiceClientImpl; - async updateRoot(): Promise { - const config = await this.configService.getConfiguration(); - const fileStat = await this.fileService.resolve( - new URI(config.sketchDirUri) + @inject(SketchbookTree) protected readonly tree: SketchbookTree; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + @inject(FrontendApplicationStateService) + protected readonly applicationState: FrontendApplicationStateService; + + @inject(ProgressService) + protected readonly progressService: ProgressService; + + @postConstruct() + protected init(): void { + super.init(); + this.reportBusyProgress(); + this.initializeRoot(); + } + + protected readonly pendingBusyProgress = new Map>(); + protected reportBusyProgress(): void { + this.toDispose.push( + this.onDidChangeBusy((node) => { + const pending = this.pendingBusyProgress.get(node.id); + if (pending) { + if (!node.busy) { + pending.resolve(); + this.pendingBusyProgress.delete(node.id); + } + return; + } + if (node.busy) { + const progress = new Deferred(); + this.pendingBusyProgress.set(node.id, progress); + this.progressService.withProgress( + '', + 'explorer', + () => progress.promise + ); + } + }) + ); + this.toDispose.push( + Disposable.create(() => { + for (const pending of this.pendingBusyProgress.values()) { + pending.resolve(); + } + this.pendingBusyProgress.clear(); + }) ); - const showAllFiles = - this.arduinoPreferences['arduino.sketchbook.showAllFiles']; - this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles); + } + + protected async initializeRoot(): Promise { + await Promise.all([ + this.applicationState.reachedState('initialized_layout'), + this.workspaceService.roots, + ]); + await this.updateRoot(); + if (this.toDispose.disposed) { + return; + } + this.toDispose.push( + this.workspaceService.onWorkspaceChanged(() => this.updateRoot()) + ); + this.toDispose.push( + this.workspaceService.onWorkspaceLocationChanged(() => this.updateRoot()) + ); + if (this.selectedNodes.length) { + return; + } + const root = this.root; + if (CompositeTreeNode.is(root) && root.children.length === 1) { + const child = root.children[0]; + if ( + SelectableTreeNode.is(child) && + !child.selected && + ExpandableTreeNode.is(child) + ) { + this.selectNode(child); + this.expandNode(child); + } + } + } + + previewNode(node: TreeNode): void { + if (FileNode.is(node)) { + open(this.openerService, node.uri, { + mode: 'reveal', + preview: true, + }); + } + } + + *getNodesByUri(uri: URI): IterableIterator { + const workspace = this.root; + if (WorkspaceNode.is(workspace)) { + for (const root of workspace.children) { + const id = this.tree.createId(root, uri); + const node = this.getNode(id); + if (node) { + yield node; + } + } + } + } + + public async updateRoot(): Promise { + this.root = await this.createRoot(); + } + + protected async createRoot(): Promise { + const config = await this.configService.getConfiguration(); + const stat = await this.fileService.resolve(new URI(config.sketchDirUri)); + + if (this.workspaceService.opened) { + const isMulti = stat ? !stat.isDirectory : false; + const workspaceNode = isMulti + ? this.createMultipleRootNode() + : WorkspaceNode.createRoot(); + workspaceNode.children.push( + await this.tree.createWorkspaceRoot(stat, workspaceNode) + ); + + return workspaceNode; + } + } + + /** + * Create multiple root node used to display + * the multiple root workspace name. + * + * @returns `WorkspaceNode` + */ + protected createMultipleRootNode(): WorkspaceNode { + const workspace = this.workspaceService.workspace; + let name = workspace ? workspace.resource.path.name : 'untitled'; + name += ' (Workspace)'; + return WorkspaceNode.createRoot(name); + } + + /** + * Move the given source file or directory to the given target directory. + */ + async move(source: TreeNode, target: TreeNode): Promise { + if (source.parent && WorkspaceRootNode.is(source)) { + // do not support moving a root folder + return undefined; + } + return super.move(source, target); + } + + /** + * Reveals node in the navigator by given file uri. + * + * @param uri uri to file which should be revealed in the navigator + * @returns file tree node if the file with given uri was revealed, undefined otherwise + */ + async revealFile(uri: URI): Promise { + if (!uri.path.isAbsolute) { + return undefined; + } + let node = this.getNodeClosestToRootByUri(uri); + + // success stop condition + // we have to reach workspace root because expanded node could be inside collapsed one + if (WorkspaceRootNode.is(node)) { + if (ExpandableTreeNode.is(node)) { + if (!node.expanded) { + node = await this.expandNode(node); + } + return node; + } + // shouldn't happen, root node is always directory, i.e. expandable + return undefined; + } + + // fail stop condition + if (uri.path.isRoot) { + // file system root is reached but workspace root wasn't found, it means that + // given uri is not in workspace root folder or points to not existing file. + return undefined; + } + + if (await this.revealFile(uri.parent)) { + if (node === undefined) { + // get node if it wasn't mounted into navigator tree before expansion + node = this.getNodeClosestToRootByUri(uri); + } + if (ExpandableTreeNode.is(node) && !node.expanded) { + node = await this.expandNode(node); + } + return node; + } + return undefined; + } + + protected getNodeClosestToRootByUri(uri: URI): TreeNode | undefined { + const nodes = [...this.getNodesByUri(uri)]; + return nodes.length > 0 + ? nodes.reduce( + ( + node1, + node2 // return the node closest to the workspace root + ) => (node1.id.length >= node2.id.length ? node1 : node2) + ) + : undefined; } // selectNode gets called when the user single-clicks on an item diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts index d5087c2e6..f8091c513 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts @@ -1,40 +1,39 @@ import { inject, injectable } from 'inversify'; import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { Command } from '@theia/core/lib/common/command'; -import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree'; -import { - DirNode, - FileStatNode, - FileTree, -} from '@theia/filesystem/lib/browser/file-tree'; +import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree'; +import { DirNode, FileStatNode } from '@theia/filesystem/lib/browser/file-tree'; import { SketchesService } from '../../../common/protocol'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { SketchbookCommands } from './sketchbook-commands'; +import { + FileNavigatorTree, + WorkspaceNode, +} from '@theia/navigator/lib/browser/navigator-tree'; +import { ArduinoPreferences } from '../../arduino-preferences'; @injectable() -export class SketchbookTree extends FileTree { +export class SketchbookTree extends FileNavigatorTree { @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(SketchesService) protected readonly sketchesService: SketchesService; + @inject(ArduinoPreferences) + protected readonly arduinoPreferences: ArduinoPreferences; + async resolveChildren(parent: CompositeTreeNode): Promise { - if (!FileStatNode.is(parent)) { - return super.resolveChildren(parent); - } - const { root } = this; - if (!root) { - return []; - } - if (!SketchbookTree.RootNode.is(root)) { - return []; - } + const showAllFiles = + this.arduinoPreferences['arduino.sketchbook.showAllFiles']; + + console.log(`showAllFiles: ${showAllFiles}`); + const children = ( await Promise.all( ( await super.resolveChildren(parent) - ).map((node) => this.maybeDecorateNode(node, root.showAllFiles)) + ).map((node) => this.maybeDecorateNode(node, showAllFiles)) ) ).filter((node) => { // filter out hidden nodes @@ -43,7 +42,9 @@ export class SketchbookTree extends FileTree { } return true; }); - if (SketchbookTree.RootNode.is(parent)) { + + // filter out hardware and libraries + if (WorkspaceNode.is(parent.parent)) { return children .filter(DirNode.is) .filter( @@ -53,12 +54,57 @@ export class SketchbookTree extends FileTree { ) === -1 ); } - if (SketchbookTree.SketchDirNode.is(parent)) { - return children.filter(FileStatNode.is); + + // return the Arduino directory containing all user sketches + if (WorkspaceNode.is(parent)) { + return children; } + return children; + + // return this.filter.filter(super.resolveChildren(parent)); } + // async resolveChildren(parent: CompositeTreeNode): Promise { + // if (!FileStatNode.is(parent)) { + // return super.resolveChildren(parent); + // } + // const { root } = this; + // if (!root) { + // return []; + // } + // if (!SketchbookTree.RootNode.is(root)) { + // return []; + // } + // const children = ( + // await Promise.all( + // ( + // await super.resolveChildren(parent) + // ).map((node) => this.maybeDecorateNode(node, root.showAllFiles)) + // ) + // ).filter((node) => { + // // filter out hidden nodes + // if (DirNode.is(node) || FileStatNode.is(node)) { + // return node.fileStat.name.indexOf('.') !== 0; + // } + // return true; + // }); + // if (SketchbookTree.RootNode.is(parent)) { + // return children + // .filter(DirNode.is) + // .filter( + // (node) => + // ['libraries', 'hardware'].indexOf( + // this.labelProvider.getName(node) + // ) === -1 + // ); + // } + // if (SketchbookTree.SketchDirNode.is(parent)) { + // return children.filter(FileStatNode.is); + // } + // return children; + // } + protected async maybeDecorateNode( node: TreeNode, showAllFiles: boolean diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts index a0b2ebbbb..4db59ece8 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts @@ -14,7 +14,10 @@ import { SketchbookCommands } from './sketchbook-commands'; import { WorkspaceService } from '../../theia/workspace/workspace-service'; import { ContextMenuRenderer, + Navigatable, RenderContextMenuOptions, + SelectableTreeNode, + Widget, } from '@theia/core/lib/browser'; import { Disposable, @@ -76,6 +79,10 @@ export class SketchbookWidgetContribution } onStart(): void { + this.shell.currentChanged.connect(() => + this.onCurrentWidgetChangedHandler() + ); + this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => { if (preferenceName === 'arduino.sketchbook.showAllFiles') { this.mainMenuManager.update(); @@ -187,4 +194,27 @@ export class SketchbookWidgetContribution order: '0', }); } + + /** + * Reveals and selects node in the file navigator to which given widget is related. + * Does nothing if given widget undefined or doesn't have related resource. + * + * @param widget widget file resource of which should be revealed and selected + */ + async selectWidgetFileNode(widget: Widget | undefined): Promise { + if (Navigatable.is(widget)) { + const resourceUri = widget.getResourceUri(); + if (resourceUri) { + const { model } = (await this.widget).getTreeWidget(); + const node = await model.revealFile(resourceUri); + if (SelectableTreeNode.is(node)) { + model.selectNode(node); + } + } + } + } + + protected onCurrentWidgetChangedHandler(): void { + this.selectWidgetFileNode(this.shell.currentWidget); + } } diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx index 66e6e97a8..acde13450 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx @@ -38,6 +38,10 @@ export class SketchbookWidget extends BaseWidget { ); } + getTreeWidget(): SketchbookTreeWidget { + return this.localSketchbookTreeWidget; + } + protected onActivateRequest(message: Message): void { super.onActivateRequest(message);