Improve remote sketchbook explorer (#459)

* Refactor remote sketchbook explorer
* sketches sorting
This commit is contained in:
Francesco Stasi
2021-07-22 14:34:10 +02:00
committed by GitHub
parent 4da5d573e4
commit d790266cc8
16 changed files with 592 additions and 613 deletions

View File

@@ -0,0 +1,34 @@
import { FileStat } from '@theia/filesystem/lib/common/files';
import { injectable } from 'inversify';
import { toPosixPath } from '../../create/create-paths';
import { Create } from '../../create/typings';
@injectable()
export class SketchCache {
sketches: Record<string, Create.Sketch> = {};
fileStats: Record<string, FileStat> = {};
init(): void {
// reset the data
this.sketches = {};
this.fileStats = {};
}
addItem(item: FileStat): void {
this.fileStats[item.resource.path.toString()] = item;
}
getItem(path: string): FileStat | null {
return this.fileStats[path] || null;
}
addSketch(sketch: Create.Sketch): void {
const { path } = sketch;
const posixPath = toPosixPath(path);
this.sketches[posixPath] = sketch;
}
getSketch(path: string): Create.Sketch | null {
return this.sketches[path] || null;
}
}

View File

@@ -35,6 +35,10 @@ export class CloudSketchbookCompositeWidget extends BaseWidget {
this.id = 'cloud-sketchbook-composite-widget';
}
public getTreeWidget(): CloudSketchbookTreeWidget {
return this.cloudSketchbookTreeWidget;
}
protected onAfterAttach(message: Message): void {
super.onAfterAttach(message);
Widget.attach(this.cloudSketchbookTreeWidget, this.compositeNode);

View File

@@ -166,11 +166,11 @@ export class CloudSketchbookContribution extends Contribution {
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
!!arg.node.synced,
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
!!arg.node.synced,
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
});
registry.registerCommand(CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR, {
@@ -257,18 +257,10 @@ export class CloudSketchbookContribution extends Contribution {
const currentSketch = await this.sketchServiceClient.currentSketch();
const localUri = await arg.model.cloudSketchbookTree.localUri(
arg.node
);
let underlying = null;
if (arg.node && localUri) {
underlying = await this.fileService.toUnderlyingResource(localUri);
}
// disable the "open sketch" command for the current sketch and for those not in sync
if (
!underlying ||
(currentSketch && currentSketch.uri === underlying.toString())
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node) ||
(currentSketch && currentSketch.uri === arg.node.uri.toString())
) {
const placeholder = new PlaceholderMenuNode(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
@@ -284,7 +276,6 @@ export class CloudSketchbookContribution extends Contribution {
)
);
} else {
arg.node.uri = localUri;
this.menuRegistry.registerMenuAction(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
{

View File

@@ -1,81 +1,76 @@
import { inject, injectable, postConstruct } from 'inversify';
import { TreeNode } from '@theia/core/lib/browser/tree';
import { toPosixPath, posixSegments, posix } from '../../create/create-paths';
import { CreateApi, Create } from '../../create/create-api';
import { posixSegments, splitSketchPath } from '../../create/create-paths';
import { CreateApi } from '../../create/create-api';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import {
LocalCacheFsProvider,
LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
import { ArduinoPreferences } from '../../arduino-preferences';
import { ConfigService } from '../../../common/protocol';
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
import { CreateUri } from '../../create/create-uri';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { LocalCacheFsProvider } from '../../local-cache/local-cache-fs-provider';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import URI from '@theia/core/lib/common/uri';
import { SketchCache } from './cloud-sketch-cache';
import { Create } from '../../create/typings';
export type CreateCache = Record<string, Create.Resource>;
export namespace CreateCache {
export function build(resources: Create.Resource[]): CreateCache {
const treeData: CreateCache = {};
treeData[posix.sep] = CloudSketchbookTree.rootResource;
for (const resource of resources) {
const { path } = resource;
const posixPath = toPosixPath(path);
if (treeData[posixPath] !== undefined) {
throw new Error(
`Already visited resource for path: ${posixPath}.\nData: ${JSON.stringify(
treeData,
null,
2
)}`
);
}
treeData[posixPath] = resource;
}
return treeData;
export function sketchBaseDir(sketch: Create.Sketch): FileStat {
// extract the sketch path
const [, path] = splitSketchPath(sketch.path);
const dirs = posixSegments(path);
const mtime = Date.parse(sketch.modified_at);
const ctime = Date.parse(sketch.created_at);
const createPath = CreateUri.toUri(dirs[0]);
const baseDir: FileStat = {
name: dirs[0],
isDirectory: true,
isFile: false,
isSymbolicLink: false,
resource: createPath,
mtime,
ctime,
};
return baseDir;
}
export function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] {
const sketchesBaseDirs: Record<string, FileStat> = {};
for (const sketch of sketches) {
const sketchBaseDirFileStat = sketchBaseDir(sketch);
sketchesBaseDirs[sketchBaseDirFileStat.resource.toString()] =
sketchBaseDirFileStat;
}
export function childrenOf(
resource: Create.Resource,
cache: CreateCache
): Create.Resource[] | undefined {
if (resource.type === 'file') {
return undefined;
}
const posixPath = toPosixPath(resource.path);
const childSegmentCount = posixSegments(posixPath).length + 1;
return Object.keys(cache)
.filter(
(key) =>
key.startsWith(posixPath) &&
posixSegments(key).length === childSegmentCount
)
.map((childPosixPath) => cache[childPosixPath]);
}
return Object.keys(sketchesBaseDirs).map(
(dirUri) => sketchesBaseDirs[dirUri]
);
}
@injectable()
export class CloudSketchbookTreeModel extends SketchbookTreeModel {
@inject(FileService)
protected readonly fileService: FileService;
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(CloudSketchbookTree)
protected readonly cloudSketchbookTree: CloudSketchbookTree;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(CommandRegistry)
public readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchCache)
protected readonly sketchCache: SketchCache;
@postConstruct()
protected init(): void {
@@ -85,56 +80,25 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
);
}
async updateRoot(): Promise<void> {
async createRoot(): Promise<TreeNode | undefined> {
const { session } = this.authenticationService;
if (!session) {
this.tree.root = undefined;
return;
}
this.createApi.init(this.authenticationService, this.arduinoPreferences);
const resources = await this.createApi.readDirectory(posix.sep, {
recursive: true,
secrets: true,
});
const cache = CreateCache.build(resources);
// also read local files
for await (const path of Object.keys(cache)) {
if (cache[path].type === 'sketch') {
const localUri = LocalCacheUri.root.resolve(path);
const exists = await this.fileService.exists(localUri);
if (exists) {
const fileStat = await this.fileService.resolve(localUri);
// add every missing file
fileStat.children
?.filter(
(child) =>
!Object.keys(cache).includes(path + posix.sep + child.name)
)
.forEach((child) => {
const localChild: Create.Resource = {
modified_at: '',
href: cache[path].href + posix.sep + child.name,
mimetype: '',
name: child.name,
path: cache[path].path + posix.sep + child.name,
sketchId: '',
type: child.isFile ? 'file' : 'folder',
};
cache[path + posix.sep + child.name] = localChild;
});
}
this.sketchCache.init();
const sketches = await this.createApi.sketches();
const rootFileStats = sketchesToFileStats(sketches);
if (this.workspaceService.opened) {
const workspaceNode = WorkspaceNode.createRoot('Remote');
for await (const stat of rootFileStats) {
workspaceNode.children.push(
await this.tree.createWorkspaceRoot(stat, workspaceNode)
);
}
return workspaceNode;
}
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
this.tree.root = CloudSketchbookTree.CloudRootNode.create(
cache,
showAllFiles
);
}
sketchbookTree(): CloudSketchbookTree {
@@ -143,9 +107,6 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
protected recursivelyFindSketchRoot(node: TreeNode): any {
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
if (node.hasOwnProperty('underlying')) {
return { ...node, uri: node.underlying };
}
return node;
}
@@ -156,4 +117,15 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
// can't find a root, return false
return false;
}
async revealFile(uri: URI): Promise<TreeNode | undefined> {
// we use remote uris as keys for the tree
// convert local URIs
const remoteuri = this.localCacheFsProvider.from(uri);
if (remoteuri) {
return super.revealFile(remoteuri);
} else {
return super.revealFile(uri);
}
}
}

View File

@@ -91,7 +91,7 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
node.commands &&
(node.id === this.hoveredNodeId ||
this.currentSketchUri === node.underlying?.toString())
this.currentSketchUri === node.uri.toString())
) {
return Array.from(new Set(node.commands)).map((command) =>
this.renderInlineCommand(command.id, node)
@@ -135,37 +135,17 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
);
}
protected async handleClickEvent(
node: any,
protected handleDblClickEvent(
node: TreeNode,
event: React.MouseEvent<HTMLElement>
) {
): void {
event.persist();
let uri = node.uri;
// overwrite the uri using the local-cache
const localUri = await this.cloudSketchbookTree.localUri(node);
if (node && localUri) {
const underlying = await this.fileService.toUnderlyingResource(localUri);
uri = underlying;
}
super.handleClickEvent({ ...node, uri }, event);
}
protected async handleDblClickEvent(
node: any,
event: React.MouseEvent<HTMLElement>
) {
event.persist();
let uri = node.uri;
// overwrite the uri using the local-cache
// if the localURI does not exists, ignore the double click, so that the sketch is not opened
const localUri = await this.cloudSketchbookTree.localUri(node);
if (node && localUri) {
const underlying = await this.fileService.toUnderlyingResource(localUri);
uri = underlying;
super.handleDblClickEvent({ ...node, uri }, event);
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
) {
super.handleDblClickEvent(node, event);
}
}
}

View File

@@ -1,15 +1,15 @@
import { SketchCache } from './cloud-sketch-cache';
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { MaybePromise } from '@theia/core/lib/common/types';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStatNode } from '@theia/filesystem/lib/browser/file-tree';
import { Command } from '@theia/core/lib/common/command';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { DecoratedTreeNode } from '@theia/core/lib/browser/tree/tree-decorator';
import {
FileNode,
DirNode,
FileNode,
} from '@theia/filesystem/lib/browser/file-tree/file-tree';
import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree';
import {
@@ -18,20 +18,21 @@ import {
} from '@theia/core/lib/browser/preferences/preference-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { REMOTE_ONLY_FILES } from './../../create/create-fs-provider';
import { posix } from '../../create/create-paths';
import { Create, CreateApi } from '../../create/create-api';
import { CreateApi } from '../../create/create-api';
import { CreateUri } from '../../create/create-uri';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import {
CloudSketchbookTreeModel,
CreateCache,
} from './cloud-sketchbook-tree-model';
import { LocalCacheUri } from '../../local-cache/local-cache-fs-provider';
LocalCacheFsProvider,
LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider';
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
import { DoNotAskAgainConfirmDialog } from '../../dialogs.ts/dialogs';
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
import { firstToUpperCase } from '../../../common/utils';
import { ArduinoPreferences } from '../../arduino-preferences';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
const MESSAGE_TIMEOUT = 5 * 1000;
const deepmerge = require('deepmerge').default;
@@ -41,6 +42,12 @@ export class CloudSketchbookTree extends SketchbookTree {
@inject(FileService)
protected readonly fileService: FileService;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(SketchCache)
protected readonly sketchCache: SketchCache;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@@ -95,7 +102,8 @@ export class CloudSketchbookTree extends SketchbookTree {
} = arg;
const warn =
node.synced && this.arduinoPreferences['arduino.cloud.pull.warn'];
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
this.arduinoPreferences['arduino.cloud.pull.warn'];
if (warn) {
const ok = await new DoNotAskAgainConfirmDialog({
@@ -120,11 +128,9 @@ export class CloudSketchbookTree extends SketchbookTree {
node.commands = [];
// check if the sketch dir already exist
if (node.synced) {
if (CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
const filesToPull = (
await this.createApi.readDirectory(node.uri.path.toString(), {
secrets: true,
})
await this.createApi.readDirectory(node.remoteUri.path.toString())
).filter((file: any) => !REMOTE_ONLY_FILES.includes(file.name));
await Promise.all(
@@ -140,9 +146,9 @@ export class CloudSketchbookTree extends SketchbookTree {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (
!CreateUri.is(node.uri) &&
currentSketch &&
node.underlying &&
currentSketch.uri === node.underlying.toString()
currentSketch.uri === node.uri.toString()
) {
filesToPull.forEach(async (file) => {
const localUri = LocalCacheUri.root.resolve(
@@ -157,7 +163,7 @@ export class CloudSketchbookTree extends SketchbookTree {
}
} else {
await this.fileService.copy(
node.uri,
node.remoteUri,
LocalCacheUri.root.resolve(node.uri.path),
{ overwrite: true }
);
@@ -171,7 +177,7 @@ export class CloudSketchbookTree extends SketchbookTree {
}
async push(node: CloudSketchbookTree.CloudSketchDirNode): Promise<void> {
if (!node.synced) {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error('Cannot push to Cloud. It is not yet pulled.');
}
@@ -201,20 +207,17 @@ export class CloudSketchbookTree extends SketchbookTree {
}
}
this.runWithState(node, 'pushing', async (node) => {
if (!node.synced) {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
'You have to pull first to be able to push to the Cloud.'
);
}
const commandsCopy = node.commands;
node.commands = [];
// delete every first level file, then push everything
const result = await this.fileService.copy(
LocalCacheUri.root.resolve(node.uri.path),
node.uri,
{ overwrite: true }
);
const result = await this.fileService.copy(node.uri, node.remoteUri, {
overwrite: true,
});
node.commands = commandsCopy;
this.messageService.info(`Done pushing ${result.name}.`, {
timeout: MESSAGE_TIMEOUT,
@@ -225,23 +228,10 @@ export class CloudSketchbookTree extends SketchbookTree {
async refresh(
node?: CompositeTreeNode
): Promise<CompositeTreeNode | undefined> {
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
const localUri = await this.localUri(node);
if (localUri) {
node.synced = true;
if (
node.commands?.indexOf(CloudSketchbookCommands.PUSH_SKETCH) === -1
) {
node.commands.splice(1, 0, CloudSketchbookCommands.PUSH_SKETCH);
}
// remove italic from synced nodes
if (
'decorationData' in node &&
'fontData' in (node as any).decorationData
) {
delete (node as any).decorationData.fontData;
}
}
if (node) {
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
await this.decorateNode(node, showAllFiles);
}
return super.refresh(node);
}
@@ -276,20 +266,140 @@ export class CloudSketchbookTree extends SketchbookTree {
}
}
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
return (await super.resolveChildren(parent)).sort((a, b) => {
if (
WorkspaceNode.is(parent) &&
FileStatNode.is(a) &&
FileStatNode.is(b)
) {
const syncNodeA =
CloudSketchbookTree.CloudSketchTreeNode.is(a) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(a);
const syncNodeB =
CloudSketchbookTree.CloudSketchTreeNode.is(b) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(b);
const syncComparison = Number(syncNodeB) - Number(syncNodeA);
// same sync status, compare on modified time
if (syncComparison === 0) {
return (a.fileStat.mtime || 0) - (b.fileStat.mtime || 0);
}
return syncComparison;
}
return 0;
});
}
/**
* Retrieve fileStats for the given node, merging the local and remote childrens
* Local children take prevedence over remote ones
* @param node
* @returns
*/
protected async resolveFileStat(
node: FileStatNode
): Promise<FileStat | undefined> {
if (
CreateUri.is(node.uri) &&
CloudSketchbookTree.CloudRootNode.is(this.root)
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
CreateUri.is(node.remoteUri)
) {
const resource = this.root.cache[node.uri.path.toString()];
if (!resource) {
return undefined;
let remoteFileStat: FileStat;
const cacheHit = this.sketchCache.getItem(node.remoteUri.path.toString());
if (cacheHit) {
remoteFileStat = cacheHit;
} else {
// not found, fetch and add it for future calls
remoteFileStat = await this.fileService.resolve(node.remoteUri);
if (remoteFileStat) {
this.sketchCache.addItem(remoteFileStat);
}
}
return CloudSketchbookTree.toFileStat(resource, this.root.cache, 1);
const children: FileStat[] = [...(remoteFileStat?.children || [])];
const childrenLocalPaths = children.map((child) => {
return (
this.localCacheFsProvider.currentUserUri.path.toString() +
child.resource.path.toString()
);
});
// if the node is in sync, also get local-only children
if (CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
const localFileStat = await this.fileService.resolve(node.uri);
// merge the two children
for (const child of localFileStat.children || []) {
if (!childrenLocalPaths.includes(child.resource.path.toString())) {
children.push(child);
}
}
}
// add a remote uri for the children. it's used as ID for the nodes
const childrenWithRemoteUri: FileStat[] = await Promise.all(
children.map(async (childFs) => {
let remoteUri: URI = childFs.resource;
if (!CreateUri.is(childFs.resource)) {
let refUri = node.fileStat.resource;
if (node.fileStat.hasOwnProperty('remoteUri')) {
refUri = (node.fileStat as any).remoteUri;
}
remoteUri = refUri.resolve(childFs.name);
}
return { ...childFs, remoteUri };
})
);
const fileStat = { ...remoteFileStat, children: childrenWithRemoteUri };
node.fileStat = fileStat;
return fileStat;
} else {
// it's a local-only file
return super.resolveFileStat(node);
}
return super.resolveFileStat(node);
}
protected toNode(
fileStat: any,
parent: CompositeTreeNode
): FileNode | DirNode {
const uri = fileStat.resource;
let idUri;
if (fileStat.remoteUri) {
idUri = fileStat.remoteUri;
}
const id = this.toNodeId(idUri || uri, parent);
const node = this.getNode(id);
if (fileStat.isDirectory) {
if (DirNode.is(node)) {
node.fileStat = fileStat;
return node;
}
return <DirNode>{
id,
uri,
fileStat,
parent,
expanded: false,
selected: false,
children: [],
};
}
if (FileNode.is(node)) {
node.fileStat = fileStat;
return node;
}
return <FileNode>{
id,
uri,
fileStat,
parent,
selected: false,
};
}
protected readonly notInSyncDecoration: WidgetDecoration.Data = {
@@ -297,75 +407,90 @@ export class CloudSketchbookTree extends SketchbookTree {
color: 'var(--theia-activityBar-inactiveForeground)',
},
};
protected async toNodes(
fileStat: FileStat,
parent: CompositeTreeNode
): Promise<CloudSketchbookTree.CloudSketchTreeNode[]> {
const children = await super.toNodes(fileStat, parent);
for (const child of children.filter(FileStatNode.is)) {
if (!CreateFileStat.is(child.fileStat)) {
continue;
}
const localUri = await this.localUri(child);
let underlying = null;
if (localUri) {
underlying = await this.fileService.toUnderlyingResource(localUri);
Object.assign(child, { underlying });
}
protected readonly inSyncDecoration: WidgetDecoration.Data = {
fontData: {},
};
if (CloudSketchbookTree.CloudSketchDirNode.is(child)) {
if (child.fileStat.sketchId) {
child.sketchId = child.fileStat.sketchId;
child.isPublic = child.fileStat.isPublic;
}
const commands = [CloudSketchbookCommands.PULL_SKETCH];
/**
* Add commands available to the given node.
* In the case the node is a sketch, it also adds sketchId and isPublic flags
* @param node
* @returns
*/
protected async augmentSketchNode(node: DirNode): Promise<void> {
const sketch = this.sketchCache.getSketch(
node.fileStat.resource.path.toString()
);
if (underlying) {
child.synced = true;
commands.push(CloudSketchbookCommands.PUSH_SKETCH);
} else {
this.mergeDecoration(child, this.notInSyncDecoration);
}
const commands = [CloudSketchbookCommands.PULL_SKETCH];
commands.push(CloudSketchbookCommands.OPEN_SKETCHBOOKSYNC_CONTEXT_MENU);
Object.assign(child, { commands });
if (!this.showAllFiles) {
delete (child as any).expanded;
}
} else if (CloudSketchbookTree.CloudSketchDirNode.is(parent)) {
if (!parent.synced) {
this.mergeDecoration(child, this.notInSyncDecoration);
} else {
this.setDecoration(
child,
underlying ? undefined : this.notInSyncDecoration
);
}
}
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
) {
commands.push(CloudSketchbookCommands.PUSH_SKETCH);
}
if (CloudSketchbookTree.SketchDirNode.is(parent) && !this.showAllFiles) {
return [];
}
return children;
commands.push(CloudSketchbookCommands.OPEN_SKETCHBOOKSYNC_CONTEXT_MENU);
Object.assign(node, {
type: 'sketch',
...(sketch && {
isPublic: sketch.is_public,
}),
...(sketch && {
sketchId: sketch.id,
}),
commands,
});
}
protected toNode(
fileStat: FileStat,
parent: CompositeTreeNode
): FileNode | DirNode {
const node = super.toNode(fileStat, parent);
if (CreateFileStat.is(fileStat)) {
Object.assign(node, {
type: fileStat.type,
isPublic: fileStat.isPublic,
sketchId: fileStat.sketchId,
});
protected async nodeLocalUri(node: TreeNode): Promise<TreeNode> {
if (FileStatNode.is(node) && CreateUri.is(node.uri)) {
Object.assign(node, { remoteUri: node.uri });
const localUri = await this.localUri(node);
if (localUri) {
// if the node has a local uri, use it
const underlying = await this.fileService.toUnderlyingResource(
localUri
);
node.uri = underlying;
}
}
// add style decoration for not-in-sync files
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
) {
this.mergeDecoration(node, this.notInSyncDecoration);
} else {
this.removeDecoration(node, this.notInSyncDecoration);
}
return node;
}
protected async decorateNode(
node: TreeNode,
showAllFiles: boolean
): Promise<TreeNode> {
node = await this.nodeLocalUri(node);
node = await super.decorateNode(node, showAllFiles);
return node;
}
protected async isSketchNode(node: DirNode): Promise<boolean> {
if (DirNode.is(node)) {
const sketch = this.sketchCache.getSketch(
node.fileStat.resource.path.toString()
);
return !!sketch;
}
return false;
}
private mergeDecoration(
node: TreeNode,
decorationData: WidgetDecoration.Data
@@ -378,14 +503,16 @@ export class CloudSketchbookTree extends SketchbookTree {
});
}
private setDecoration(
private removeDecoration(
node: TreeNode,
decorationData: WidgetDecoration.Data | undefined
decorationData: WidgetDecoration.Data
): void {
if (!decorationData) {
delete (node as any).decorationData;
} else {
Object.assign(node, { decorationData });
if (DecoratedTreeNode.is(node)) {
for (const property of Object.keys(decorationData)) {
if (node.decorationData.hasOwnProperty(property)) {
delete (node.decorationData as any)[property];
}
}
}
}
@@ -397,74 +524,31 @@ export class CloudSketchbookTree extends SketchbookTree {
}
return undefined;
}
private get showAllFiles(): boolean {
return this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
}
}
export interface CreateFileStat extends FileStat {
type: Create.ResourceType;
sketchId?: string;
isPublic?: boolean;
}
export namespace CreateFileStat {
export function is(
stat: FileStat & { type?: Create.ResourceType }
): stat is CreateFileStat {
return !!stat.type;
}
}
export namespace CloudSketchbookTree {
export const rootResource: Create.Resource = Object.freeze({
modified_at: '',
name: '',
path: posix.sep,
type: 'folder',
children: Number.MIN_SAFE_INTEGER,
size: Number.MIN_SAFE_INTEGER,
sketchId: '',
});
export interface CloudRootNode extends SketchbookTree.RootNode {
readonly cache: CreateCache;
export interface CloudSketchTreeNode extends FileStatNode {
remoteUri: URI;
}
export namespace CloudRootNode {
export function create(
cache: CreateCache,
showAllFiles: boolean
): CloudRootNode {
return Object.assign(
SketchbookTree.RootNode.create(
toFileStat(rootResource, cache, 1),
showAllFiles
),
{ cache }
);
export namespace CloudSketchTreeNode {
export function is(node: TreeNode): node is CloudSketchTreeNode {
return !!node && typeof node.hasOwnProperty('remoteUri') !== 'undefined';
}
export function is(
node: (TreeNode & Partial<CloudRootNode>) | undefined
): node is CloudRootNode {
return !!node && !!node.cache && SketchbookTree.RootNode.is(node);
export function isSynced(node: CloudSketchTreeNode): boolean {
return node.remoteUri !== node.uri;
}
}
export interface CloudSketchDirNode extends SketchbookTree.SketchDirNode {
export interface CloudSketchDirNode
extends Omit<SketchbookTree.SketchDirNode, 'fileStat'>,
CloudSketchTreeNode {
state?: CloudSketchDirNode.State;
synced?: true;
sketchId?: string;
isPublic?: boolean;
sketchId?: string;
commands?: Command[];
underlying?: URI;
}
export interface CloudSketchTreeNode extends TreeNode {
underlying?: URI;
}
export namespace CloudSketchDirNode {
export function is(node: TreeNode): node is CloudSketchDirNode {
return SketchbookTree.SketchDirNode.is(node);
@@ -472,28 +556,4 @@ export namespace CloudSketchbookTree {
export type State = 'syncing' | 'pulling' | 'pushing';
}
export function toFileStat(
resource: Create.Resource,
cache: CreateCache,
depth = 0
): CreateFileStat {
return {
isDirectory: resource.type !== 'file',
isFile: resource.type === 'file',
isPublic: resource.isPublic,
isSymbolicLink: false,
name: resource.name,
resource: CreateUri.toUri(resource),
size: resource.size,
mtime: Date.parse(resource.modified_at),
sketchId: resource.sketchId || undefined,
type: resource.type,
...(!!depth && {
children: CreateCache.childrenOf(resource, cache)?.map(
(childResource) => toFileStat(childResource, cache, depth - 1)
),
}),
};
}
}

View File

@@ -16,6 +16,15 @@ export class CloudSketchbookWidget extends SketchbookWidget {
super.init();
}
getTreeWidget(): any {
const widget: any = this.sketchbookTreesContainer.selectedWidgets().next();
if (widget && typeof widget.getTreeWidget !== 'undefined') {
return (widget as CloudSketchbookCompositeWidget).getTreeWidget();
}
return widget;
}
checkCloudEnabled() {
if (this.arduinoPreferences['arduino.cloud.enabled']) {
this.sketchbookTreesContainer.activateWidget(this.widget);

View File

@@ -34,7 +34,7 @@ export class SketchbookTreeModel extends FileTreeModel {
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
public readonly commandRegistry: CommandRegistry;
@inject(ConfigService)
protected readonly configService: ConfigService;
@@ -162,34 +162,24 @@ export class SketchbookTreeModel extends FileTreeModel {
protected async createRoot(): Promise<TreeNode | undefined> {
const config = await this.configService.getConfiguration();
const stat = await this.fileService.resolve(new URI(config.sketchDirUri));
const rootFileStats = 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)
);
if (this.workspaceService.opened && rootFileStats.children) {
// filter out libraries and hardware
return workspaceNode;
if (this.workspaceService.opened) {
const workspaceNode = WorkspaceNode.createRoot();
workspaceNode.children.push(
await this.tree.createWorkspaceRoot(rootFileStats, 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.
*/

View File

@@ -1,22 +1,17 @@
import { inject, injectable } from 'inversify';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { Command } from '@theia/core/lib/common/command';
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,
WorkspaceRootNode,
} from '@theia/navigator/lib/browser/navigator-tree';
import { ArduinoPreferences } from '../../arduino-preferences';
@injectable()
export class SketchbookTree extends FileNavigatorTree {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
@@ -27,61 +22,71 @@ export class SketchbookTree extends FileNavigatorTree {
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
const children = (
await Promise.all(
(
await super.resolveChildren(parent)
).map((node) => this.maybeDecorateNode(node, showAllFiles))
)
).filter((node) => {
// filter out hidden nodes
if (DirNode.is(node) || FileStatNode.is(node)) {
return node.fileStat.name.indexOf('.') !== 0;
const children = (await super.resolveChildren(parent)).filter((child) => {
// strip libraries and hardware directories
if (
DirNode.is(child) &&
['libraries', 'hardware'].includes(child.fileStat.name) &&
WorkspaceRootNode.is(child.parent)
) {
return false;
}
// strip files if only directories are admitted
if (!DirNode.is(child) && !showAllFiles) {
return false;
}
// strip hidden files
if (FileStatNode.is(child) && child.fileStat.name.indexOf('.') === 0) {
return false;
}
return true;
});
// filter out hardware and libraries
if (WorkspaceNode.is(parent.parent)) {
return children
.filter(DirNode.is)
.filter(
(node) =>
['libraries', 'hardware'].indexOf(
this.labelProvider.getName(node)
) === -1
);
if (children.length === 0) {
delete (parent as any).expanded;
}
// return the Arduino directory containing all user sketches
if (WorkspaceNode.is(parent)) {
return children;
}
return children;
// return this.filter.filter(super.resolveChildren(parent));
return await Promise.all(
children.map(
async (childNode) => await this.decorateNode(childNode, showAllFiles)
)
);
}
protected async maybeDecorateNode(
protected async isSketchNode(node: DirNode): Promise<boolean> {
const sketch = await this.sketchesService.maybeLoadSketch(
node.uri.toString()
);
return !!sketch;
}
/**
* Add commands available for the given node
* @param node
* @returns
*/
protected async augmentSketchNode(node: DirNode): Promise<void> {
Object.assign(node, {
type: 'sketch',
commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU],
});
}
protected async decorateNode(
node: TreeNode,
showAllFiles: boolean
): Promise<TreeNode> {
if (DirNode.is(node)) {
const sketch = await this.sketchesService.maybeLoadSketch(
node.uri.toString()
);
if (sketch) {
Object.assign(node, {
type: 'sketch',
commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU],
});
if (!showAllFiles) {
delete (node as any).expanded;
node.children = [];
} else {
node.expanded = false;
}
return node;
if (DirNode.is(node) && (await this.isSketchNode(node))) {
await this.augmentSketchNode(node);
if (!showAllFiles) {
delete (node as any).expanded;
(node as any).children = [];
} else {
(node as any).expanded = false;
}
}
return node;
@@ -89,25 +94,6 @@ export class SketchbookTree extends FileNavigatorTree {
}
export namespace SketchbookTree {
export interface RootNode extends DirNode {
readonly showAllFiles: boolean;
}
export namespace RootNode {
export function is(node: TreeNode & Partial<RootNode>): node is RootNode {
return typeof node.showAllFiles === 'boolean';
}
export function create(
fileStat: FileStat,
showAllFiles: boolean
): RootNode {
return Object.assign(DirNode.createRoot(fileStat), {
showAllFiles,
visible: false,
});
}
}
export interface SketchDirNode extends DirNode {
readonly type: 'sketch';
readonly commands?: Command[];

View File

@@ -100,10 +100,7 @@ export class SketchbookWidgetContribution
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
execute: async (arg) => {
const underlying = await this.fileService.toUnderlyingResource(
arg.node.uri
);
return this.workspaceService.open(underlying);
return this.workspaceService.open(arg.node.uri);
},
isEnabled: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
@@ -214,7 +211,8 @@ export class SketchbookWidgetContribution
if (Navigatable.is(widget)) {
const resourceUri = widget.getResourceUri();
if (resourceUri) {
const { model } = (await this.widget).getTreeWidget();
const treeWidget = (await this.widget).getTreeWidget();
const { model } = treeWidget;
const node = await model.revealFile(resourceUri);
if (SelectableTreeNode.is(node)) {
model.selectNode(node);