import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree'; 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 { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model'; import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree'; import { CreateUri } from '../../create/create-uri'; import { FileChangesEvent, FileStat } from '@theia/filesystem/lib/common/files'; import { LocalCacheFsProvider, LocalCacheUri, } from '../../local-cache/local-cache-fs-provider'; import URI from '@theia/core/lib/common/uri'; import { Create } from '../../create/typings'; import { nls } from '@theia/core/lib/common/nls'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service'; 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; } function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] { const sketchesBaseDirs: Record = {}; for (const sketch of sketches) { const sketchBaseDirFileStat = sketchBaseDir(sketch); sketchesBaseDirs[sketchBaseDirFileStat.resource.toString()] = sketchBaseDirFileStat; } return Object.keys(sketchesBaseDirs).map( (dirUri) => sketchesBaseDirs[dirUri] ); } @injectable() export class CloudSketchbookTreeModel extends SketchbookTreeModel { @inject(CreateApi) private readonly createApi: CreateApi; @inject(AuthenticationClientService) private readonly authenticationService: AuthenticationClientService; @inject(LocalCacheFsProvider) private readonly localCacheFsProvider: LocalCacheFsProvider; @inject(ApplicationConnectionStatusContribution) private readonly connectionStatus: ApplicationConnectionStatusContribution; private _localCacheFsProviderReady: Deferred | undefined; @postConstruct() protected override init(): void { super.init(); this.toDispose.pushAll([ this.authenticationService.onSessionDidChange(() => this.updateRoot()), this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => { if (!offlineStatus) { this.updateRoot(); } }), ]); } override *getNodesByUri(uri: URI): IterableIterator { if (uri.scheme === LocalCacheUri.scheme) { const workspace = this.root; const { session } = this.authenticationService; if (session && WorkspaceNode.is(workspace)) { const currentUri = this.localCacheFsProvider.to(uri); if (currentUri) { const rootPath = this.localCacheFsProvider .toUri(session) .path.toString(); const currentPath = currentUri.path.toString(); if (rootPath === currentPath) { return workspace; } if (currentPath.startsWith(rootPath)) { const id = currentPath.substring(rootPath.length); const node = this.getNode(id); if (node) { yield node; } } } } } } protected override isRootAffected(changes: FileChangesEvent): boolean { return changes.changes .map(({ resource }) => resource) .some( (uri) => uri.parent.toString().startsWith(LocalCacheUri.root.toString()) // all files under the root might affect the tree ); } override async refresh( parent?: Readonly ): Promise { if (parent) { return super.refresh(parent); } await this.updateRoot(); return super.refresh(); } override async createRoot(): Promise { const { session } = this.authenticationService; if (!session) { this.tree.root = undefined; return; } this.createApi.sketchCache.init(); const [sketches] = await Promise.all([ this.createApi.sketches(), this.ensureLocalFsProviderReady(), ]); const rootFileStats = sketchesToFileStats(sketches); if (this.workspaceService.opened) { const workspaceNode = WorkspaceNode.createRoot( nls.localize('arduino/cloud/remote', 'Remote') ); for await (const stat of rootFileStats) { workspaceNode.children.push( await this.tree.createWorkspaceRoot(stat, workspaceNode) ); } return workspaceNode; } } sketchbookTree(): CloudSketchbookTree { return this.tree as CloudSketchbookTree; } protected override recursivelyFindSketchRoot( node: TreeNode ): TreeNode | false { if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) { return node; } if (node && node.parent) { return this.recursivelyFindSketchRoot(node.parent); } // can't find a root, return false return false; } override async revealFile(uri: URI): Promise { await this.localCacheFsProvider.ready.promise; // 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); } } private async ensureLocalFsProviderReady(): Promise { if (this._localCacheFsProviderReady) { return this._localCacheFsProviderReady.promise; } this._localCacheFsProviderReady = new Deferred(); this.fileService .access(LocalCacheUri.root) .then(() => this._localCacheFsProviderReady?.resolve()); return this._localCacheFsProviderReady.promise; } }