Skip to content

Commit 7e6d933

Browse files
author
Akos Kitta
committed
feat: copy sketch to the cloud
Closes #1876 Signed-off-by: Akos Kitta <[email protected]>
1 parent eca7922 commit 7e6d933

11 files changed

+274
-82
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ import { CreateFeatures } from './create/create-features';
352352
import { Account } from './contributions/account';
353353
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
354354
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
355+
import { CreateCloudCopy } from './contributions/create-cloud-copy';
355356

356357
export default new ContainerModule((bind, unbind, isBound, rebind) => {
357358
// Commands and toolbar items
@@ -741,6 +742,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
741742
Contribution.configure(bind, RenameCloudSketch);
742743
Contribution.configure(bind, Account);
743744
Contribution.configure(bind, CloudSketchbookContribution);
745+
Contribution.configure(bind, CreateCloudCopy);
744746

745747
bindContributionProvider(bind, StartupTaskProvider);
746748
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { nls } from '@theia/core/lib/common/nls';
2+
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
3+
import { ApplicationShell } from '@theia/core/lib/browser/shell';
4+
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
5+
import { Progress } from '@theia/core/lib/common/message-service-protocol';
6+
import { injectable } from '@theia/core/shared/inversify';
7+
import { Create } from '../create/typings';
8+
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
9+
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
10+
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
11+
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
12+
import {
13+
CreateNewCloudSketchCallback,
14+
NewCloudSketch,
15+
NewCloudSketchParams,
16+
} from './new-cloud-sketch';
17+
import { saveOntoCopiedSketch } from './save-as-sketch';
18+
19+
interface CreateCloudCopyParams {
20+
readonly model: SketchbookTreeModel;
21+
readonly node: SketchbookTree.SketchDirNode;
22+
}
23+
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
24+
return (
25+
(<CreateCloudCopyParams>arg).model !== undefined &&
26+
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
27+
(<CreateCloudCopyParams>arg).node !== undefined &&
28+
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
29+
);
30+
}
31+
32+
@injectable()
33+
export class CreateCloudCopy extends CloudSketchContribution {
34+
private shell: ApplicationShell;
35+
36+
override onStart(app: FrontendApplication): void {
37+
this.shell = app.shell;
38+
}
39+
40+
override registerCommands(registry: CommandRegistry): void {
41+
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
42+
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
43+
isEnabled: (args: unknown) =>
44+
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
45+
isVisible: (args: unknown) =>
46+
Boolean(this.createFeatures.enabled) &&
47+
Boolean(this.createFeatures.session) &&
48+
isCreateCloudCopyParams(args),
49+
});
50+
}
51+
52+
/**
53+
* - creates new cloud sketch with the name of the params sketch,
54+
* - pulls the cloud sketch,
55+
* - copies files from params sketch to pulled cloud sketch in the cache folder,
56+
* - pushes the cloud sketch, and
57+
* - opens in new window.
58+
*/
59+
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
60+
const sketch = await this.sketchesService.loadSketch(
61+
params.node.fileStat.resource.toString()
62+
);
63+
const callback: CreateNewCloudSketchCallback = async (
64+
newSketch: Create.Sketch,
65+
newNode: CloudSketchbookTree.CloudSketchDirNode,
66+
progress: Progress
67+
) => {
68+
const treeModel = await this.treeModel();
69+
if (!treeModel) {
70+
throw new Error('Could not retrieve the cloud sketchbook tree model.');
71+
}
72+
73+
progress.report({
74+
message: nls.localize(
75+
'arduino/createCloudCopy/copyingSketchFilesMessage',
76+
'Copying local sketch files...'
77+
),
78+
});
79+
const localCacheFolderUri = newNode.uri.toString();
80+
await this.sketchesService.copy(sketch, { destinationUri: localCacheFolderUri, onlySketchFiles: true });
81+
await saveOntoCopiedSketch(
82+
sketch,
83+
localCacheFolderUri,
84+
this.shell,
85+
this.editorManager
86+
);
87+
88+
progress.report({ message: pushingSketch(newSketch.name) });
89+
await treeModel.sketchbookTree().push(newNode);
90+
};
91+
return this.commandService.executeCommand(
92+
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
93+
<NewCloudSketchParams>{
94+
initialValue: params.node.fileStat.name,
95+
callback,
96+
}
97+
);
98+
}
99+
}
100+
101+
export namespace CreateCloudCopy {
102+
export namespace Commands {
103+
export const CREATE_CLOUD_COPY: Command = {
104+
id: 'arduino-create-cloud-copy',
105+
iconClass: 'push-sketch-icon',
106+
};
107+
}
108+
}

Diff for: arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts

