Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: nested cloud sketch folder sync #2388

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions arduino-ide-extension/src/browser/create/create-fs-provider.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Event } from '@theia/core/lib/common/event';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { Event } from '@theia/core/lib/common/event';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
FileService,
FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service';
import {
Stat,
FileType,
FileChange,
FileWriteOptions,
FileDeleteOptions,
FileOverwriteOptions,
FileSystemProvider,
FileSystemProviderCapabilities,
FileSystemProviderError,
FileSystemProviderErrorCode,
FileSystemProviderCapabilities,
FileType,
FileWriteOptions,
Stat,
WatchOptions,
createFileSystemProviderError,
} from '@theia/filesystem/lib/common/files';
import {
FileService,
FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service';
import { SketchesService } from '../../common/protocol';
import { stringToUint8Array } from '../../common/utils';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { CreateApi } from './create-api';
import { CreateUri } from './create-uri';
import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
import { Create } from './typings';
import { stringToUint8Array } from '../../common/utils';
import { Create, isNotFound } from './typings';

@injectable()
export class CreateFsProvider
@@ -90,14 +91,27 @@ export class CreateFsProvider
size: 0,
};
}
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
try {
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
} catch (err) {
let errToRethrow = err;
// Not Found (Create API) errors must be remapped to VS Code filesystem provider specific errors
// https://code.visualstudio.com/api/references/vscode-api#FileSystemError
if (isNotFound(errToRethrow)) {
errToRethrow = createFileSystemProviderError(
errToRethrow,
FileSystemProviderErrorCode.FileNotFound
);
}
throw errToRethrow;
}
}

async mkdir(uri: URI): Promise<void> {
Original file line number Diff line number Diff line change
@@ -58,6 +58,13 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
execute: (uri) => this.newFile(uri),
})
);
registry.unregisterCommand(WorkspaceCommands.NEW_FOLDER);
registry.registerCommand(
WorkspaceCommands.NEW_FOLDER,
this.newWorkspaceRootUriAwareCommandHandler({
execute: (uri) => this.newFolder(uri),
})
);
registry.unregisterCommand(WorkspaceCommands.FILE_RENAME);
registry.registerCommand(
WorkspaceCommands.FILE_RENAME,
@@ -72,6 +79,37 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
);
}

private async newFolder(uri: URI | undefined): Promise<void> {
if (!uri) {
return;
}

const parent = await this.getDirectory(uri);
if (!parent) {
return;
}

const dialog = new WorkspaceInputDialog(
{
title: nls.localizeByDefault('New Folder...'),
parentUri: uri,
placeholder: nls.localize(
'theia/workspace/newFolderPlaceholder',
'Folder Name'
),
validate: (name) => this.validateFileName(name, parent, true),
},
this.labelProvider
);
const name = await this.openDialog(dialog, uri);
if (!name) {
return;
}
const folderUri = uri.resolve(name);
await this.fileService.createFolder(folderUri);
this.fireCreateNewFile({ parent: uri, uri: folderUri });
}

private async newFile(uri: URI | undefined): Promise<void> {
if (!uri) {
return;
Original file line number Diff line number Diff line change
@@ -389,21 +389,28 @@ export class CloudSketchbookTree extends SketchbookTree {

private async sync(source: URI, dest: URI): Promise<void> {
const { filesToWrite, filesToDelete } = await this.treeDiff(source, dest);
await Promise.all(
filesToWrite.map(async ({ source, dest }) => {
if ((await this.fileService.resolve(source)).isFile) {
const content = await this.fileService.read(source);
return this.fileService.write(dest, content.value);
}
return this.fileService.createFolder(dest);
})
// Sort by the URIs. The shortest comes first. It's to ensure creating the parent folder for nested resources, for example.
// When sorting the URIs, it does not matter whether on source or dest, only the URI path and its length matters; they're the same for a source+dest pair
const uriPathLengthComparator = (left: URI, right: URI) =>
left.path.toString().length - right.path.toString().length;
filesToWrite.sort((left, right) =>
uriPathLengthComparator(left.source, right.source)
);
for (const { source, dest } of filesToWrite) {
const stat = await this.fileService.resolve(source);
if (stat.isFile) {
const content = await this.fileService.read(source);
await this.fileService.write(dest, content.value);
} else {
await this.fileService.createFolder(dest);
}
}

await Promise.all(
filesToDelete.map((file) =>
this.fileService.delete(file, { recursive: true })
)
);
// Longes URI paths come first to delete the most nested ones first.
filesToDelete.sort(uriPathLengthComparator).reverse();
for (const resource of filesToDelete) {
await this.fileService.delete(resource, { recursive: true });
}
}

override async resolveChildren(
Original file line number Diff line number Diff line change
@@ -25,6 +25,14 @@ export namespace SketchbookCommands {
'arduino/sketch/openFolder'
);

export const NEW_FOLDER = Command.toLocalizedCommand(
{
id: 'arduino-sketchbook--new-folder',
label: 'New Folder',
},
'arduino/sketch/newFolder'
);

export const OPEN_SKETCHBOOK_CONTEXT_MENU: Command = {
id: 'arduino-sketchbook--open-sketch-context-menu',
iconClass: 'sketchbook-tree__opts',
Original file line number Diff line number Diff line change
@@ -28,7 +28,10 @@ import {
} from '../../sketches-service-client-impl';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { URI } from '../../contributions/contribution';
import { WorkspaceInput } from '@theia/workspace/lib/browser';
import {
WorkspaceCommands,
WorkspaceInput,
} from '@theia/workspace/lib/browser';

export const SKETCHBOOK__CONTEXT = ['arduino-sketchbook--context'];

@@ -130,6 +133,21 @@ export class SketchbookWidgetContribution
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
});

registry.registerCommand(SketchbookCommands.NEW_FOLDER, {
execute: async (arg) => {
if (arg.node.uri) {
return registry.executeCommand(
WorkspaceCommands.NEW_FOLDER.id,
arg.node.uri
);
}
},
isEnabled: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
});

registry.registerCommand(SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU, {
isEnabled: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
@@ -206,6 +224,12 @@ export class SketchbookWidgetContribution
label: SketchbookCommands.REVEAL_IN_FINDER.label,
order: '0',
});

registry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
commandId: SketchbookCommands.NEW_FOLDER.id,
label: SketchbookCommands.NEW_FOLDER.label,
order: '1',
});
}

private openNewWindow(
4 changes: 3 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
@@ -455,6 +455,7 @@
"moving": "Moving",
"movingMsg": "The file \"{0}\" needs to be inside a sketch folder named \"{1}\".\nCreate this folder, move the file, and continue?",
"new": "New Sketch",
"newFolder": "New Folder",
"noTrailingPeriod": "A filename cannot end with a dot",
"openFolder": "Open Folder",
"openRecent": "Open Recent",
@@ -545,7 +546,8 @@
"deleteCurrentSketch": "The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
"fileNewName": "Name for new file",
"invalidExtension": ".{0} is not a valid extension",
"newFileName": "New name for file"
"newFileName": "New name for file",
"newFolderPlaceholder": "Folder Name"
}
}
}