mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-08 20:06:32 +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[] {
|
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
|
||||||
|
@ -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.toDispose.push(
|
||||||
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
|
Disposable.create(() => {
|
||||||
this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles);
|
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
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,10 +52,14 @@ 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(
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user