Skip to content

fix: enforce valid sketch folder name on Save as #1821

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { ConfigServiceClient } from './config/config-service-client';
import { ValidateSketch } from './contributions/validate-sketch';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
Expand Down Expand Up @@ -729,6 +730,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
Contribution.configure(bind, NewCloudSketch);
Contribution.configure(bind, ValidateSketch);

bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-ske
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { Command, CommandRegistry, Contribution, URI } from './contribution';
import {
Command,
CommandRegistry,
Contribution,
Sketch,
URI,
} from './contribution';

@injectable()
export class NewCloudSketch extends Contribution {
Expand Down Expand Up @@ -234,14 +240,7 @@ export class NewCloudSketch extends Contribution {
input
);
}
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
return '';
}
return nls.localize(
'arduino/newCloudSketch/invalidSketchName',
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
);
return Sketch.validateCloudSketchFolderName(input) ?? '';
},
},
this.labelProvider,
Expand Down
77 changes: 64 additions & 13 deletions arduino-ide-extension/src/browser/contributions/save-as-sketch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as remote from '@theia/core/electron-shared/@electron/remote';
import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Sketch,
SketchContribution,
URI,
Command,
Expand Down Expand Up @@ -90,20 +91,9 @@ export class SaveAsSketch extends SketchContribution {
: sketch.name
);
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,
}
const destinationUri = await this.promptSketchFolderDestination(
defaultPath
);
if (!filePath || canceled) {
return false;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return false;
}
Expand Down Expand Up @@ -133,6 +123,67 @@ export class SaveAsSketch extends SketchContribution {
return !!workspaceUri;
}

/**
* 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 promptSketchFolderDestination(
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);
const sketchFolderName = new URI(destinationUri).path.base;
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
if (errorMessage) {
const message = `
${nls.localize(
'arduino/sketch/invalidSketchFolderNameTitle',
"Invalid sketch folder name: '{0}'",
sketchFolderName
)}

${errorMessage}

${nls.localize(
'arduino/sketch/editInvalidSketchFolderName',
'Do you want to try to save the sketch folder with a different name?'
)}`.trim();
defaultPath = filePath;
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message,
buttons: [
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('vscode/extensionsUtils/yes', 'Yes'),
],
});
// cancel
if (response === 0) {
return undefined;
}
} else {
sketchFolderDestinationUri = destinationUri;
}
}
return sketchFolderDestinationUri;
}

private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, object>();
Expand Down
171 changes: 171 additions & 0 deletions arduino-ide-extension/src/browser/contributions/validate-sketch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { Dialog } from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { injectable } from '@theia/core/shared/inversify';
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { Sketch, SketchContribution, URI } from './contribution';
import { SaveAsSketch } from './save-as-sketch';

@injectable()
export class ValidateSketch extends SketchContribution {
override onReady(): void {
this.validate();
}

private async validate(): Promise<void> {
const result = await this.promptFixActions();
if (!result) {
const yes = await this.prompt(
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
nls.localize(
'arduino/validateSketch/abortFixMessage',
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
Dialog.NO
),
[Dialog.NO, Dialog.YES]
);
if (yes) {
return this.validate();
}
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), {
preserveWindow: true,
});
}
}

/**
* Returns with an array of actions the user has to perform to fix the invalid sketch.
*/
private validateSketch(sketch: Sketch): FixAction[] {
// sketch folder + main sketch file (requires `Save as...` and window reload)
const sketchFolderName = new URI(sketch.uri).path.base;
const sketchFolderNameError =
Sketch.validateSketchFolderName(sketchFolderName);
if (sketchFolderNameError) {
return [
{
execute: async () => {
const unknown =
(await this.promptRenameSketch(sketch)) &&
(await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
<SaveAsSketch.Options>{
markAsRecentlyOpened: true,
openAfterMove: true,
wipeOriginal: true,
}
));
return !!unknown;
},
},
];
}

// sketch code files (does not require window reload)
return Sketch.uris(sketch)
.filter((uri) => uri !== sketch.mainFileUri)
.map((uri) => new URI(uri))
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
.map((uri) => ({
uri,
error: Sketch.validateSketchFolderName(uri.path.name),
}))
.filter(({ error }) => Boolean(error))
.map(({ uri }) => ({
execute: async () => {
const unknown =
(await this.promptRenameSketchFile(uri)) &&
(await this.commandService.executeCommand(
WorkspaceCommands.FILE_RENAME.id,
uri
));
return !!unknown;
},
}));
}

private async currentSketch(): Promise<Sketch> {
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
if (CurrentSketch.isValid(sketch)) {
return sketch;
}
const deferred = new Deferred<Sketch>();
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
(sketch) => {
if (CurrentSketch.isValid(sketch)) {
disposable.dispose();
deferred.resolve(sketch);
}
}
);
return deferred.promise;
}

private async promptFixActions(): Promise<boolean> {
const sketch = await this.currentSketch();
const fixActions = this.validateSketch(sketch);
for (const fixAction of fixActions) {
const result = await fixAction.execute();
if (!result) {
return false;
}
}
return true;
}

private async promptRenameSketch(sketch: Sketch): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFolderTitle',
'Invalid sketch name'
),
nls.localize(
'arduino/validateSketch/renameSketchFolderMessage',
"The sketch '{0}' cannot be used. Sketch names must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters. To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
sketch.name
)
);
}

private async promptRenameSketchFile(uri: URI): Promise<boolean> {
return this.prompt(
nls.localize(
'arduino/validateSketch/renameSketchFileTitle',
'Invalid sketch filename'
),
nls.localize(
'arduino/validateSketch/renameSketchFileMessage',
"The sketch file '{0}' cannot be used. Sketch filenames must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters without the file extension. To get rid of this message, rename the sketch file. Do you want to rename the sketch file now?",
uri.path.base
)
);
}

private async prompt(
title: string,
message: string,
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
): Promise<boolean> {
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
title,
message,
type: 'warning',
buttons,
}
);
// cancel
if (response === 0) {
return false;
}
return true;
}
}

interface FixAction {
execute(): Promise<boolean>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface VerifySketchParams {
}

/**
* - `"idle"` when neither verify, not upload is running,
* - `"idle"` when neither verify, nor upload is running,
* - `"explicit-verify"` when only verify is running triggered by the user, and
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
*/
Expand Down
Loading