Skip to content

Commit c0ca186

Browse files
author
Akos Kitta
committed
fix: Prompt sketch move when opening an invalid outside from IDE2
Closes #964 Signed-off-by: Akos Kitta <[email protected]>
1 parent 99b1094 commit c0ca186

File tree

6 files changed

+280
-54
lines changed

6 files changed

+280
-54
lines changed

Diff for: arduino-ide-extension/src/browser/contributions/contribution.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
1212
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
1313
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
1414
import { MessageService } from '@theia/core/lib/common/message-service';
15-
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
1615
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
1716

1817
import {
@@ -61,6 +60,7 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
6160
import { BoardsDataStore } from '../boards/boards-data-store';
6261
import { NotificationManager } from '../theia/messages/notifications-manager';
6362
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
63+
import { WorkspaceService } from '../theia/workspace/workspace-service';
6464

6565
export {
6666
Command,

Diff for: arduino-ide-extension/src/browser/contributions/open-sketch-files.ts

+94-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { nls } from '@theia/core/lib/common/nls';
2-
import { injectable } from '@theia/core/shared/inversify';
2+
3+
import { inject, injectable } from '@theia/core/shared/inversify';
34
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
45
import { Later } from '../../common/nls';
56
import { SketchesError } from '../../common/protocol';
@@ -10,9 +11,19 @@ import {
1011
URI,
1112
} from './contribution';
1213
import { SaveAsSketch } from './save-as-sketch';
14+
import { promptMoveSketch } from './open-sketch';
15+
import { ApplicationError } from '@theia/core/lib/common/application-error';
16+
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
17+
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
18+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
19+
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
20+
import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService';
1321

1422
@injectable()
1523
export class OpenSketchFiles extends SketchContribution {
24+
@inject(VSCodeContextKeyService)
25+
private readonly contextKeyService: VSCodeContextKeyService;
26+
1627
override registerCommands(registry: CommandRegistry): void {
1728
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
1829
execute: (uri: URI) => this.openSketchFiles(uri),
@@ -55,9 +66,17 @@ export class OpenSketchFiles extends SketchContribution {
5566
}
5667
});
5768
}
69+
const { workspaceError: startupError } = this.workspaceService;
70+
if (SketchesError.InvalidName.is(startupError)) {
71+
return this.promptMove(startupError);
72+
}
5873
} catch (err) {
74+
if (SketchesError.InvalidName.is(err)) {
75+
return this.promptMove(err);
76+
}
77+
5978
if (SketchesError.NotFound.is(err)) {
60-
this.openFallbackSketch();
79+
return this.openFallbackSketch();
6180
} else {
6281
console.error(err);
6382
const message =
@@ -71,6 +90,29 @@ export class OpenSketchFiles extends SketchContribution {
7190
}
7291
}
7392

93+
private async promptMove(
94+
err: ApplicationError<
95+
number,
96+
{
97+
invalidMainSketchUri: string;
98+
}
99+
>
100+
): Promise<void> {
101+
const { invalidMainSketchUri } = err.data;
102+
requestAnimationFrame(() => this.messageService.error(err.message));
103+
await wait(10); // let IDE2 toast the error message.
104+
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
105+
fileService: this.fileService,
106+
sketchService: this.sketchService,
107+
labelProvider: this.labelProvider,
108+
});
109+
if (movedSketch) {
110+
return this.workspaceService.open(new URI(movedSketch.uri), {
111+
preserveWindow: true,
112+
});
113+
}
114+
}
115+
74116
private async openFallbackSketch(): Promise<void> {
75117
const sketch = await this.sketchService.createNewSketch();
76118
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
@@ -84,15 +126,63 @@ export class OpenSketchFiles extends SketchContribution {
84126
const widget = this.editorManager.all.find(
85127
(widget) => widget.editor.uri.toString() === uri
86128
);
129+
const disposables = new DisposableCollection();
87130
if (!widget || forceOpen) {
88-
return this.editorManager.open(
131+
const deferred = new Deferred<EditorWidget>();
132+
disposables.push(
133+
this.editorManager.onCreated((editor) => {
134+
if (editor.editor.uri.toString() === uri) {
135+
if (editor.isVisible) {
136+
disposables.dispose();
137+
deferred.resolve(editor);
138+
} else {
139+
// In Theia, the promise resolves after opening the editor, but the editor is neither attached to the DOM, nor visible.
140+
// This is a hack to first get an event from monaco after the widget update request, then IDE2 waits for the next monaco context key event.
141+
// Here, the monaco context key event is not used, but this is the first event after the editor is visible in the UI.
142+
disposables.push(
143+
(editor.editor as MonacoEditor).onDidResize((dimension) => {
144+
if (dimension) {
145+
const isKeyOwner = (
146+
arg: unknown
147+
): arg is { key: string } => {
148+
if (typeof arg === 'object') {
149+
const object = arg as Record<string, unknown>;
150+
return typeof object['key'] === 'string';
151+
}
152+
return false;
153+
};
154+
disposables.push(
155+
this.contextKeyService.onDidChangeContext((e) => {
156+
// `commentIsEmpty` is the first context key change event received from monaco after the editor is for real visible in the UI.
157+
if (isKeyOwner(e) && e.key === 'commentIsEmpty') {
158+
deferred.resolve(editor);
159+
disposables.dispose();
160+
}
161+
})
162+
);
163+
}
164+
})
165+
);
166+
}
167+
}
168+
})
169+
);
170+
this.editorManager.open(
89171
new URI(uri),
90172
options ?? {
91-
mode: 'reveal',
173+
mode: 'activate',
92174
preview: false,
93175
counter: 0,
94176
}
95177
);
178+
const result = await Promise.race([
179+
deferred.promise,
180+
wait(5_000).then(() => 'timeout'),
181+
]);
182+
if (result === 'timeout') {
183+
console.warn(`Editor did not show up in time. URI: ${uri}`);
184+
}
185+
return result;
96186
}
97187
}
98188
}

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

+73-42
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as remote from '@theia/core/electron-shared/@electron/remote';
2+
import { LabelProvider } from '@theia/core/lib/browser';
23
import { nls } from '@theia/core/lib/common/nls';
34
import { injectable } from '@theia/core/shared/inversify';
4-
import { SketchesError, SketchRef } from '../../common/protocol';
5+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
6+
import {
7+
SketchesError,
8+
SketchesService,
9+
SketchRef,
10+
} from '../../common/protocol';
511
import { ArduinoMenus } from '../menu/arduino-menus';
612
import {
713
Command,
@@ -103,50 +109,23 @@ export class OpenSketch extends SketchContribution {
103109
}
104110
const sketchFilePath = filePaths[0];
105111
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
106-
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
107-
if (sketch) {
108-
return sketch;
112+
try {
113+
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
114+
if (sketch) {
115+
return sketch;
116+
}
117+
} catch (err) {
118+
if (!SketchesError.InvalidName.is(err)) {
119+
throw err;
120+
}
121+
// Let IDE2 offer to correct the invalid sketch name by a move.
109122
}
110123
if (Sketch.isSketchFile(sketchFileUri)) {
111-
const name = new URI(sketchFileUri).path.name;
112-
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
113-
const { response } = await remote.dialog.showMessageBox({
114-
title: nls.localize('arduino/sketch/moving', 'Moving'),
115-
type: 'question',
116-
buttons: [
117-
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
118-
nls.localize('vscode/issueMainService/ok', 'OK'),
119-
],
120-
message: nls.localize(
121-
'arduino/sketch/movingMsg',
122-
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
123-
nameWithExt,
124-
name
125-
),
124+
return promptMoveSketch(sketchFileUri, {
125+
fileService: this.fileService,
126+
sketchService: this.sketchService,
127+
labelProvider: this.labelProvider,
126128
});
127-
if (response === 1) {
128-
// OK
129-
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
130-
const exists = await this.fileService.exists(newSketchUri);
131-
if (exists) {
132-
await remote.dialog.showMessageBox({
133-
type: 'error',
134-
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
135-
message: nls.localize(
136-
'arduino/sketch/cantOpen',
137-
'A folder named "{0}" already exists. Can\'t open sketch.',
138-
name
139-
),
140-
});
141-
return undefined;
142-
}
143-
await this.fileService.createFolder(newSketchUri);
144-
await this.fileService.move(
145-
new URI(sketchFileUri),
146-
new URI(newSketchUri.resolve(nameWithExt).toString())
147-
);
148-
return this.sketchService.getSketchFolder(newSketchUri.toString());
149-
}
150129
}
151130
}
152131
}
@@ -158,3 +137,55 @@ export namespace OpenSketch {
158137
};
159138
}
160139
}
140+
141+
export async function promptMoveSketch(
142+
sketchFileUri: string | URI,
143+
options: {
144+
fileService: FileService;
145+
sketchService: SketchesService;
146+
labelProvider: LabelProvider;
147+
}
148+
): Promise<Sketch | undefined> {
149+
const { fileService, sketchService, labelProvider } = options;
150+
const uri =
151+
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
152+
const name = uri.path.name;
153+
const nameWithExt = labelProvider.getName(uri);
154+
const { response } = await remote.dialog.showMessageBox({
155+
title: nls.localize('arduino/sketch/moving', 'Moving'),
156+
type: 'question',
157+
buttons: [
158+
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
159+
nls.localize('vscode/issueMainService/ok', 'OK'),
160+
],
161+
message: nls.localize(
162+
'arduino/sketch/movingMsg',
163+
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
164+
nameWithExt,
165+
name
166+
),
167+
});
168+
if (response === 1) {
169+
// OK
170+
const newSketchUri = uri.parent.resolve(name);
171+
const exists = await fileService.exists(newSketchUri);
172+
if (exists) {
173+
await remote.dialog.showMessageBox({
174+
type: 'error',
175+
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
176+
message: nls.localize(
177+
'arduino/sketch/cantOpen',
178+
'A folder named "{0}" already exists. Can\'t open sketch.',
179+
name
180+
),
181+
});
182+
return undefined;
183+
}
184+
await fileService.createFolder(newSketchUri);
185+
await fileService.move(
186+
uri,
187+
new URI(newSketchUri.resolve(nameWithExt).toString())
188+
);
189+
return sketchService.getSketchFolder(newSketchUri.toString());
190+
}
191+
}

Diff for: arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts

+28
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import {
1717
SketchesService,
1818
Sketch,
19+
SketchesError,
1920
} from '../../../common/protocol/sketches-service';
2021
import { FileStat } from '@theia/filesystem/lib/common/files';
2122
import {
@@ -38,6 +39,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
3839
private readonly providers: ContributionProvider<StartupTaskProvider>;
3940

4041
private version?: string;
42+
private _workspaceError: Error | undefined;
4143

4244
async onStart(application: FrontendApplication): Promise<void> {
4345
const info = await this.applicationServer.getApplicationInfo();
@@ -51,6 +53,10 @@ export class WorkspaceService extends TheiaWorkspaceService {
5153
this.onCurrentWidgetChange({ newValue, oldValue: null });
5254
}
5355

56+
get workspaceError(): Error | undefined {
57+
return this._workspaceError;
58+
}
59+
5460
protected override async toFileStat(
5561
uri: string | URI | undefined
5662
): Promise<FileStat | undefined> {
@@ -59,6 +65,28 @@ export class WorkspaceService extends TheiaWorkspaceService {
5965
const newSketchUri = await this.sketchService.createNewSketch();
6066
return this.toFileStat(newSketchUri.uri);
6167
}
68+
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
69+
// Here, IDE2 tries to load the sketch from the main sketch file.
70+
// If loading the sketch is OK, IDE2 starts and uses that the sketch folder as the workspace instead of the sketch file.
71+
// If loading fails due to invalid name issues, IDE2 loads a temp sketch and preserves the startup error, and offers the move to the user.
72+
// If loading fails, IDE2 loads a fallback sketch.
73+
if (stat.isFile && stat.resource.path.ext === '.ino') {
74+
try {
75+
const sketch = await this.sketchService.loadSketch(
76+
stat.resource.toString()
77+
);
78+
return this.toFileStat(sketch.uri);
79+
} catch (err) {
80+
if (
81+
SketchesError.NotFound.is(err) ||
82+
SketchesError.InvalidName.is(err)
83+
) {
84+
this._workspaceError = err;
85+
const newSketchUri = await this.sketchService.createNewSketch();
86+
return this.toFileStat(newSketchUri.uri);
87+
}
88+
}
89+
}
6290
return stat;
6391
}
6492

Diff for: arduino-ide-extension/src/common/protocol/sketches-service.ts

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import URI from '@theia/core/lib/common/uri';
44
export namespace SketchesError {
55
export const Codes = {
66
NotFound: 5001,
7+
InvalidName: 5002,
78
};
89
export const NotFound = ApplicationError.declare(
910
Codes.NotFound,
@@ -14,6 +15,15 @@ export namespace SketchesError {
1415
};
1516
}
1617
);
18+
export const InvalidName = ApplicationError.declare(
19+
Codes.InvalidName,
20+
(message: string, invalidMainSketchUri: string) => {
21+
return {
22+
message,
23+
data: { invalidMainSketchUri },
24+
};
25+
}
26+
);
1727
}
1828

1929
export const SketchesServicePath = '/services/sketches-service';

0 commit comments

Comments
 (0)