mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-05 03:36:35 +00:00
Improve remote sketchbook explorer (#459)
* Refactor remote sketchbook explorer * sketches sorting
This commit is contained in:
parent
4da5d573e4
commit
d790266cc8
@ -236,6 +236,7 @@ import { CloudSketchbookCompositeWidget } from './widgets/cloud-sketchbook/cloud
|
||||
import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
|
||||
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
|
||||
import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
@ -686,6 +687,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
createCloudSketchbookTreeWidget(container)
|
||||
);
|
||||
bind(CreateApi).toSelf().inSingletonScope();
|
||||
bind(SketchCache).toSelf().inSingletonScope();
|
||||
|
||||
bind(ShareSketchDialog).toSelf().inSingletonScope();
|
||||
bind(AuthenticationClientService).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(AuthenticationClientService);
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import * as createPaths from './create-paths';
|
||||
import { posix, splitSketchPath } from './create-paths';
|
||||
import { posix } from './create-paths';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import { Create, CreateError } from './typings';
|
||||
|
||||
export interface ResponseResultProvider {
|
||||
(response: Response): Promise<any>;
|
||||
@ -15,10 +17,11 @@ export namespace ResponseResultProvider {
|
||||
|
||||
type ResourceType = 'f' | 'd';
|
||||
|
||||
export let sketchCache: Create.Sketch[] = [];
|
||||
|
||||
@injectable()
|
||||
export class CreateApi {
|
||||
@inject(SketchCache)
|
||||
protected sketchCache: SketchCache;
|
||||
|
||||
protected authenticationService: AuthenticationClientService;
|
||||
protected arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@ -32,48 +35,20 @@ export class CreateApi {
|
||||
return this;
|
||||
}
|
||||
|
||||
public sketchCompareByPath = (param: string) => {
|
||||
return (sketch: Create.Sketch) => {
|
||||
const [, spath] = splitSketchPath(sketch.path);
|
||||
return param === spath;
|
||||
};
|
||||
};
|
||||
|
||||
async findSketchInCache(
|
||||
compareFn: (sketch: Create.Sketch) => boolean,
|
||||
trustCache = true
|
||||
): Promise<Create.Sketch | undefined> {
|
||||
const sketch = sketchCache.find((sketch) => compareFn(sketch));
|
||||
if (trustCache) {
|
||||
return Promise.resolve(sketch);
|
||||
}
|
||||
return await this.sketch({ id: sketch?.id });
|
||||
}
|
||||
|
||||
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
|
||||
return {
|
||||
href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`,
|
||||
modified_at: sketch.modified_at,
|
||||
created_at: sketch.created_at,
|
||||
name: `${Create.arduino_secrets_file}`,
|
||||
path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`,
|
||||
mimetype: 'text/x-c++src; charset=utf-8',
|
||||
type: 'file',
|
||||
sketchId: sketch.id,
|
||||
};
|
||||
}
|
||||
|
||||
async sketch(opt: {
|
||||
id?: string;
|
||||
path?: string;
|
||||
}): Promise<Create.Sketch | undefined> {
|
||||
let url;
|
||||
if (opt.id) {
|
||||
url = new URL(`${this.domain()}/sketches/byID/${opt.id}`);
|
||||
} else if (opt.path) {
|
||||
url = new URL(`${this.domain()}/sketches/byPath${opt.path}`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
async sketch(id: string): Promise<Create.Sketch> {
|
||||
const url = new URL(`${this.domain()}/sketches/byID/${id}`);
|
||||
|
||||
url.searchParams.set('user_id', 'me');
|
||||
const headers = await this.headers();
|
||||
@ -92,7 +67,7 @@ export class CreateApi {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
sketchCache = result.sketches;
|
||||
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
|
||||
return result.sketches;
|
||||
}
|
||||
|
||||
@ -118,7 +93,7 @@ export class CreateApi {
|
||||
|
||||
async readDirectory(
|
||||
posixPath: string,
|
||||
options: { recursive?: boolean; match?: string; secrets?: boolean } = {}
|
||||
options: { recursive?: boolean; match?: string } = {}
|
||||
): Promise<Create.Resource[]> {
|
||||
const url = new URL(
|
||||
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
|
||||
@ -131,58 +106,21 @@ export class CreateApi {
|
||||
}
|
||||
const headers = await this.headers();
|
||||
|
||||
const sketchProm = options.secrets
|
||||
? this.sketches()
|
||||
: Promise.resolve(sketchCache);
|
||||
return this.run<Create.RawResource[]>(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
})
|
||||
.then(async (result) => {
|
||||
// add arduino_secrets.h to the results, when reading a sketch main folder
|
||||
if (posixPath.length && posixPath !== posix.sep) {
|
||||
const sketch = this.sketchCache.getSketch(posixPath);
|
||||
|
||||
return Promise.all([
|
||||
this.run<Create.RawResource[]>(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
}),
|
||||
sketchProm,
|
||||
])
|
||||
.then(async ([result, sketches]) => {
|
||||
if (options.secrets) {
|
||||
// for every sketch with secrets, create a fake arduino_secrets.h
|
||||
result.forEach(async (res) => {
|
||||
if (res.type !== 'sketch') {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, spath] = createPaths.splitSketchPath(res.path);
|
||||
const sketch = await this.findSketchInCache(
|
||||
this.sketchCompareByPath(spath)
|
||||
);
|
||||
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
|
||||
result.push(this.getSketchSecretStat(sketch));
|
||||
}
|
||||
});
|
||||
|
||||
if (posixPath !== posix.sep) {
|
||||
const sketch = await this.findSketchInCache(
|
||||
this.sketchCompareByPath(posixPath)
|
||||
);
|
||||
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
|
||||
result.push(this.getSketchSecretStat(sketch));
|
||||
}
|
||||
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
|
||||
result.push(this.getSketchSecretStat(sketch));
|
||||
}
|
||||
}
|
||||
const sketchesMap: Record<string, Create.Sketch> = sketches.reduce(
|
||||
(prev, curr) => {
|
||||
return { ...prev, [curr.path]: curr };
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// add the sketch id and isPublic to the resource
|
||||
return result.map((resource) => {
|
||||
return {
|
||||
...resource,
|
||||
sketchId: sketchesMap[resource.path]?.id || '',
|
||||
isPublic: sketchesMap[resource.path]?.is_public || false,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason?.status === 404) return [] as Create.Resource[];
|
||||
@ -214,18 +152,16 @@ export class CreateApi {
|
||||
|
||||
let resources;
|
||||
if (basename === Create.arduino_secrets_file) {
|
||||
const sketch = await this.findSketchInCache(
|
||||
this.sketchCompareByPath(parentPosixPath)
|
||||
);
|
||||
const sketch = this.sketchCache.getSketch(parentPosixPath);
|
||||
resources = sketch ? [this.getSketchSecretStat(sketch)] : [];
|
||||
} else {
|
||||
resources = await this.readDirectory(parentPosixPath, {
|
||||
match: basename,
|
||||
});
|
||||
}
|
||||
|
||||
resources.sort((left, right) => left.path.length - right.path.length);
|
||||
const resource = resources.find(({ name }) => name === basename);
|
||||
const resource = resources.find(
|
||||
({ path }) => createPaths.splitSketchPath(path)[1] === posixPath
|
||||
);
|
||||
if (!resource) {
|
||||
throw new CreateError(`Not found: ${posixPath}.`, 404);
|
||||
}
|
||||
@ -248,10 +184,7 @@ export class CreateApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
const sketch = await this.findSketchInCache((sketch) => {
|
||||
const [, spath] = splitSketchPath(sketch.path);
|
||||
return spath === createPaths.parentPosix(path);
|
||||
}, true);
|
||||
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
|
||||
|
||||
if (
|
||||
sketch &&
|
||||
@ -273,14 +206,25 @@ export class CreateApi {
|
||||
|
||||
if (basename === Create.arduino_secrets_file) {
|
||||
const parentPosixPath = createPaths.parentPosix(posixPath);
|
||||
const sketch = await this.findSketchInCache(
|
||||
this.sketchCompareByPath(parentPosixPath),
|
||||
false
|
||||
);
|
||||
|
||||
//retrieve the sketch id from the cache
|
||||
const cacheSketch = this.sketchCache.getSketch(parentPosixPath);
|
||||
if (!cacheSketch) {
|
||||
throw new Error(`Unable to find sketch ${parentPosixPath} in cache`);
|
||||
}
|
||||
|
||||
// get a fresh copy of the sketch in order to guarantee fresh secrets
|
||||
const sketch = await this.sketch(cacheSketch.id);
|
||||
if (!sketch) {
|
||||
throw new Error(
|
||||
`Unable to get a fresh copy of the sketch ${cacheSketch.id}`
|
||||
);
|
||||
}
|
||||
this.sketchCache.addSketch(sketch);
|
||||
|
||||
let file = '';
|
||||
if (sketch && sketch.secrets) {
|
||||
for (const item of sketch?.secrets) {
|
||||
for (const item of sketch.secrets) {
|
||||
file += `#define ${item.name} "${item.value}"\r\n`;
|
||||
}
|
||||
}
|
||||
@ -310,9 +254,9 @@ export class CreateApi {
|
||||
|
||||
if (basename === Create.arduino_secrets_file) {
|
||||
const parentPosixPath = createPaths.parentPosix(posixPath);
|
||||
const sketch = await this.findSketchInCache(
|
||||
this.sketchCompareByPath(parentPosixPath)
|
||||
);
|
||||
|
||||
const sketch = this.sketchCache.getSketch(parentPosixPath);
|
||||
|
||||
if (sketch) {
|
||||
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
|
||||
const headers = await this.headers();
|
||||
@ -356,9 +300,10 @@ export class CreateApi {
|
||||
secrets: { data: secrets },
|
||||
};
|
||||
|
||||
// replace the sketch in the cache, so other calls will not overwrite each other
|
||||
sketchCache = sketchCache.filter((skt) => skt.id !== sketch.id);
|
||||
sketchCache.push({ ...sketch, secrets });
|
||||
// replace the sketch in the cache with the one we are pushing
|
||||
// TODO: we should do a get after the POST, in order to be sure the cache
|
||||
// is updated the most recent metadata
|
||||
this.sketchCache.addSketch(sketch);
|
||||
|
||||
const init = {
|
||||
method: 'POST',
|
||||
@ -370,6 +315,14 @@ export class CreateApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// do not upload "do_not_sync" files/directoris and their descendants
|
||||
const segments = posixPath.split(posix.sep) || [];
|
||||
if (
|
||||
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
|
||||
);
|
||||
@ -512,75 +465,3 @@ void loop() {
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
export namespace Create {
|
||||
export interface Sketch {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly modified_at: string;
|
||||
readonly created_at: string;
|
||||
|
||||
readonly secrets?: { name: string; value: string }[];
|
||||
|
||||
readonly id: string;
|
||||
readonly is_public: boolean;
|
||||
// readonly board_fqbn: '',
|
||||
// readonly board_name: '',
|
||||
// readonly board_type: 'serial' | 'network' | 'cloud' | '',
|
||||
readonly href?: string;
|
||||
readonly libraries: string[];
|
||||
// readonly tutorials: string[] | null;
|
||||
// readonly types: string[] | null;
|
||||
// readonly user_id: string;
|
||||
}
|
||||
|
||||
export type ResourceType = 'sketch' | 'folder' | 'file';
|
||||
export const arduino_secrets_file = 'arduino_secrets.h';
|
||||
export interface Resource {
|
||||
readonly name: string;
|
||||
/**
|
||||
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
|
||||
*/
|
||||
readonly path: string;
|
||||
readonly type: ResourceType;
|
||||
readonly sketchId: string;
|
||||
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
|
||||
readonly children?: number; // For 'sketch' and 'folder' types.
|
||||
readonly size?: number; // For 'sketch' type only.
|
||||
readonly isPublic?: boolean; // For 'sketch' type only.
|
||||
|
||||
readonly mimetype?: string; // For 'file' type.
|
||||
readonly href?: string;
|
||||
}
|
||||
export namespace Resource {
|
||||
export function is(arg: any): arg is Resource {
|
||||
return (
|
||||
!!arg &&
|
||||
'name' in arg &&
|
||||
typeof arg['name'] === 'string' &&
|
||||
'path' in arg &&
|
||||
typeof arg['path'] === 'string' &&
|
||||
'type' in arg &&
|
||||
typeof arg['type'] === 'string' &&
|
||||
'modified_at' in arg &&
|
||||
typeof arg['modified_at'] === 'string' &&
|
||||
(arg['type'] === 'sketch' ||
|
||||
arg['type'] === 'folder' ||
|
||||
arg['type'] === 'file')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
|
||||
}
|
||||
|
||||
export class CreateError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
readonly details?: string
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, CreateError.prototype);
|
||||
}
|
||||
}
|
||||
|
@ -24,10 +24,11 @@ import {
|
||||
FileServiceContribution,
|
||||
} from '@theia/filesystem/lib/browser/file-service';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { Create, CreateApi } from './create-api';
|
||||
import { CreateApi } from './create-api';
|
||||
import { CreateUri } from './create-uri';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { Create } from './typings';
|
||||
|
||||
export const REMOTE_ONLY_FILES = ['sketch.json'];
|
||||
|
||||
@ -106,10 +107,7 @@ export class CreateFsProvider
|
||||
|
||||
async readdir(uri: URI): Promise<[string, FileType][]> {
|
||||
const resources = await this.getCreateApi.readDirectory(
|
||||
uri.path.toString(),
|
||||
{
|
||||
secrets: true,
|
||||
}
|
||||
uri.path.toString()
|
||||
);
|
||||
return resources
|
||||
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name))
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { URI as Uri } from 'vscode-uri';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Create } from './create-api';
|
||||
import { toPosixPath, parentPosix, posix } from './create-paths';
|
||||
import { Create } from './typings';
|
||||
|
||||
export namespace CreateUri {
|
||||
export const scheme = 'arduino-create';
|
||||
|
73
arduino-ide-extension/src/browser/create/typings.ts
Normal file
73
arduino-ide-extension/src/browser/create/typings.ts
Normal file
@ -0,0 +1,73 @@
|
||||
export namespace Create {
|
||||
export interface Sketch {
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly modified_at: string;
|
||||
readonly created_at: string;
|
||||
|
||||
readonly secrets?: { name: string; value: string }[];
|
||||
|
||||
readonly id: string;
|
||||
readonly is_public: boolean;
|
||||
readonly board_fqbn: '';
|
||||
readonly board_name: '';
|
||||
readonly board_type: 'serial' | 'network' | 'cloud' | '';
|
||||
readonly href?: string;
|
||||
readonly libraries: string[];
|
||||
readonly tutorials: string[] | null;
|
||||
readonly types: string[] | null;
|
||||
readonly user_id: string;
|
||||
}
|
||||
|
||||
export type ResourceType = 'sketch' | 'folder' | 'file';
|
||||
export const arduino_secrets_file = 'arduino_secrets.h';
|
||||
export const do_not_sync_files = ['.theia'];
|
||||
export interface Resource {
|
||||
readonly name: string;
|
||||
/**
|
||||
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
|
||||
*/
|
||||
readonly path: string;
|
||||
readonly type: ResourceType;
|
||||
readonly sketchId?: string;
|
||||
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
|
||||
readonly created_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
|
||||
readonly children?: number; // For 'sketch' and 'folder' types.
|
||||
readonly size?: number; // For 'sketch' type only.
|
||||
readonly isPublic?: boolean; // For 'sketch' type only.
|
||||
|
||||
readonly mimetype?: string; // For 'file' type.
|
||||
readonly href?: string;
|
||||
}
|
||||
export namespace Resource {
|
||||
export function is(arg: any): arg is Resource {
|
||||
return (
|
||||
!!arg &&
|
||||
'name' in arg &&
|
||||
typeof arg['name'] === 'string' &&
|
||||
'path' in arg &&
|
||||
typeof arg['path'] === 'string' &&
|
||||
'type' in arg &&
|
||||
typeof arg['type'] === 'string' &&
|
||||
'modified_at' in arg &&
|
||||
typeof arg['modified_at'] === 'string' &&
|
||||
(arg['type'] === 'sketch' ||
|
||||
arg['type'] === 'folder' ||
|
||||
arg['type'] === 'file')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
|
||||
}
|
||||
|
||||
export class CreateError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
readonly details?: string
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, CreateError.prototype);
|
||||
}
|
||||
}
|
@ -103,7 +103,7 @@ export class LocalCacheFsProvider
|
||||
});
|
||||
}
|
||||
|
||||
private get currentUserUri(): URI {
|
||||
public get currentUserUri(): URI {
|
||||
const { session } = this.authenticationService;
|
||||
if (!session) {
|
||||
throw new FileSystemProviderError(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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[];
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user