import { nls } from '@theia/core/lib/common/nls'; import { injectable } from '@theia/core/shared/inversify'; import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager'; import { Later } from '../../common/nls'; import { Sketch, SketchesError } from '../../common/protocol'; import { Command, CommandRegistry, SketchContribution, URI, } from './contribution'; import { SaveAsSketch } from './save-as-sketch'; import { promptMoveSketch } from './open-sketch'; import { ApplicationError } from '@theia/core/lib/common/application-error'; import { Deferred, wait } from '@theia/core/lib/common/promise-util'; import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; @injectable() export class OpenSketchFiles extends SketchContribution { override registerCommands(registry: CommandRegistry): void { registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, { execute: (uri: URI, focusMainSketchFile) => this.openSketchFiles(uri, focusMainSketchFile), }); registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, { execute: ( uri: string, forceOpen?: boolean, options?: EditorOpenerOptions ) => { this.ensureOpened(uri, forceOpen, options); }, }); } private async openSketchFiles( uri: URI, focusMainSketchFile = false ): Promise { try { const sketch = await this.sketchesService.loadSketch(uri.toString()); const { mainFileUri, rootFolderFileUris } = sketch; for (const uri of [mainFileUri, ...rootFolderFileUris]) { await this.ensureOpened(uri); } if (focusMainSketchFile) { await this.ensureOpened(mainFileUri, true, { mode: 'activate', preview: false, counter: 0, }); } if (mainFileUri.endsWith('.pde')) { const message = nls.localize( 'arduino/common/oldFormat', "The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?", sketch.name ); const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); this.messageService.info(message, Later, yes).then((answer) => { if (answer === yes) { this.commandService.executeCommand( SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false, } ); } }); } const { workspaceError } = this.workspaceService; // 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) if (SketchesError.InvalidName.is(workspaceError)) { await this.promptMove(workspaceError); } } catch (err) { // This happens when the user gracefully closed IDE2, all went well // but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2 // the workspace path still exists, but the sketch path is not valid anymore. (#964) if (SketchesError.InvalidName.is(err)) { const movedSketch = await this.promptMove(err); if (!movedSketch) { // If user did not accept the move, or move was not possible, force reload with a fallback. return this.openFallbackSketch(); } } if (SketchesError.NotFound.is(err)) { return this.openFallbackSketch(); } else { console.error(err); const message = err instanceof Error ? err.message : typeof err === 'string' ? err : String(err); this.messageService.error(message); } } } private async promptMove( err: ApplicationError< number, { invalidMainSketchUri: string; } > ): Promise { const { invalidMainSketchUri } = err.data; requestAnimationFrame(() => this.messageService.error(err.message)); await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog const movedSketch = await promptMoveSketch(invalidMainSketchUri, { fileService: this.fileService, sketchesService: this.sketchesService, labelProvider: this.labelProvider, dialogService: this.dialogService, }); if (movedSketch) { this.workspaceService.open(new URI(movedSketch.uri), { preserveWindow: true, }); return movedSketch; } return undefined; } private async openFallbackSketch(): Promise { const sketch = await this.sketchesService.createNewSketch(); this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true }); } private async ensureOpened( uri: string, forceOpen = false, options?: EditorOpenerOptions ): Promise { const widget = this.editorManager.all.find( (widget) => widget.editor.uri.toString() === uri ); if (widget && !forceOpen) { return widget; } const disposables = new DisposableCollection(); const deferred = new Deferred(); // An editor can be in two primary states: // - The editor is not yet opened. The `widget` is `undefined`. With `editorManager#open`, Theia will create an editor and fire an `editorManager#onCreated` event. // - The editor is opened. Can be active, current, or open. // - If the editor has the focus (the cursor blinks in the editor): it's the active editor. // - If the editor does not have the focus (the focus is on a different widget or the context menu is opened in the editor): it's the current editor. // - If the editor is not the top editor in the main area, it's opened. if (!widget) { // If the widget is `undefined`, IDE2 expects one `onCreate` event. Subscribe to the `onCreated` event // and resolve the promise with the editor only when the new editor's visibility changes. disposables.push( this.editorManager.onCreated((editor) => { if (editor.editor.uri.toString() === uri) { if (editor.isAttached && editor.isVisible) { deferred.resolve(editor); } else { disposables.push( editor.onDidChangeVisibility((visible) => { if (visible) { // wait an animation frame. although the visible and attached props are true the editor is not there. // let the browser render the widget setTimeout( () => requestAnimationFrame(() => deferred.resolve(editor)), 0 ); } }) ); } } }) ); } this.editorManager .open( new URI(uri), options ?? { mode: 'reveal', preview: false, counter: 0, } ) .then((editorWidget) => { // If the widget was defined, it was already opened. // The editor is expected to be attached to the shell and visible in the UI. // The deferred promise does not have to wait for the `editorManager#onCreated` event. // It can resolve earlier. if (widget) { deferred.resolve(editorWidget); } }); const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI const result: EditorWidget | undefined | 'timeout' = await Promise.race([ deferred.promise, wait(timeout).then(() => { disposables.dispose(); return 'timeout' as const; }), ]); if (result === 'timeout') { console.warn( `Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}` ); return undefined; } return result; } } export namespace OpenSketchFiles { export namespace Commands { export const OPEN_SKETCH_FILES: Command = { id: 'arduino-open-sketch-files', }; export const ENSURE_OPENED: Command = { id: 'arduino-ensure-opened', }; } }