Skip to content

Commit

Permalink
feat: copy sketch to the cloud
Browse files Browse the repository at this point in the history
Closes #1876

Signed-off-by: Akos Kitta <[email protected]>
  • Loading branch information
Akos Kitta committed Feb 24, 2023
1 parent eca7922 commit 7e6d933
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ import { CreateFeatures } from './create/create-features';
import { Account } from './contributions/account';
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import { CreateCloudCopy } from './contributions/create-cloud-copy';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
Expand Down Expand Up @@ -741,6 +742,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, RenameCloudSketch);
Contribution.configure(bind, Account);
Contribution.configure(bind, CloudSketchbookContribution);
Contribution.configure(bind, CreateCloudCopy);

bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
Expand Down
108 changes: 108 additions & 0 deletions arduino-ide-extension/src/browser/contributions/create-cloud-copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { nls } from '@theia/core/lib/common/nls';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ApplicationShell } from '@theia/core/lib/browser/shell';
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { injectable } from '@theia/core/shared/inversify';
import { Create } from '../create/typings';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
import {
CreateNewCloudSketchCallback,
NewCloudSketch,
NewCloudSketchParams,
} from './new-cloud-sketch';
import { saveOntoCopiedSketch } from './save-as-sketch';

interface CreateCloudCopyParams {
readonly model: SketchbookTreeModel;
readonly node: SketchbookTree.SketchDirNode;
}
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
return (
(<CreateCloudCopyParams>arg).model !== undefined &&
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
(<CreateCloudCopyParams>arg).node !== undefined &&
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
);
}

@injectable()
export class CreateCloudCopy extends CloudSketchContribution {
private shell: ApplicationShell;

override onStart(app: FrontendApplication): void {
this.shell = app.shell;
}

override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
isEnabled: (args: unknown) =>
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
isVisible: (args: unknown) =>
Boolean(this.createFeatures.enabled) &&
Boolean(this.createFeatures.session) &&
isCreateCloudCopyParams(args),
});
}

/**
* - creates new cloud sketch with the name of the params sketch,
* - pulls the cloud sketch,
* - copies files from params sketch to pulled cloud sketch in the cache folder,
* - pushes the cloud sketch, and
* - opens in new window.
*/
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
const sketch = await this.sketchesService.loadSketch(
params.node.fileStat.resource.toString()
);
const callback: CreateNewCloudSketchCallback = async (
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
) => {
const treeModel = await this.treeModel();
if (!treeModel) {
throw new Error('Could not retrieve the cloud sketchbook tree model.');
}

progress.report({
message: nls.localize(
'arduino/createCloudCopy/copyingSketchFilesMessage',
'Copying local sketch files...'
),
});
const localCacheFolderUri = newNode.uri.toString();
await this.sketchesService.copy(sketch, { destinationUri: localCacheFolderUri, onlySketchFiles: true });
await saveOntoCopiedSketch(
sketch,
localCacheFolderUri,
this.shell,
this.editorManager
);

progress.report({ message: pushingSketch(newSketch.name) });
await treeModel.sketchbookTree().push(newNode);
};
return this.commandService.executeCommand(
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
<NewCloudSketchParams>{
initialValue: params.node.fileStat.name,
callback,
}
);
}
}

export namespace CreateCloudCopy {
export namespace Commands {
export const CREATE_CLOUD_COPY: Command = {
id: 'arduino-create-cloud-copy',
iconClass: 'push-sketch-icon',
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import { Create, isConflict } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
TaskFactoryImpl,
Expand All @@ -15,13 +15,32 @@ import {
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { Command, CommandRegistry, Sketch } from './contribution';
import {
CloudSketchContribution,
pullingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch } from './contribution';

export interface CreateNewCloudSketchCallback {
(
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
): Promise<void>;
}

export interface NewCloudSketchParams {
/**
* Value to populate the dialog `<input>` when it opens.
*/
readonly initialValue?: string | undefined;
/**
* Additional callback to call when the new cloud sketch has been created.
*/
readonly callback?: CreateNewCloudSketchCallback;
}

@injectable()
export class NewCloudSketch extends CloudSketchContribution {
Expand All @@ -43,7 +62,8 @@ export class NewCloudSketch extends CloudSketchContribution {

override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: () => this.createNewSketch(true),
execute: (params: NewCloudSketchParams) =>
this.createNewSketch(true, params.initialValue, params.callback),
isEnabled: () => Boolean(this.createFeatures.session),
isVisible: () => this.createFeatures.enabled,
});
Expand All @@ -66,7 +86,8 @@ export class NewCloudSketch extends CloudSketchContribution {

private async createNewSketch(
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const treeModel = await this.treeModel();
if (treeModel) {
Expand All @@ -75,7 +96,8 @@ export class NewCloudSketch extends CloudSketchContribution {
rootNode,
treeModel,
skipShowErrorMessageOnOpen,
initialValue
initialValue,
callback
);
}
}
Expand All @@ -84,13 +106,14 @@ export class NewCloudSketch extends CloudSketchContribution {
rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.createNewSketchWithProgress(treeModel, value)
this.createNewSketchWithProgress(treeModel, value, callback)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
Expand Down Expand Up @@ -118,15 +141,20 @@ export class NewCloudSketch extends CloudSketchContribution {
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.createNewSketch(false, taskFactory.value ?? initialValue);
return this.createNewSketch(
false,
taskFactory.value ?? initialValue,
callback
);
}
throw err;
}
}

private createNewSketchWithProgress(
treeModel: CloudSketchbookTreeModel,
value: string
value: string,
callback?: CreateNewCloudSketchCallback
): (
progress: Progress
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
Expand All @@ -143,6 +171,9 @@ export class NewCloudSketch extends CloudSketchContribution {
await treeModel.refresh();
progress.report({ message: pullingSketch(sketch.name) });
const node = await this.pull(sketch);
if (callback && node) {
await callback(sketch, node, progress);
}
return node;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class RenameCloudSketch extends CloudSketchContribution {
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
// push
progress.report({ message: pushingSketch(params.sketch.name) });
await treeModel.sketchbookTree().push(node);
await treeModel.sketchbookTree().push(node, true);

// rename
progress.report({
Expand Down
102 changes: 53 additions & 49 deletions arduino-ide-extension/src/browser/contributions/save-as-sketch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
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 { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
import { StartupTask } from '../../electron-common/startup-task';
import { ArduinoMenus } from '../menu/arduino-menus';
Expand All @@ -28,7 +29,7 @@ import {
@injectable()
export class SaveAsSketch extends CloudSketchContribution {
@inject(ApplicationShell)
private readonly applicationShell: ApplicationShell;
private readonly shell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;

Expand Down Expand Up @@ -87,7 +88,12 @@ export class SaveAsSketch extends CloudSketchContribution {
return false;
}

await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
await saveOntoCopiedSketch(
sketch,
newWorkspaceUri,
this.shell,
this.editorManager
);
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
Expand Down Expand Up @@ -238,53 +244,6 @@ ${dialogContent.question}`.trim();
}
return sketchFolderDestinationUri;
}

private async saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string
): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
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 (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
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(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);
}
})
);
}
}

interface InvalidSketchFolderDialogContent {
Expand Down Expand Up @@ -317,3 +276,48 @@ export namespace SaveAsSketch {
};
}
}

export async function saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string,
shell: ApplicationShell,
editorManager: EditorManager
): Promise<void> {
const widgets = shell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
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 (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
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(newSketchFolderUri + path);
try {
const widget = await editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
})
);
}
Loading

0 comments on commit 7e6d933

Please sign in to comment.