Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 72df8c1

Browse files
author
Akos Kitta
committedJan 25, 2023
fix: local sketch deletion + rename
Ref: #1825 Signed-off-by: Akos Kitta <[email protected]>
1 parent 2aea3df commit 72df8c1

File tree

7 files changed

+157
-107
lines changed

7 files changed

+157
-107
lines changed
 

‎arduino-ide-extension/src/browser/contributions/sketch-control.ts

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,19 @@ import {
1818
open,
1919
} from './contribution';
2020
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
21-
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
22-
import {
23-
CurrentSketch,
24-
SketchesServiceClientImpl,
25-
} from '../../common/protocol/sketches-service-client-impl';
26-
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
21+
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
2722
import { nls } from '@theia/core/lib/common';
2823

2924
@injectable()
3025
export class SketchControl extends SketchContribution {
3126
@inject(ApplicationShell)
32-
protected readonly shell: ApplicationShell;
27+
private readonly shell: ApplicationShell;
3328

3429
@inject(MenuModelRegistry)
35-
protected readonly menuRegistry: MenuModelRegistry;
30+
private readonly menuRegistry: MenuModelRegistry;
3631

3732
@inject(ContextMenuRenderer)
38-
protected readonly contextMenuRenderer: ContextMenuRenderer;
39-
40-
@inject(EditorManager)
41-
protected override readonly editorManager: EditorManager;
42-
43-
@inject(SketchesServiceClientImpl)
44-
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
45-
46-
@inject(LocalCacheFsProvider)
47-
protected readonly localCacheFsProvider: LocalCacheFsProvider;
33+
private readonly contextMenuRenderer: ContextMenuRenderer;
4834

4935
protected readonly toDisposeBeforeCreateNewContextMenu =
5036
new DisposableCollection();
@@ -84,12 +70,7 @@ export class SketchControl extends SketchContribution {
8470
);
8571

8672
// if the current file is in the current opened sketch, show extra menus
87-
if (
88-
sketch &&
89-
parentSketch &&
90-
parentSketch.uri === sketch.uri &&
91-
this.allowRename(parentSketch.uri)
92-
) {
73+
if (sketch && parentSketch && parentSketch.uri === sketch.uri) {
9374
this.menuRegistry.registerMenuAction(
9475
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
9576
{
@@ -121,12 +102,7 @@ export class SketchControl extends SketchContribution {
121102
);
122103
}
123104

124-
if (
125-
sketch &&
126-
parentSketch &&
127-
parentSketch.uri === sketch.uri &&
128-
this.allowDelete(parentSketch.uri)
129-
) {
105+
if (sketch && parentSketch && parentSketch.uri === sketch.uri) {
130106
this.menuRegistry.registerMenuAction(
131107
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
132108
{
@@ -249,27 +225,6 @@ export class SketchControl extends SketchContribution {
249225
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
250226
});
251227
}
252-
253-
protected isCloudSketch(uri: string): boolean {
254-
try {
255-
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
256-
257-
if (cloudCacheLocation) {
258-
return true;
259-
}
260-
return false;
261-
} catch {
262-
return false;
263-
}
264-
}
265-
266-
protected allowRename(uri: string): boolean {
267-
return !this.isCloudSketch(uri);
268-
}
269-
270-
protected allowDelete(uri: string): boolean {
271-
return !this.isCloudSketch(uri);
272-
}
273228
}
274229

275230
export namespace SketchControl {

‎arduino-ide-extension/src/browser/create/create-api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export namespace ResponseResultProvider {
1515
export const JSON: ResponseResultProvider = (response) => response.json();
1616
}
1717

18+
// TODO: check if this is still needed: https://github.com/electron/electron/issues/18733
19+
// The original issue was reported for Electron 5.x and 6.x. Theia uses 15.x
1820
export function Utf8ArrayToStr(array: Uint8Array): string {
1921
let out, i, c;
2022
let char2, char3;
@@ -381,7 +383,7 @@ export class CreateApi {
381383
return;
382384
}
383385

384-
// do not upload "do_not_sync" files/directoris and their descendants
386+
// do not upload "do_not_sync" files/directories and their descendants
385387
const segments = posixPath.split(posix.sep) || [];
386388
if (
387389
segments.some((segment) => Create.do_not_sync_files.includes(segment))
Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
1+
import type { Widget } from '@phosphor/widgets';
22
import * as remote from '@theia/core/electron-shared/@electron/remote';
3+
import { Dialog } from '@theia/core/lib/browser/dialogs';
4+
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
5+
import { WindowService } from '@theia/core/lib/browser/window/window-service';
6+
import { nls } from '@theia/core/lib/common';
7+
import type { MaybeArray } from '@theia/core/lib/common/types';
38
import URI from '@theia/core/lib/common/uri';
9+
import { inject, injectable } from '@theia/core/shared/inversify';
410
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
11+
import { SketchesService } from '../../../common/protocol';
512
import {
613
CurrentSketch,
714
SketchesServiceClientImpl,
815
} from '../../../common/protocol/sketches-service-client-impl';
9-
import { nls } from '@theia/core/lib/common';
16+
import { Sketch } from '../../contributions/contribution';
1017

1118
@injectable()
1219
export class WorkspaceDeleteHandler extends TheiaWorkspaceDeleteHandler {
20+
@inject(WindowService)
21+
private readonly windowService: WindowService;
22+
@inject(SketchesService)
23+
private readonly sketchesService: SketchesService;
1324
@inject(SketchesServiceClientImpl)
14-
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
25+
private readonly sketchesServiceClient: SketchesServiceClientImpl;
1526

1627
override async execute(uris: URI[]): Promise<void> {
1728
const sketch = await this.sketchesServiceClient.currentSketch();
@@ -27,29 +38,48 @@ export class WorkspaceDeleteHandler extends TheiaWorkspaceDeleteHandler {
2738
const { response } = await remote.dialog.showMessageBox({
2839
title: nls.localize('vscode/fileActions/delete', 'Delete'),
2940
type: 'question',
30-
buttons: [
31-
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
32-
nls.localize('vscode/issueMainService/ok', 'OK'),
33-
],
41+
buttons: [Dialog.CANCEL, Dialog.OK],
3442
message: nls.localize(
3543
'theia/workspace/deleteCurrentSketch',
3644
'Do you want to delete the current sketch?'
3745
),
3846
});
3947
if (response === 1) {
4048
// OK
41-
await Promise.all(
42-
[
43-
...sketch.additionalFileUris,
44-
...sketch.otherSketchFileUris,
45-
sketch.mainFileUri,
46-
].map((uri) => this.closeWithoutSaving(new URI(uri)))
47-
);
48-
await this.fileService.delete(new URI(sketch.uri));
49-
window.close();
49+
await Promise.all([
50+
...Sketch.uris(sketch).map((uri) =>
51+
this.closeWithoutSaving(new URI(uri))
52+
),
53+
]);
54+
this.windowService.setSafeToShutDown();
55+
this.sketchesService.deleteSketch(sketch);
56+
return window.close();
5057
}
51-
return;
5258
}
5359
return super.execute(uris);
5460
}
61+
62+
// https://github.com/eclipse-theia/theia/issues/12107
63+
protected override async closeWithoutSaving(uri: URI): Promise<void> {
64+
const affected = getAffected(this.shell.widgets, uri);
65+
const toClose = [...affected].map(([, widget]) => widget);
66+
await this.shell.closeMany(toClose, { save: false });
67+
}
68+
}
69+
70+
export function getAffected<T extends Widget>(
71+
widgets: Iterable<T>,
72+
context: MaybeArray<URI>
73+
): [URI, T & NavigatableWidget][] {
74+
const uris = Array.isArray(context) ? context : [context];
75+
const result: [URI, T & NavigatableWidget][] = [];
76+
for (const widget of widgets) {
77+
if (NavigatableWidget.is(widget)) {
78+
const resourceUri = widget.getResourceUri();
79+
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
80+
result.push([resourceUri, widget]);
81+
}
82+
}
83+
}
84+
return result;
5585
}

‎arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { SketchCache } from './cloud-sketch-cache';
22
import { inject, injectable } from '@theia/core/shared/inversify';
33
import URI from '@theia/core/lib/common/uri';
44
import { MaybePromise } from '@theia/core/lib/common/types';
5-
import { FileService } from '@theia/filesystem/lib/browser/file-service';
65
import { FileStatNode } from '@theia/filesystem/lib/browser/file-tree';
76
import { Command } from '@theia/core/lib/common/command';
87
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
@@ -28,8 +27,6 @@ import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
2827
import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
2928
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
3029
import { firstToUpperCase } from '../../../common/utils';
31-
import { ArduinoPreferences } from '../../arduino-preferences';
32-
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
3330
import { FileStat } from '@theia/filesystem/lib/common/files';
3431
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
3532
import { posix, splitSketchPath } from '../../create/create-paths';
@@ -46,29 +43,20 @@ type FilesToSync = {
4643
};
4744
@injectable()
4845
export class CloudSketchbookTree extends SketchbookTree {
49-
@inject(FileService)
50-
protected override readonly fileService: FileService;
51-
5246
@inject(LocalCacheFsProvider)
53-
protected readonly localCacheFsProvider: LocalCacheFsProvider;
47+
private readonly localCacheFsProvider: LocalCacheFsProvider;
5448

5549
@inject(SketchCache)
56-
protected readonly sketchCache: SketchCache;
57-
58-
@inject(ArduinoPreferences)
59-
protected override readonly arduinoPreferences: ArduinoPreferences;
50+
private readonly sketchCache: SketchCache;
6051

6152
@inject(PreferenceService)
62-
protected readonly preferenceService: PreferenceService;
53+
private readonly preferenceService: PreferenceService;
6354

6455
@inject(MessageService)
65-
protected readonly messageService: MessageService;
66-
67-
@inject(SketchesServiceClientImpl)
68-
protected readonly sketchServiceClient: SketchesServiceClientImpl;
56+
private readonly messageService: MessageService;
6957

7058
@inject(CreateApi)
71-
protected readonly createApi: CreateApi;
59+
private readonly createApi: CreateApi;
7260

7361
async pushPublicWarn(
7462
node: CloudSketchbookTree.CloudSketchDirNode
@@ -93,15 +81,13 @@ export class CloudSketchbookTree extends SketchbookTree {
9381
PreferenceScope.User
9482
),
9583
}).open();
96-
if (!ok) {
97-
return false;
98-
}
99-
return true;
84+
return Boolean(ok);
10085
} else {
10186
return true;
10287
}
10388
}
10489

90+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
10591
async pull(arg: any): Promise<void> {
10692
const {
10793
// model,
@@ -136,6 +122,7 @@ export class CloudSketchbookTree extends SketchbookTree {
136122
return;
137123
}
138124
}
125+
// this.sketchCache.init();
139126
return this.runWithState(node, 'pulling', async (node) => {
140127
const commandsCopy = node.commands;
141128
node.commands = [];
@@ -229,7 +216,7 @@ export class CloudSketchbookTree extends SketchbookTree {
229216
});
230217
}
231218

232-
async recursiveURIs(uri: URI): Promise<URI[]> {
219+
private async recursiveURIs(uri: URI): Promise<URI[]> {
233220
// remote resources can be fetched one-shot via api
234221
if (CreateUri.is(uri)) {
235222
const resources = await this.createApi.readDirectory(
@@ -286,15 +273,15 @@ export class CloudSketchbookTree extends SketchbookTree {
286273
}, {});
287274
}
288275

289-
async getUrisMap(uri: URI) {
276+
private async getUrisMap(uri: URI): Promise<Record<string, URI>> {
290277
const basepath = uri.toString();
291278
const exists = await this.fileService.exists(uri);
292279
const uris =
293280
(exists && this.URIsToMap(await this.recursiveURIs(uri), basepath)) || {};
294281
return uris;
295282
}
296283

297-
async treeDiff(source: URI, dest: URI): Promise<FilesToSync> {
284+
private async treeDiff(source: URI, dest: URI): Promise<FilesToSync> {
298285
const [sourceURIs, destURIs] = await Promise.all([
299286
this.getUrisMap(source),
300287
this.getUrisMap(dest),
@@ -356,7 +343,7 @@ export class CloudSketchbookTree extends SketchbookTree {
356343
}
357344
}
358345

359-
async sync(source: URI, dest: URI) {
346+
private async sync(source: URI, dest: URI): Promise<void> {
360347
const { filesToWrite, filesToDelete } = await this.treeDiff(source, dest);
361348
await Promise.all(
362349
filesToWrite.map(async ({ source, dest }) => {
@@ -375,7 +362,9 @@ export class CloudSketchbookTree extends SketchbookTree {
375362
);
376363
}
377364

378-
override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
365+
override async resolveChildren(
366+
parent: CompositeTreeNode
367+
): Promise<TreeNode[]> {
379368
return (await super.resolveChildren(parent)).sort((a, b) => {
380369
if (
381370
WorkspaceNode.is(parent) &&
@@ -453,6 +442,7 @@ export class CloudSketchbookTree extends SketchbookTree {
453442
if (!CreateUri.is(childFs.resource)) {
454443
let refUri = node.fileStat.resource;
455444
if (node.fileStat.hasOwnProperty('remoteUri')) {
445+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
456446
refUri = (node.fileStat as any).remoteUri;
457447
}
458448
remoteUri = refUri.resolve(childFs.name);
@@ -471,6 +461,7 @@ export class CloudSketchbookTree extends SketchbookTree {
471461
}
472462

473463
protected override toNode(
464+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
474465
fileStat: any,
475466
parent: CompositeTreeNode
476467
): FileNode | DirNode {
@@ -621,6 +612,7 @@ export class CloudSketchbookTree extends SketchbookTree {
621612
if (DecoratedTreeNode.is(node)) {
622613
for (const property of Object.keys(decorationData)) {
623614
if (node.decorationData.hasOwnProperty(property)) {
615+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
624616
delete (node.decorationData as any)[property];
625617
}
626618
}

‎arduino-ide-extension/src/node/arduino-ide-backend-module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
196196
// Shared sketches service
197197
bind(SketchesServiceImpl).toSelf().inSingletonScope();
198198
bind(SketchesService).toService(SketchesServiceImpl);
199+
bind(BackendApplicationContribution).toService(SketchesServiceImpl);
199200
bind(ConnectionHandler)
200201
.toDynamicValue(
201202
(context) =>

‎arduino-ide-extension/src/node/boards-service-impl.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
BoardsPackage,
88
Board,
99
BoardDetails,
10-
Tool,
1110
ConfigOption,
1211
ConfigValue,
1312
Programmer,
@@ -97,14 +96,11 @@ export class BoardsServiceImpl
9796

9897
const debuggingSupported = detailsResp.getDebuggingSupported();
9998

100-
const requiredTools = detailsResp.getToolsDependenciesList().map(
101-
(t) =>
102-
<Tool>{
103-
name: t.getName(),
104-
packager: t.getPackager(),
105-
version: t.getVersion(),
106-
}
107-
);
99+
const requiredTools = detailsResp.getToolsDependenciesList().map((t) => ({
100+
name: t.getName(),
101+
packager: t.getPackager(),
102+
version: t.getVersion(),
103+
}));
108104

109105
const configOptions = detailsResp.getConfigOptionsList().map(
110106
(c) =>

‎arduino-ide-extension/src/node/sketches-service-impl.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { injectable, inject, named } from '@theia/core/shared/inversify';
2-
import { promises as fs, realpath, lstat, Stats, constants, rm } from 'fs';
2+
import {
3+
promises as fs,
4+
realpath,
5+
lstat,
6+
Stats,
7+
constants,
8+
rm,
9+
rmSync,
10+
} from 'fs';
311
import * as os from 'os';
412
import * as temp from 'temp';
513
import * as path from 'path';
@@ -43,6 +51,11 @@ import {
4351
firstToUpperCase,
4452
startsWithUpperCase,
4553
} from '../common/utils';
54+
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
55+
import {
56+
Disposable,
57+
DisposableCollection,
58+
} from '@theia/core/lib/common/disposable';
4659

4760
const RecentSketches = 'recent-sketches.json';
4861
const DefaultIno = `void setup() {
@@ -59,7 +72,7 @@ void loop() {
5972
@injectable()
6073
export class SketchesServiceImpl
6174
extends CoreClientAware
62-
implements SketchesService
75+
implements SketchesService, BackendApplicationContribution
6376
{
6477
private sketchSuffixIndex = 1;
6578
private lastSketchBaseName: string;
@@ -69,6 +82,32 @@ export class SketchesServiceImpl
6982
concurrency: 1,
7083
});
7184
private inoContent: Deferred<string> | undefined;
85+
/**
86+
* It contains all things the IDE2 must clean up before a normal stop.
87+
*
88+
* When deleting the sketch, the IDE2 must close the browser window and
89+
* recursively delete the sketch folder from the filesystem. The sketch
90+
* cannot be deleted when the window is open because that is the currently
91+
* opened workspace. IDE2 cannot delete the sketch folder from the
92+
* filesystem after closing the browser window because the window can be
93+
* the last, and when the last window closes, the application quits.
94+
* There is no way to clean up the undesired resources.
95+
*
96+
* This array contains disposable instances wrapping synchronous sketch
97+
* delete operations. When IDE2 closes the browser window, it schedules
98+
* the sketch deletion, and the window closes.
99+
*
100+
* When IDE2 schedules a sketch for deletion, it creates a synchronous
101+
* folder deletion as a disposable instance and pushes it into this
102+
* array. After the push, IDE2 starts the sketch deletion in an
103+
* asynchronous way. When the deletion completes, the disposable is
104+
* removed. If the app quits when the asynchronous deletion is still in
105+
* progress, it disposes the elements of this array. Since it is
106+
* synchronous, it is [ensured by Theia](https://github.com/eclipse-theia/theia/blob/678e335644f1b38cb27522cc27a3b8209293cf31/packages/core/src/node/backend-application.ts#L91-L97)
107+
* that IDE2 won't quit before the cleanup is done. It works only in normal
108+
* quit.
109+
*/
110+
private readonly scheduledDeletions: Disposable[] = [];
72111

73112
@inject(ILogger)
74113
@named('sketches-service')
@@ -86,6 +125,14 @@ export class SketchesServiceImpl
86125
@inject(IsTempSketch)
87126
private readonly isTempSketch: IsTempSketch;
88127

128+
onStop(): void {
129+
if (this.scheduledDeletions.length) {
130+
this.logger.info(`>>> Disposing sketches service...`);
131+
new DisposableCollection(...this.scheduledDeletions).dispose();
132+
this.logger.info(`<<< Disposed sketches service.`);
133+
}
134+
}
135+
89136
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
90137
const root = await this.root(uri);
91138
if (!root) {
@@ -629,20 +676,47 @@ export class SketchesServiceImpl
629676
}
630677

631678
async deleteSketch(sketch: Sketch): Promise<void> {
679+
const sketchPath = FileUri.fsPath(sketch.uri);
680+
const disposable = Disposable.create(() =>
681+
this.deleteSketchSync(sketchPath)
682+
);
683+
this.scheduledDeletions.push(disposable);
632684
return new Promise<void>((resolve, reject) => {
633-
const sketchPath = FileUri.fsPath(sketch.uri);
634685
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
635686
if (error) {
636687
this.logger.error(`Failed to delete sketch at ${sketchPath}.`, error);
637688
reject(error);
638689
} else {
639690
this.logger.info(`Successfully deleted sketch at ${sketchPath}.`);
640691
resolve();
692+
const index = this.scheduledDeletions.indexOf(disposable);
693+
if (index >= 0) {
694+
this.scheduledDeletions.splice(index, 1);
695+
} else {
696+
this.logger.warn(
697+
`Could not find the scheduled sketch deletion: ${sketchPath}`
698+
);
699+
}
641700
}
642701
});
643702
});
644703
}
645704

705+
private deleteSketchSync(sketchPath: string): void {
706+
this.logger.info(
707+
`>>> Running sketch deletion ${sketchPath} before app quit...`
708+
);
709+
try {
710+
rmSync(sketchPath, { recursive: true, maxRetries: 5 });
711+
this.logger.info(`<<< Deleted sketch ${sketchPath}.`);
712+
} catch (err) {
713+
if (ErrnoException.isENOENT(err)) {
714+
// ignore. it does not exist
715+
}
716+
throw err;
717+
}
718+
}
719+
646720
// Returns the default.ino from the settings or from default folder.
647721
private async readSettings(): Promise<Record<string, unknown> | undefined> {
648722
const configDirUri = await this.envVariableServer.getConfigDirUri();

0 commit comments

Comments
 (0)
Please sign in to comment.