Skip to content

Commit 4da5d57

Browse files
authored
[atl-1433][atl-1433] improve local sketchbook explorer (#446)
1 parent 4e6f9ae commit 4da5d57

File tree

6 files changed

+287
-43
lines changed

6 files changed

+287
-43
lines changed

Diff for: 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

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

+226-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,217 @@ 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+
})
94+
);
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())
38108
);
39-
const showAllFiles =
40-
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
41-
this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles);
109+
this.toDispose.push(
110+
this.workspaceService.onWorkspaceLocationChanged(() => this.updateRoot())
111+
);
112+
this.toDispose.push(
113+
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
114+
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
115+
this.updateRoot();
116+
}
117+
})
118+
);
119+
120+
if (this.selectedNodes.length) {
121+
return;
122+
}
123+
const root = this.root;
124+
if (CompositeTreeNode.is(root) && root.children.length === 1) {
125+
const child = root.children[0];
126+
if (
127+
SelectableTreeNode.is(child) &&
128+
!child.selected &&
129+
ExpandableTreeNode.is(child)
130+
) {
131+
this.selectNode(child);
132+
this.expandNode(child);
133+
}
134+
}
135+
}
136+
137+
previewNode(node: TreeNode): void {
138+
if (FileNode.is(node)) {
139+
open(this.openerService, node.uri, {
140+
mode: 'reveal',
141+
preview: true,
142+
});
143+
}
144+
}
145+
146+
*getNodesByUri(uri: URI): IterableIterator<TreeNode> {
147+
const workspace = this.root;
148+
if (WorkspaceNode.is(workspace)) {
149+
for (const root of workspace.children) {
150+
const id = this.tree.createId(root, uri);
151+
const node = this.getNode(id);
152+
if (node) {
153+
yield node;
154+
}
155+
}
156+
}
157+
}
158+
159+
public async updateRoot(): Promise<void> {
160+
this.root = await this.createRoot();
161+
}
162+
163+
protected async createRoot(): Promise<TreeNode | undefined> {
164+
const config = await this.configService.getConfiguration();
165+
const stat = await this.fileService.resolve(new URI(config.sketchDirUri));
166+
167+
if (this.workspaceService.opened) {
168+
const isMulti = stat ? !stat.isDirectory : false;
169+
const workspaceNode = isMulti
170+
? this.createMultipleRootNode()
171+
: WorkspaceNode.createRoot();
172+
workspaceNode.children.push(
173+
await this.tree.createWorkspaceRoot(stat, workspaceNode)
174+
);
175+
176+
return workspaceNode;
177+
}
178+
}
179+
180+
/**
181+
* Create multiple root node used to display
182+
* the multiple root workspace name.
183+
*
184+
* @returns `WorkspaceNode`
185+
*/
186+
protected createMultipleRootNode(): WorkspaceNode {
187+
const workspace = this.workspaceService.workspace;
188+
let name = workspace ? workspace.resource.path.name : 'untitled';
189+
name += ' (Workspace)';
190+
return WorkspaceNode.createRoot(name);
191+
}
192+
193+
/**
194+
* Move the given source file or directory to the given target directory.
195+
*/
196+
async move(source: TreeNode, target: TreeNode): Promise<URI | undefined> {
197+
if (source.parent && WorkspaceRootNode.is(source)) {
198+
// do not support moving a root folder
199+
return undefined;
200+
}
201+
return super.move(source, target);
202+
}
203+
204+
/**
205+
* Reveals node in the navigator by given file uri.
206+
*
207+
* @param uri uri to file which should be revealed in the navigator
208+
* @returns file tree node if the file with given uri was revealed, undefined otherwise
209+
*/
210+
async revealFile(uri: URI): Promise<TreeNode | undefined> {
211+
if (!uri.path.isAbsolute) {
212+
return undefined;
213+
}
214+
let node = this.getNodeClosestToRootByUri(uri);
215+
216+
// success stop condition
217+
// we have to reach workspace root because expanded node could be inside collapsed one
218+
if (WorkspaceRootNode.is(node)) {
219+
if (ExpandableTreeNode.is(node)) {
220+
if (!node.expanded) {
221+
node = await this.expandNode(node);
222+
}
223+
return node;
224+
}
225+
// shouldn't happen, root node is always directory, i.e. expandable
226+
return undefined;
227+
}
228+
229+
// fail stop condition
230+
if (uri.path.isRoot) {
231+
// file system root is reached but workspace root wasn't found, it means that
232+
// given uri is not in workspace root folder or points to not existing file.
233+
return undefined;
234+
}
235+
236+
if (await this.revealFile(uri.parent)) {
237+
if (node === undefined) {
238+
// get node if it wasn't mounted into navigator tree before expansion
239+
node = this.getNodeClosestToRootByUri(uri);
240+
}
241+
if (ExpandableTreeNode.is(node) && !node.expanded) {
242+
node = await this.expandNode(node);
243+
}
244+
return node;
245+
}
246+
return undefined;
247+
}
248+
249+
protected getNodeClosestToRootByUri(uri: URI): TreeNode | undefined {
250+
const nodes = [...this.getNodesByUri(uri)];
251+
return nodes.length > 0
252+
? nodes.reduce(
253+
(
254+
node1,
255+
node2 // return the node closest to the workspace root
256+
) => (node1.id.length >= node2.id.length ? node1 : node2)
257+
)
258+
: undefined;
42259
}
43260

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

Diff for: arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx

-12
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,11 @@ export class SketchbookTreeWidget extends FileTreeWidget {
4848
@postConstruct()
4949
protected async init(): Promise<void> {
5050
super.init();
51-
this.toDispose.push(
52-
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
53-
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
54-
this.updateModel();
55-
}
56-
})
57-
);
58-
this.updateModel();
5951
// cache the current open sketch uri
6052
const currentSketch = await this.sketchServiceClient.currentSketch();
6153
this.currentSketchUri = (currentSketch && currentSketch.uri) || '';
6254
}
6355

64-
async updateModel(): Promise<void> {
65-
return this.model.updateRoot();
66-
}
67-
6856
protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] {
6957
const classNames = super.createNodeClassNames(node, props);
7058

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

+27-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,10 +52,14 @@ 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+
// return this.filter.filter(super.resolveChildren(parent));
6063
}
6164

6265
protected async maybeDecorateNode(
@@ -74,6 +77,9 @@ export class SketchbookTree extends FileTree {
7477
});
7578
if (!showAllFiles) {
7679
delete (node as any).expanded;
80+
node.children = [];
81+
} else {
82+
node.expanded = false;
7783
}
7884
return node;
7985
}

0 commit comments

Comments
 (0)