diff --git a/arduino-ide-extension/src/browser/create/create-fs-provider.ts b/arduino-ide-extension/src/browser/create/create-fs-provider.ts index 7908d0556..db1d7b0f3 100644 --- a/arduino-ide-extension/src/browser/create/create-fs-provider.ts +++ b/arduino-ide-extension/src/browser/create/create-fs-provider.ts @@ -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 { diff --git a/arduino-ide-extension/src/browser/theia/workspace/workspace-commands.ts b/arduino-ide-extension/src/browser/theia/workspace/workspace-commands.ts index ed960356e..f4a608bcd 100644 --- a/arduino-ide-extension/src/browser/theia/workspace/workspace-commands.ts +++ b/arduino-ide-extension/src/browser/theia/workspace/workspace-commands.ts @@ -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 { + 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 { if (!uri) { return; diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts index 9a5571da2..09b4ced7b 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts @@ -389,21 +389,28 @@ export class CloudSketchbookTree extends SketchbookTree { private async sync(source: URI, dest: URI): Promise { 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( diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts index c8935d663..625cf6fde 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts @@ -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', diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts index e73bb7e2f..0c368f7e7 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts @@ -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( diff --git a/i18n/en.json b/i18n/en.json index c83d280c1..800569d43 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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" } } }