1
1
import { nls } from '@theia/core/lib/common/nls' ;
2
- import { injectable } from '@theia/core/shared/inversify' ;
2
+ import { inject , injectable } from '@theia/core/shared/inversify' ;
3
3
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager' ;
4
4
import { Later } from '../../common/nls' ;
5
- import { SketchesError } from '../../common/protocol' ;
5
+ import { Sketch , SketchesError } from '../../common/protocol' ;
6
6
import {
7
7
Command ,
8
8
CommandRegistry ,
9
9
SketchContribution ,
10
10
URI ,
11
11
} from './contribution' ;
12
12
import { SaveAsSketch } from './save-as-sketch' ;
13
+ import { promptMoveSketch } from './open-sketch' ;
14
+ import { ApplicationError } from '@theia/core/lib/common/application-error' ;
15
+ import { Deferred , wait } from '@theia/core/lib/common/promise-util' ;
16
+ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget' ;
17
+ import { DisposableCollection } from '@theia/core/lib/common/disposable' ;
18
+ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor' ;
19
+ import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService' ;
13
20
14
21
@injectable ( )
15
22
export class OpenSketchFiles extends SketchContribution {
23
+ @inject ( VSCodeContextKeyService )
24
+ private readonly contextKeyService : VSCodeContextKeyService ;
25
+
16
26
override registerCommands ( registry : CommandRegistry ) : void {
17
27
registry . registerCommand ( OpenSketchFiles . Commands . OPEN_SKETCH_FILES , {
18
28
execute : ( uri : URI ) => this . openSketchFiles ( uri ) ,
@@ -55,9 +65,26 @@ export class OpenSketchFiles extends SketchContribution {
55
65
}
56
66
} ) ;
57
67
}
68
+ const { workspaceError } = this . workspaceService ;
69
+ // This happens when the IDE2 has been started from a terminal with a /path/to/invalid/sketch. (#964)
70
+ // Or user has started the IDE2 from clicking on an `ino` file.
71
+ if ( SketchesError . InvalidName . is ( workspaceError ) ) {
72
+ await this . promptMove ( workspaceError ) ;
73
+ }
58
74
} catch ( err ) {
75
+ // This happens when the user gracefully closed IDE2, all went well
76
+ // but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
77
+ // the workspace path still exists, but the sketch path is not valid anymore. (#964)
78
+ if ( SketchesError . InvalidName . is ( err ) ) {
79
+ const movedSketch = await this . promptMove ( err ) ;
80
+ if ( ! movedSketch ) {
81
+ // If user did not accept the move, or move was not possible, force reload with a fallback.
82
+ return this . openFallbackSketch ( ) ;
83
+ }
84
+ }
85
+
59
86
if ( SketchesError . NotFound . is ( err ) ) {
60
- this . openFallbackSketch ( ) ;
87
+ return this . openFallbackSketch ( ) ;
61
88
} else {
62
89
console . error ( err ) ;
63
90
const message =
@@ -71,6 +98,31 @@ export class OpenSketchFiles extends SketchContribution {
71
98
}
72
99
}
73
100
101
+ private async promptMove (
102
+ err : ApplicationError <
103
+ number ,
104
+ {
105
+ invalidMainSketchUri : string ;
106
+ }
107
+ >
108
+ ) : Promise < Sketch | undefined > {
109
+ const { invalidMainSketchUri } = err . data ;
110
+ requestAnimationFrame ( ( ) => this . messageService . error ( err . message ) ) ;
111
+ await wait ( 10 ) ; // let IDE2 toast the error message.
112
+ const movedSketch = await promptMoveSketch ( invalidMainSketchUri , {
113
+ fileService : this . fileService ,
114
+ sketchService : this . sketchService ,
115
+ labelProvider : this . labelProvider ,
116
+ } ) ;
117
+ if ( movedSketch ) {
118
+ this . workspaceService . open ( new URI ( movedSketch . uri ) , {
119
+ preserveWindow : true ,
120
+ } ) ;
121
+ return movedSketch ;
122
+ }
123
+ return undefined ;
124
+ }
125
+
74
126
private async openFallbackSketch ( ) : Promise < void > {
75
127
const sketch = await this . sketchService . createNewSketch ( ) ;
76
128
this . workspaceService . open ( new URI ( sketch . uri ) , { preserveWindow : true } ) ;
@@ -84,15 +136,69 @@ export class OpenSketchFiles extends SketchContribution {
84
136
const widget = this . editorManager . all . find (
85
137
( widget ) => widget . editor . uri . toString ( ) === uri
86
138
) ;
139
+ const disposables = new DisposableCollection ( ) ;
87
140
if ( ! widget || forceOpen ) {
88
- return this . editorManager . open (
141
+ const deferred = new Deferred < EditorWidget > ( ) ;
142
+ disposables . push (
143
+ this . editorManager . onCreated ( ( editor ) => {
144
+ if ( editor . editor . uri . toString ( ) === uri ) {
145
+ if ( editor . isVisible ) {
146
+ disposables . dispose ( ) ;
147
+ deferred . resolve ( editor ) ;
148
+ } else {
149
+ // In Theia, the promise resolves after opening the editor, but the editor is neither attached to the DOM, nor visible.
150
+ // 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.
151
+ // Here, the monaco context key event is not used, but this is the first event after the editor is visible in the UI.
152
+ disposables . push (
153
+ ( editor . editor as MonacoEditor ) . onDidResize ( ( dimension ) => {
154
+ if ( dimension ) {
155
+ const isKeyOwner = (
156
+ arg : unknown
157
+ ) : arg is { key : string } => {
158
+ if ( typeof arg === 'object' ) {
159
+ const object = arg as Record < string , unknown > ;
160
+ return typeof object [ 'key' ] === 'string' ;
161
+ }
162
+ return false ;
163
+ } ;
164
+ disposables . push (
165
+ this . contextKeyService . onDidChangeContext ( ( e ) => {
166
+ // `commentIsEmpty` is the first context key change event received from monaco after the editor is for real visible in the UI.
167
+ if ( isKeyOwner ( e ) && e . key === 'commentIsEmpty' ) {
168
+ deferred . resolve ( editor ) ;
169
+ disposables . dispose ( ) ;
170
+ }
171
+ } )
172
+ ) ;
173
+ }
174
+ } )
175
+ ) ;
176
+ }
177
+ }
178
+ } )
179
+ ) ;
180
+ this . editorManager . open (
89
181
new URI ( uri ) ,
90
182
options ?? {
91
183
mode : 'reveal' ,
92
184
preview : false ,
93
185
counter : 0 ,
94
186
}
95
187
) ;
188
+ const timeout = 5_000 ; // number of ms IDE2 waits for the editor to show up in the UI
189
+ const result = await Promise . race ( [
190
+ deferred . promise ,
191
+ wait ( timeout ) . then ( ( ) => {
192
+ disposables . dispose ( ) ;
193
+ return 'timeout' ;
194
+ } ) ,
195
+ ] ) ;
196
+ if ( result === 'timeout' ) {
197
+ console . warn (
198
+ `Timeout after ${ timeout } millis. The editor has not shown up in time. URI: ${ uri } `
199
+ ) ;
200
+ }
201
+ return result ;
96
202
}
97
203
}
98
204
}
0 commit comments