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