[atl-1433][atl-1433] improve local sketchbook explorer (#446)

This commit is contained in:
Francesco Stasi 2021-07-21 15:48:15 +02:00 committed by GitHub
parent 4e6f9ae75d
commit 4da5d573e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 287 additions and 43 deletions

View File

@ -94,7 +94,6 @@ export class FilterableListContainer<
} }
protected sort(items: T[]): T[] { protected sort(items: T[]): T[] {
// debugger;
const { itemLabel, itemDeprecated } = this.props; const { itemLabel, itemDeprecated } = this.props;
return items.sort((left, right) => { return items.sort((left, right) => {
// always put deprecated items at the bottom of the list // always put deprecated items at the bottom of the list

View File

@ -1,15 +1,29 @@
import { inject, injectable } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser'; import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ConfigService } from '../../../common/protocol'; import { ConfigService } from '../../../common/protocol';
import { SketchbookTree } from './sketchbook-tree'; import { SketchbookTree } from './sketchbook-tree';
import { ArduinoPreferences } from '../../arduino-preferences'; 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 { SketchbookCommands } from './sketchbook-commands';
import { OpenerService, open } from '@theia/core/lib/browser'; import { OpenerService, open } from '@theia/core/lib/browser';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { CommandRegistry } from '@theia/core/lib/common/command'; 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() @injectable()
export class SketchbookTreeModel extends FileTreeModel { export class SketchbookTreeModel extends FileTreeModel {
@ -31,14 +45,217 @@ export class SketchbookTreeModel extends FileTreeModel {
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl; protected readonly sketchServiceClient: SketchesServiceClientImpl;
async updateRoot(): Promise<void> { @inject(SketchbookTree) protected readonly tree: SketchbookTree;
const config = await this.configService.getConfiguration(); @inject(WorkspaceService)
const fileStat = await this.fileService.resolve( protected readonly workspaceService: WorkspaceService;
new URI(config.sketchDirUri) @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<string, Deferred<void>>();
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<void>();
this.pendingBusyProgress.set(node.id, progress);
this.progressService.withProgress(
'',
'explorer',
() => progress.promise
); );
const showAllFiles = }
this.arduinoPreferences['arduino.sketchbook.showAllFiles']; })
this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles); );
this.toDispose.push(
Disposable.create(() => {
for (const pending of this.pendingBusyProgress.values()) {
pending.resolve();
}
this.pendingBusyProgress.clear();
})
);
}
protected async initializeRoot(): Promise<void> {
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())
);
this.toDispose.push(
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
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<TreeNode> {
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<void> {
this.root = await this.createRoot();
}
protected async createRoot(): Promise<TreeNode | undefined> {
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<URI | undefined> {
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<TreeNode | undefined> {
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 // selectNode gets called when the user single-clicks on an item

View File

@ -48,23 +48,11 @@ export class SketchbookTreeWidget extends FileTreeWidget {
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
super.init(); super.init();
this.toDispose.push(
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
this.updateModel();
}
})
);
this.updateModel();
// cache the current open sketch uri // cache the current open sketch uri
const currentSketch = await this.sketchServiceClient.currentSketch(); const currentSketch = await this.sketchServiceClient.currentSketch();
this.currentSketchUri = (currentSketch && currentSketch.uri) || ''; this.currentSketchUri = (currentSketch && currentSketch.uri) || '';
} }
async updateModel(): Promise<void> {
return this.model.updateRoot();
}
protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] { protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] {
const classNames = super.createNodeClassNames(node, props); const classNames = super.createNodeClassNames(node, props);

View File

@ -1,40 +1,37 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { Command } from '@theia/core/lib/common/command'; import { Command } from '@theia/core/lib/common/command';
import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree'; import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
import { import { DirNode, FileStatNode } from '@theia/filesystem/lib/browser/file-tree';
DirNode,
FileStatNode,
FileTree,
} from '@theia/filesystem/lib/browser/file-tree';
import { SketchesService } from '../../../common/protocol'; import { SketchesService } from '../../../common/protocol';
import { FileStat } from '@theia/filesystem/lib/common/files'; import { FileStat } from '@theia/filesystem/lib/common/files';
import { SketchbookCommands } from './sketchbook-commands'; import { SketchbookCommands } from './sketchbook-commands';
import {
FileNavigatorTree,
WorkspaceNode,
} from '@theia/navigator/lib/browser/navigator-tree';
import { ArduinoPreferences } from '../../arduino-preferences';
@injectable() @injectable()
export class SketchbookTree extends FileTree { export class SketchbookTree extends FileNavigatorTree {
@inject(LabelProvider) @inject(LabelProvider)
protected readonly labelProvider: LabelProvider; protected readonly labelProvider: LabelProvider;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> { async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
if (!FileStatNode.is(parent)) { const showAllFiles =
return super.resolveChildren(parent); this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
}
const { root } = this;
if (!root) {
return [];
}
if (!SketchbookTree.RootNode.is(root)) {
return [];
}
const children = ( const children = (
await Promise.all( await Promise.all(
( (
await super.resolveChildren(parent) await super.resolveChildren(parent)
).map((node) => this.maybeDecorateNode(node, root.showAllFiles)) ).map((node) => this.maybeDecorateNode(node, showAllFiles))
) )
).filter((node) => { ).filter((node) => {
// filter out hidden nodes // filter out hidden nodes
@ -43,7 +40,9 @@ export class SketchbookTree extends FileTree {
} }
return true; return true;
}); });
if (SketchbookTree.RootNode.is(parent)) {
// filter out hardware and libraries
if (WorkspaceNode.is(parent.parent)) {
return children return children
.filter(DirNode.is) .filter(DirNode.is)
.filter( .filter(
@ -53,12 +52,16 @@ export class SketchbookTree extends FileTree {
) === -1 ) === -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 children;
// return this.filter.filter(super.resolveChildren(parent));
}
protected async maybeDecorateNode( protected async maybeDecorateNode(
node: TreeNode, node: TreeNode,
showAllFiles: boolean showAllFiles: boolean
@ -74,6 +77,9 @@ export class SketchbookTree extends FileTree {
}); });
if (!showAllFiles) { if (!showAllFiles) {
delete (node as any).expanded; delete (node as any).expanded;
node.children = [];
} else {
node.expanded = false;
} }
return node; return node;
} }

View File

@ -14,7 +14,10 @@ import { SketchbookCommands } from './sketchbook-commands';
import { WorkspaceService } from '../../theia/workspace/workspace-service'; import { WorkspaceService } from '../../theia/workspace/workspace-service';
import { import {
ContextMenuRenderer, ContextMenuRenderer,
Navigatable,
RenderContextMenuOptions, RenderContextMenuOptions,
SelectableTreeNode,
Widget,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { import {
Disposable, Disposable,
@ -77,6 +80,10 @@ export class SketchbookWidgetContribution
} }
onStart(): void { onStart(): void {
this.shell.currentChanged.connect(() =>
this.onCurrentWidgetChangedHandler()
);
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => { this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.sketchbook.showAllFiles') { if (preferenceName === 'arduino.sketchbook.showAllFiles') {
this.mainMenuManager.update(); this.mainMenuManager.update();
@ -196,4 +203,27 @@ export class SketchbookWidgetContribution
order: '0', 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<void> {
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);
}
} }

View File

@ -38,6 +38,10 @@ export class SketchbookWidget extends BaseWidget {
); );
} }
getTreeWidget(): SketchbookTreeWidget {
return this.localSketchbookTreeWidget;
}
protected onActivateRequest(message: Message): void { protected onActivateRequest(message: Message): void {
super.onActivateRequest(message); super.onActivateRequest(message);