mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-08 03:46:33 +00:00
[atl-1433][atl-1433] improve local sketchbook explorer (#446)
This commit is contained in:
parent
4e6f9ae75d
commit
4da5d573e4
@ -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
|
||||
|
@ -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,217 @@ export class SketchbookTreeModel extends FileTreeModel {
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
async updateRoot(): Promise<void> {
|
||||
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<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
|
||||
|
@ -48,23 +48,11 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
super.init();
|
||||
this.toDispose.push(
|
||||
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
|
||||
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
|
||||
this.updateModel();
|
||||
}
|
||||
})
|
||||
);
|
||||
this.updateModel();
|
||||
// cache the current open sketch uri
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
this.currentSketchUri = (currentSketch && currentSketch.uri) || '';
|
||||
}
|
||||
|
||||
async updateModel(): Promise<void> {
|
||||
return this.model.updateRoot();
|
||||
}
|
||||
|
||||
protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] {
|
||||
const classNames = super.createNodeClassNames(node, props);
|
||||
|
||||
|
@ -1,40 +1,37 @@
|
||||
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<TreeNode[]> {
|
||||
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'];
|
||||
|
||||
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 +40,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,10 +52,14 @@ 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));
|
||||
}
|
||||
|
||||
protected async maybeDecorateNode(
|
||||
@ -74,6 +77,9 @@ export class SketchbookTree extends FileTree {
|
||||
});
|
||||
if (!showAllFiles) {
|
||||
delete (node as any).expanded;
|
||||
node.children = [];
|
||||
} else {
|
||||
node.expanded = false;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
@ -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,
|
||||
@ -77,6 +80,10 @@ export class SketchbookWidgetContribution
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.shell.currentChanged.connect(() =>
|
||||
this.onCurrentWidgetChangedHandler()
|
||||
);
|
||||
|
||||
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
|
||||
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
|
||||
this.mainMenuManager.update();
|
||||
@ -196,4 +203,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<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);
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,10 @@ export class SketchbookWidget extends BaseWidget {
|
||||
);
|
||||
}
|
||||
|
||||
getTreeWidget(): SketchbookTreeWidget {
|
||||
return this.localSketchbookTreeWidget;
|
||||
}
|
||||
|
||||
protected onActivateRequest(message: Message): void {
|
||||
super.onActivateRequest(message);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user