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,26 @@ export class OpenSketchFiles extends SketchContribution {
5565 }
5666 } ) ;
5767 }
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+ }
5874 } 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+
5986 if ( SketchesError . NotFound . is ( err ) ) {
60- this . openFallbackSketch ( ) ;
87+ return this . openFallbackSketch ( ) ;
6188 } else {
6289 console . error ( err ) ;
6390 const message =
@@ -71,6 +98,31 @@ export class OpenSketchFiles extends SketchContribution {
7198 }
7299 }
73100
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+
74126 private async openFallbackSketch ( ) : Promise < void > {
75127 const sketch = await this . sketchService . createNewSketch ( ) ;
76128 this . workspaceService . open ( new URI ( sketch . uri ) , { preserveWindow : true } ) ;
@@ -84,15 +136,69 @@ export class OpenSketchFiles extends SketchContribution {
84136 const widget = this . editorManager . all . find (
85137 ( widget ) => widget . editor . uri . toString ( ) === uri
86138 ) ;
139+ const disposables = new DisposableCollection ( ) ;
87140 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 (
89181 new URI ( uri ) ,
90182 options ?? {
91183 mode : 'reveal' ,
92184 preview : false ,
93185 counter : 0 ,
94186 }
95187 ) ;
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 ;
96202 }
97203 }
98204}
0 commit comments