11import { nls } from '@theia/core/lib/common/nls' ;
2- import { injectable } from '@theia/core/shared/inversify' ;
2+ import { inject , injectable } from '@theia/core/shared/inversify' ;
33import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager' ;
44import { Later } from '../../common/nls' ;
5- import { SketchesError } from '../../common/protocol' ;
5+ import { Sketch , SketchesError } from '../../common/protocol' ;
66import {
77 Command ,
88 CommandRegistry ,
99 SketchContribution ,
1010 URI ,
1111} from './contribution' ;
1212import { 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' ;
1320
1421@injectable ( )
1522export class OpenSketchFiles extends SketchContribution {
23+ @inject ( VSCodeContextKeyService )
24+ private readonly contextKeyService : VSCodeContextKeyService ;
25+
1626 override registerCommands ( registry : CommandRegistry ) : void {
1727 registry . registerCommand ( OpenSketchFiles . Commands . OPEN_SKETCH_FILES , {
1828 execute : ( uri : URI ) => this . openSketchFiles ( uri ) ,
@@ -55,9 +65,25 @@ export class OpenSketchFiles extends SketchContribution {
5565 }
5666 } ) ;
5767 }
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+ }
5873 } 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+
5985 if ( SketchesError . NotFound . is ( err ) ) {
60- this . openFallbackSketch ( ) ;
86+ return this . openFallbackSketch ( ) ;
6187 } else {
6288 console . error ( err ) ;
6389 const message =
@@ -71,6 +97,31 @@ export class OpenSketchFiles extends SketchContribution {
7197 }
7298 }
7399
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+
74125 private async openFallbackSketch ( ) : Promise < void > {
75126 const sketch = await this . sketchService . createNewSketch ( ) ;
76127 this . workspaceService . open ( new URI ( sketch . uri ) , { preserveWindow : true } ) ;
@@ -84,15 +135,69 @@ export class OpenSketchFiles extends SketchContribution {
84135 const widget = this . editorManager . all . find (
85136 ( widget ) => widget . editor . uri . toString ( ) === uri
86137 ) ;
138+ const disposables = new DisposableCollection ( ) ;
87139 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 (
89180 new URI ( uri ) ,
90181 options ?? {
91182 mode : 'reveal' ,
92183 preview : false ,
93184 counter : 0 ,
94185 }
95186 ) ;
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 ;
96201 }
97202 }
98203}
0 commit comments