+40-9
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Progress } from '@theia/core/lib/common/message-service-protocol';
66
import { nls } from '@theia/core/lib/common/nls';
77
import { injectable } from '@theia/core/shared/inversify';
88
import { CreateUri } from '../create/create-uri';
9-
import { isConflict } from '../create/typings';
9+
import { Create, isConflict } from '../create/typings';
1010
import { ArduinoMenus } from '../menu/arduino-menus';
1111
import {
1212
TaskFactoryImpl,
@@ -15,13 +15,32 @@ import {
1515
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
1616
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
1717
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
18-
import { Command, CommandRegistry, Sketch } from './contribution';
1918
import {
2019
CloudSketchContribution,
2120
pullingSketch,
2221
sketchAlreadyExists,
2322
synchronizingSketchbook,
2423
} from './cloud-contribution';
24+
import { Command, CommandRegistry, Sketch } from './contribution';
25+
26+
export interface CreateNewCloudSketchCallback {
27+
(
28+
newSketch: Create.Sketch,
29+
newNode: CloudSketchbookTree.CloudSketchDirNode,
30+
progress: Progress
31+
): Promise<void>;
32+
}
33+
34+
export interface NewCloudSketchParams {
35+
/**
36+
* Value to populate the dialog `<input>` when it opens.
37+
*/
38+
readonly initialValue?: string | undefined;
39+
/**
40+
* Additional callback to call when the new cloud sketch has been created.
41+
*/
42+
readonly callback?: CreateNewCloudSketchCallback;
43+
}
2544

2645
@injectable()
2746
export class NewCloudSketch extends CloudSketchContribution {
@@ -43,7 +62,8 @@ export class NewCloudSketch extends CloudSketchContribution {
4362

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

6787
private async createNewSketch(
6888
skipShowErrorMessageOnOpen: boolean,
69-
initialValue?: string | undefined
89+
initialValue?: string | undefined,
90+
callback?: CreateNewCloudSketchCallback
7091
): Promise<void> {
7192
const treeModel = await this.treeModel();
7293
if (treeModel) {
@@ -75,7 +96,8 @@ export class NewCloudSketch extends CloudSketchContribution {
7596
rootNode,
7697
treeModel,
7798
skipShowErrorMessageOnOpen,
78-
initialValue
99+
initialValue,
100+
callback
79101
);
80102
}
81103
}
@@ -84,13 +106,14 @@ export class NewCloudSketch extends CloudSketchContribution {
84106
rootNode: CompositeTreeNode,
85107
treeModel: CloudSketchbookTreeModel,
86108
skipShowErrorMessageOnOpen: boolean,
87-
initialValue?: string | undefined
109+
initialValue?: string | undefined,
110+
callback?: CreateNewCloudSketchCallback
88111
): Promise<void> {
89112
const existingNames = rootNode.children
90113
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
91114
.map(({ fileStat }) => fileStat.name);
92115
const taskFactory = new TaskFactoryImpl((value) =>
93-
this.createNewSketchWithProgress(treeModel, value)
116+
this.createNewSketchWithProgress(treeModel, value, callback)
94117
);
95118
try {
96119
const dialog = new WorkspaceInputDialogWithProgress(
@@ -118,15 +141,20 @@ export class NewCloudSketch extends CloudSketchContribution {
118141
} catch (err) {
119142
if (isConflict(err)) {
120143
await treeModel.refresh();
121-
return this.createNewSketch(false, taskFactory.value ?? initialValue);
144+
return this.createNewSketch(
145+
false,
146+
taskFactory.value ?? initialValue,
147+
callback
148+
);
122149
}
123150
throw err;
124151
}
125152
}
126153

127154
private createNewSketchWithProgress(
128155
treeModel: CloudSketchbookTreeModel,
129-
value: string
156+
value: string,
157+
callback?: CreateNewCloudSketchCallback
130158
): (
131159
progress: Progress
132160
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
@@ -143,6 +171,9 @@ export class NewCloudSketch extends CloudSketchContribution {
143171
await treeModel.refresh();
144172
progress.report({ message: pullingSketch(sketch.name) });
145173
const node = await this.pull(sketch);
174+
if (callback && node) {
175+
await callback(sketch, node, progress);
176+
}
146177
return node;
147178
};
148179
}

Diff for: arduino-ide-extension/src/browser/contributions/rename-cloud-sketch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class RenameCloudSketch extends CloudSketchContribution {
123123
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
124124
// push
125125
progress.report({ message: pushingSketch(params.sketch.name) });
126-
await treeModel.sketchbookTree().push(node);
126+
await treeModel.sketchbookTree().push(node, true);
127127

128128
// rename
129129
progress.report({

Diff for: arduino-ide-extension/src/browser/contributions/save-as-sketch.ts

+53-49
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
66
import { WindowService } from '@theia/core/lib/browser/window/window-service';
77
import { nls } from '@theia/core/lib/common/nls';
88
import { inject, injectable } from '@theia/core/shared/inversify';
9+
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
910
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
1011
import { StartupTask } from '../../electron-common/startup-task';
1112
import { ArduinoMenus } from '../menu/arduino-menus';
@@ -28,7 +29,7 @@ import {
2829
@injectable()
2930
export class SaveAsSketch extends CloudSketchContribution {
3031
@inject(ApplicationShell)
31-
private readonly applicationShell: ApplicationShell;
32+
private readonly shell: ApplicationShell;
3233
@inject(WindowService)
3334
private readonly windowService: WindowService;
3435

@@ -87,7 +88,12 @@ export class SaveAsSketch extends CloudSketchContribution {
8788
return false;
8889
}
8990

90-
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
91+
await saveOntoCopiedSketch(
92+
sketch,
93+
newWorkspaceUri,
94+
this.shell,
95+
this.editorManager
96+
);
9197
if (markAsRecentlyOpened) {
9298
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
9399
}
@@ -238,53 +244,6 @@ ${dialogContent.question}`.trim();
238244
}
239245
return sketchFolderDestinationUri;
240246
}
241-
242-
private async saveOntoCopiedSketch(
243-
sketch: Sketch,
244-
newSketchFolderUri: string
245-
): Promise<void> {
246-
const widgets = this.applicationShell.widgets;
247-
const snapshots = new Map<string, Saveable.Snapshot>();
248-
for (const widget of widgets) {
249-
const saveable = Saveable.getDirty(widget);
250-
const uri = NavigatableWidget.getUri(widget);
251-
if (!uri) {
252-
continue;
253-
}
254-
const uriString = uri.toString();
255-
let relativePath: string;
256-
if (
257-
uriString.includes(sketch.uri) &&
258-
saveable &&
259-
saveable.createSnapshot
260-
) {
261-
// The main file will change its name during the copy process
262-
// We need to store the new name in the map
263-
if (sketch.mainFileUri === uriString) {
264-
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
265-
relativePath = '/' + lastPart;
266-
} else {
267-
relativePath = uri.toString().substring(sketch.uri.length);
268-
}
269-
snapshots.set(relativePath, saveable.createSnapshot());
270-
}
271-
}
272-
await Promise.all(
273-
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
274-
const widgetUri = new URI(newSketchFolderUri + path);
275-
try {
276-
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
277-
const saveable = Saveable.get(widget);
278-
if (saveable && saveable.applySnapshot) {
279-
saveable.applySnapshot(snapshot);
280-
await saveable.save();
281-
}
282-
} catch (e) {
283-
console.error(e);
284-
}
285-
})
286-
);
287-
}
288247
}
289248

290249
interface InvalidSketchFolderDialogContent {
@@ -317,3 +276,48 @@ export namespace SaveAsSketch {
317276
};
318277
}
319278
}
279+
280+
export async function saveOntoCopiedSketch(
281+
sketch: Sketch,
282+
newSketchFolderUri: string,
283+
shell: ApplicationShell,
284+
editorManager: EditorManager
285+
): Promise<void> {
286+
const widgets = shell.widgets;
287+
const snapshots = new Map<string, Saveable.Snapshot>();
288+
for (const widget of widgets) {
289+
const saveable = Saveable.getDirty(widget);
290+
const uri = NavigatableWidget.getUri(widget);
291+
if (!uri) {
292+
continue;
293+
}
294+
const uriString = uri.toString();
295+
let relativePath: string;
296+
if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) {
297+
// The main file will change its name during the copy process
298+
// We need to store the new name in the map
299+
if (sketch.mainFileUri === uriString) {
300+
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
301+
relativePath = '/' + lastPart;
302+
} else {
303+
relativePath = uri.toString().substring(sketch.uri.length);
304+
}
305+
snapshots.set(relativePath, saveable.createSnapshot());
306+
}
307+
}
308+
await Promise.all(
309+
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
310+
const widgetUri = new URI(newSketchFolderUri + path);
311+
try {
312+
const widget = await editorManager.getOrCreateByUri(widgetUri);
313+
const saveable = Saveable.get(widget);
314+
if (saveable && saveable.applySnapshot) {
315+
saveable.applySnapshot(snapshot);
316+
await saveable.save();
317+
}
318+
} catch (e) {
319+
console.error(e);
320+
}
321+
})
322+
);
323+
}

0 commit comments

Comments
 (0)