Skip to content

Commit 072c656

Browse files
committed
[atl-1433][atl-1433] improve sketchbook explorer
1 parent e10f0f1 commit 072c656

File tree

5 files changed

+318
-31
lines changed

5 files changed

+318
-31
lines changed

arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ export class FilterableListContainer<
9494
}
9595

9696
protected sort(items: T[]): T[] {
97-
// debugger;
9897
const { itemLabel, itemDeprecated } = this.props;
9998
return items.sort((left, right) => {
10099
// always put deprecated items at the bottom of the list

arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-model.ts

+219-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
import { inject, injectable } from 'inversify';
1+
import { inject, injectable, postConstruct } from 'inversify';
22
import URI from '@theia/core/lib/common/uri';
33
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
44
import { FileService } from '@theia/filesystem/lib/browser/file-service';
55
import { ConfigService } from '../../../common/protocol';
66
import { SketchbookTree } from './sketchbook-tree';
77
import { ArduinoPreferences } from '../../arduino-preferences';
8-
import { SelectableTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
8+
import {
9+
CompositeTreeNode,
10+
ExpandableTreeNode,
11+
SelectableTreeNode,
12+
TreeNode,
13+
} from '@theia/core/lib/browser/tree';
914
import { SketchbookCommands } from './sketchbook-commands';
1015
import { OpenerService, open } from '@theia/core/lib/browser';
1116
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
1217
import { CommandRegistry } from '@theia/core/lib/common/command';
18+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
19+
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
20+
import { ProgressService } from '@theia/core/lib/common/progress-service';
21+
import {
22+
WorkspaceNode,
23+
WorkspaceRootNode,
24+
} from '@theia/navigator/lib/browser/navigator-tree';
25+
import { Deferred } from '@theia/core/lib/common/promise-util';
26+
import { Disposable } from '@theia/core/lib/common/disposable';
1327

1428
@injectable()
1529
export class SketchbookTreeModel extends FileTreeModel {
@@ -31,14 +45,210 @@ export class SketchbookTreeModel extends FileTreeModel {
3145
@inject(SketchesServiceClientImpl)
3246
protected readonly sketchServiceClient: SketchesServiceClientImpl;
3347

34-
async updateRoot(): Promise<void> {
35-
const config = await this.configService.getConfiguration();
36-
const fileStat = await this.fileService.resolve(
37-
new URI(config.sketchDirUri)
48+
@inject(SketchbookTree) protected readonly tree: SketchbookTree;
49+
@inject(WorkspaceService)
50+
protected readonly workspaceService: WorkspaceService;
51+
@inject(FrontendApplicationStateService)
52+
protected readonly applicationState: FrontendApplicationStateService;
53+
54+
@inject(ProgressService)
55+
protected readonly progressService: ProgressService;
56+
57+
@postConstruct()
58+
protected init(): void {
59+
super.init();
60+
this.reportBusyProgress();
61+
this.initializeRoot();
62+
}
63+
64+
protected readonly pendingBusyProgress = new Map<string, Deferred<void>>();
65+
protected reportBusyProgress(): void {
66+
this.toDispose.push(
67+
this.onDidChangeBusy((node) => {
68+
const pending = this.pendingBusyProgress.get(node.id);
69+
if (pending) {
70+
if (!node.busy) {
71+
pending.resolve();
72+
this.pendingBusyProgress.delete(node.id);
73+
}
74+
return;
75+
}
76+
if (node.busy) {
77+
const progress = new Deferred<void>();
78+
this.pendingBusyProgress.set(node.id, progress);
79+
this.progressService.withProgress(
80+
'',
81+
'explorer',
82+
() => progress.promise
83+
);
84+
}
85+
})
86+
);
87+
this.toDispose.push(
88+
Disposable.create(() => {
89+
for (const pending of this.pendingBusyProgress.values()) {
90+
pending.resolve();
91+
}
92+
this.pendingBusyProgress.clear();
93+
})
3894
);
39-
const showAllFiles =
40-
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
41-
this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles);
95+
}
96+
97+
protected async initializeRoot(): Promise<void> {
98+
await Promise.all([
99+
this.applicationState.reachedState('initialized_layout'),
100+
this.workspaceService.roots,
101+
]);
102+
await this.updateRoot();
103+
if (this.toDispose.disposed) {
104+
return;
105+
}
106+
this.toDispose.push(
107+
this.workspaceService.onWorkspaceChanged(() => this.updateRoot())
108+
);
109+
this.toDispose.push(
110+
this.workspaceService.onWorkspaceLocationChanged(() => this.updateRoot())
111+
);
112+
if (this.selectedNodes.length) {
113+
return;
114+
}
115+
const root = this.root;
116+
if (CompositeTreeNode.is(root) && root.children.length === 1) {
117+
const child = root.children[0];
118+
if (
119+
SelectableTreeNode.is(child) &&
120+
!child.selected &&
121+
ExpandableTreeNode.is(child)
122+
) {
123+
this.selectNode(child);
124+
this.expandNode(child);
125+
}
126+
}
127+
}
128+
129+
previewNode(node: TreeNode): void {
130+
if (FileNode.is(node)) {
131+
open(this.openerService, node.uri, {
132+
mode: 'reveal',
133+
preview: true,
134+
});
135+
}
136+
}
137+
138+
*getNodesByUri(uri: URI): IterableIterator<TreeNode> {
139+
const workspace = this.root;
140+
if (WorkspaceNode.is(workspace)) {
141+
for (const root of workspace.children) {
142+
const id = this.tree.createId(root, uri);
143+
const node = this.getNode(id);
144+
if (node) {
145+
yield node;
146+
}
147+
}
148+
}
149+
}
150+
151+
public async updateRoot(): Promise<void> {
152+
this.root = await this.createRoot();
153+
}
154+
155+
protected async createRoot(): Promise<TreeNode | undefined> {
156+
const config = await this.configService.getConfiguration();
157+
const stat = await this.fileService.resolve(new URI(config.sketchDirUri));
158+
159+
if (this.workspaceService.opened) {
160+
const isMulti = stat ? !stat.isDirectory : false;
161+
const workspaceNode = isMulti
162+
? this.createMultipleRootNode()
163+
: WorkspaceNode.createRoot();
164+
165+
workspaceNode.children.push(
166+
await this.tree.createWorkspaceRoot(stat, workspaceNode)
167+
);
168+
169+
return workspaceNode;
170+
}
171+
}
172+
173+
/**
174+
* Create multiple root node used to display
175+
* the multiple root workspace name.
176+
*
177+
* @returns `WorkspaceNode`
178+
*/
179+
protected createMultipleRootNode(): WorkspaceNode {
180+
const workspace = this.workspaceService.workspace;
181+
let name = workspace ? workspace.resource.path.name : 'untitled';
182+
name += ' (Workspace)';
183+
return WorkspaceNode.createRoot(name);
184+
}
185+
186+
/**
187+
* Move the given source file or directory to the given target directory.
188+
*/
189+
async move(source: TreeNode, target: TreeNode): Promise<URI | undefined> {
190+
if (source.parent && WorkspaceRootNode.is(source)) {
191+
// do not support moving a root folder
192+
return undefined;
193+
}
194+
return super.move(source, target);
195+
}
196+
197+
/**
198+
* Reveals node in the navigator by given file uri.
199+
*
200+
* @param uri uri to file which should be revealed in the navigator
201+
* @returns file tree node if the file with given uri was revealed, undefined otherwise
202+
*/
203+
async revealFile(uri: URI): Promise<TreeNode | undefined> {
204+
if (!uri.path.isAbsolute) {
205+
return undefined;
206+
}
207+
let node = this.getNodeClosestToRootByUri(uri);
208+
209+
// success stop condition
210+
// we have to reach workspace root because expanded node could be inside collapsed one
211+
if (WorkspaceRootNode.is(node)) {
212+
if (ExpandableTreeNode.is(node)) {
213+
if (!node.expanded) {
214+
node = await this.expandNode(node);
215+
}
216+
return node;
217+
}
218+
// shouldn't happen, root node is always directory, i.e. expandable
219+
return undefined;
220+
}
221+
222+
// fail stop condition
223+
if (uri.path.isRoot) {
224+
// file system root is reached but workspace root wasn't found, it means that
225+
// given uri is not in workspace root folder or points to not existing file.
226+
return undefined;
227+
}
228+
229+
if (await this.revealFile(uri.parent)) {
230+
if (node === undefined) {
231+
// get node if it wasn't mounted into navigator tree before expansion
232+
node = this.getNodeClosestToRootByUri(uri);
233+
}
234+
if (ExpandableTreeNode.is(node) && !node.expanded) {
235+
node = await this.expandNode(node);
236+
}
237+
return node;
238+
}
239+
return undefined;
240+
}
241+
242+
protected getNodeClosestToRootByUri(uri: URI): TreeNode | undefined {
243+
const nodes = [...this.getNodesByUri(uri)];
244+
return nodes.length > 0
245+
? nodes.reduce(
246+
(
247+
node1,
248+
node2 // return the node closest to the workspace root
249+
) => (node1.id.length >= node2.id.length ? node1 : node2)
250+
)
251+
: undefined;
42252
}
43253

44254
// selectNode gets called when the user single-clicks on an item

arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts

+65-21
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,37 @@
11
import { inject, injectable } from 'inversify';
22
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
33
import { Command } from '@theia/core/lib/common/command';
4-
import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree';
5-
import {
6-
DirNode,
7-
FileStatNode,
8-
FileTree,
9-
} from '@theia/filesystem/lib/browser/file-tree';
4+
import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
5+
import { DirNode, FileStatNode } from '@theia/filesystem/lib/browser/file-tree';
106
import { SketchesService } from '../../../common/protocol';
117
import { FileStat } from '@theia/filesystem/lib/common/files';
128
import { SketchbookCommands } from './sketchbook-commands';
9+
import {
10+
FileNavigatorTree,
11+
WorkspaceNode,
12+
} from '@theia/navigator/lib/browser/navigator-tree';
13+
import { ArduinoPreferences } from '../../arduino-preferences';
1314

1415
@injectable()
15-
export class SketchbookTree extends FileTree {
16+
export class SketchbookTree extends FileNavigatorTree {
1617
@inject(LabelProvider)
1718
protected readonly labelProvider: LabelProvider;
1819

1920
@inject(SketchesService)
2021
protected readonly sketchesService: SketchesService;
2122

23+
@inject(ArduinoPreferences)
24+
protected readonly arduinoPreferences: ArduinoPreferences;
25+
2226
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
23-
if (!FileStatNode.is(parent)) {
24-
return super.resolveChildren(parent);
25-
}
26-
const { root } = this;
27-
if (!root) {
28-
return [];
29-
}
30-
if (!SketchbookTree.RootNode.is(root)) {
31-
return [];
32-
}
27+
const showAllFiles =
28+
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
29+
3330
const children = (
3431
await Promise.all(
3532
(
3633
await super.resolveChildren(parent)
37-
).map((node) => this.maybeDecorateNode(node, root.showAllFiles))
34+
).map((node) => this.maybeDecorateNode(node, showAllFiles))
3835
)
3936
).filter((node) => {
4037
// filter out hidden nodes
@@ -43,7 +40,9 @@ export class SketchbookTree extends FileTree {
4340
}
4441
return true;
4542
});
46-
if (SketchbookTree.RootNode.is(parent)) {
43+
44+
// filter out hardware and libraries
45+
if (WorkspaceNode.is(parent.parent)) {
4746
return children
4847
.filter(DirNode.is)
4948
.filter(
@@ -53,12 +52,57 @@ export class SketchbookTree extends FileTree {
5352
) === -1
5453
);
5554
}
56-
if (SketchbookTree.SketchDirNode.is(parent)) {
57-
return children.filter(FileStatNode.is);
55+
56+
// return the Arduino directory containing all user sketches
57+
if (WorkspaceNode.is(parent)) {
58+
return children;
5859
}
60+
5961
return children;
62+
63+
// return this.filter.filter(super.resolveChildren(parent));
6064
}
6165

66+
// async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
67+
// if (!FileStatNode.is(parent)) {
68+
// return super.resolveChildren(parent);
69+
// }
70+
// const { root } = this;
71+
// if (!root) {
72+
// return [];
73+
// }
74+
// if (!SketchbookTree.RootNode.is(root)) {
75+
// return [];
76+
// }
77+
// const children = (
78+
// await Promise.all(
79+
// (
80+
// await super.resolveChildren(parent)
81+
// ).map((node) => this.maybeDecorateNode(node, root.showAllFiles))
82+
// )
83+
// ).filter((node) => {
84+
// // filter out hidden nodes
85+
// if (DirNode.is(node) || FileStatNode.is(node)) {
86+
// return node.fileStat.name.indexOf('.') !== 0;
87+
// }
88+
// return true;
89+
// });
90+
// if (SketchbookTree.RootNode.is(parent)) {
91+
// return children
92+
// .filter(DirNode.is)
93+
// .filter(
94+
// (node) =>
95+
// ['libraries', 'hardware'].indexOf(
96+
// this.labelProvider.getName(node)
97+
// ) === -1
98+
// );
99+
// }
100+
// if (SketchbookTree.SketchDirNode.is(parent)) {
101+
// return children.filter(FileStatNode.is);
102+
// }
103+
// return children;
104+
// }
105+
62106
protected async maybeDecorateNode(
63107
node: TreeNode,
64108
showAllFiles: boolean

0 commit comments

Comments
 (0)