mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-01 22:48:32 +00:00
feat: rename, deletion, and validation support
Closes #1599 Closes #1825 Closes #649 Closes #1847 Closes #1882 Co-authored-by: Akos Kitta <a.kitta@arduino.cc> Co-authored-by: per1234 <accounts@perglass.com> Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -1,28 +1,34 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
import {
|
||||
RenameCloudSketch,
|
||||
RenameCloudSketchParams,
|
||||
} from './rename-cloud-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
@@ -35,7 +41,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
|
||||
label: nls.localizeByDefault('Save As...'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
@@ -63,11 +69,63 @@ export class SaveAsSketch extends SketchContribution {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
let destinationUri: string | undefined;
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (cloudUri) {
|
||||
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
|
||||
} else {
|
||||
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
|
||||
}
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (!newWorkspaceUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [{ toDelete: sketch.uri }],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(newWorkspaceUri), options);
|
||||
}
|
||||
return !!newWorkspaceUri;
|
||||
}
|
||||
|
||||
private async createCloudCopy(
|
||||
params: RenameCloudSketchParams
|
||||
): Promise<string | undefined> {
|
||||
return this.commandService.executeCommand<string>(
|
||||
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
private async createLocalCopy(
|
||||
sketch: Sketch,
|
||||
execOnlyIfTemp?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = await this.defaultUri();
|
||||
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
|
||||
@@ -84,91 +142,157 @@ export class SaveAsSketch extends SketchContribution {
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
|
||||
const defaultUri = containerDirUri.resolve(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
Sketch.toValidSketchFolderName(sketch.name, exists)
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath || canceled) {
|
||||
return false;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
const workspaceUri = await this.sketchService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (workspaceUri) {
|
||||
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchService.markAsRecentlyOpened(workspaceUri);
|
||||
}
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (workspaceUri && openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [sketch.uri],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), options);
|
||||
}
|
||||
return !!workspaceUri;
|
||||
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
|
||||
}
|
||||
|
||||
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
|
||||
/**
|
||||
* Prompts for the new sketch folder name until a valid one is give,
|
||||
* then resolves with the destination sketch folder URI string,
|
||||
* or `undefined` if the operation was canceled.
|
||||
*/
|
||||
private async promptLocalSketchFolderDestination(
|
||||
sketch: Sketch,
|
||||
defaultPath: string
|
||||
): Promise<string | undefined> {
|
||||
let sketchFolderDestinationUri: string | undefined;
|
||||
while (!sketchFolderDestinationUri) {
|
||||
const { filePath } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
// The new location of the sketch cannot be inside the location of current sketch.
|
||||
// https://github.com/arduino/arduino-ide/issues/1882
|
||||
let dialogContent: InvalidSketchFolderDialogContent | undefined;
|
||||
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationMessage',
|
||||
"Invalid sketch folder location: '{0}'",
|
||||
filePath
|
||||
),
|
||||
details: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationDetails',
|
||||
'You cannot save a sketch into a folder inside itself.'
|
||||
),
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
|
||||
'Do you want to try saving the sketch to a different location?'
|
||||
),
|
||||
};
|
||||
}
|
||||
if (!dialogContent) {
|
||||
const sketchFolderName = new URI(destinationUri).path.base;
|
||||
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
|
||||
if (errorMessage) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderNameMessage',
|
||||
"Invalid sketch folder name: '{0}'",
|
||||
sketchFolderName
|
||||
),
|
||||
details: errorMessage,
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderQuestion',
|
||||
'Do you want to try saving the sketch with a different name?'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (dialogContent) {
|
||||
const message = `
|
||||
${dialogContent.message}
|
||||
|
||||
${dialogContent.details}
|
||||
|
||||
${dialogContent.question}`.trim();
|
||||
defaultPath = filePath;
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message,
|
||||
buttons: [Dialog.CANCEL, Dialog.YES],
|
||||
}
|
||||
);
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
sketchFolderDestinationUri = destinationUri;
|
||||
}
|
||||
}
|
||||
return sketchFolderDestinationUri;
|
||||
}
|
||||
|
||||
private async saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string
|
||||
): Promise<void> {
|
||||
const widgets = this.applicationShell.widgets;
|
||||
const snapshots = new Map<string, object>();
|
||||
const snapshots = new Map<string, Saveable.Snapshot>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
const uriString = uri?.toString();
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
const uriString = uri.toString();
|
||||
let relativePath: string;
|
||||
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
|
||||
if (
|
||||
uriString.includes(sketch.uri) &&
|
||||
saveable &&
|
||||
saveable.createSnapshot
|
||||
) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
|
||||
if (sketch.mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketchUri.length);
|
||||
relativePath = uri.toString().substring(sketch.uri.length);
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
await Promise.all(
|
||||
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchFolderUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidSketchFolderDialogContent {
|
||||
readonly message: string;
|
||||
readonly details: string;
|
||||
readonly question: string;
|
||||
}
|
||||
|
||||
export namespace SaveAsSketch {
|
||||
export namespace Commands {
|
||||
export const SAVE_AS_SKETCH: Command = {
|
||||
|
||||
Reference in New Issue
Block a user