diff --git a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts index 8158808b4..547e44229 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts @@ -1,5 +1,4 @@ -import { toUnix } from 'upath'; -import URI from '@theia/core/lib/common/uri'; +import { URI } from '@theia/core/shared/vscode-uri'; import { isWindows } from '@theia/core/lib/common/os'; import { notEmpty } from '@theia/core/lib/common/objects'; import { MaybePromise } from '@theia/core/lib/common/types'; @@ -61,12 +60,8 @@ export class ArduinoWorkspaceRootResolver { // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423 protected hashToUri(hash: string | undefined): string | undefined { if (hash && hash.length > 1 && hash.startsWith('#')) { - const path = hash.slice(1); // Trim the leading `#`. - return new URI( - toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)) - ) - .withScheme('file') - .toString(); + const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators + return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString(); } return undefined; } diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index e625a8288..1b7c58cae 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -8,22 +8,39 @@ import { import { fork } from 'child_process'; import { AddressInfo } from 'net'; import { join } from 'path'; +import * as fs from 'fs-extra'; import { initSplashScreen } from '../splash/splash-screen'; import { MaybePromise } from '@theia/core/lib/common/types'; import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import { ElectronMainApplication as TheiaElectronMainApplication, + ElectronMainExecutionParams, TheiaBrowserWindowOptions, } from '@theia/core/lib/electron-main/electron-main-application'; import { SplashServiceImpl } from '../splash/splash-service-impl'; import { ipcMain } from '@theia/core/shared/electron'; +import { URI } from '@theia/core/shared/vscode-uri'; app.commandLine.appendSwitch('disable-http-cache'); +interface WorkspaceOptions { + file: string + x: number + y: number + width: number + height: number + isMaximized: boolean + isFullScreen: boolean + time: number +} + +const WORKSPACES = 'workspaces'; + @injectable() export class ElectronMainApplication extends TheiaElectronMainApplication { protected _windows: BrowserWindow[] = []; + protected startup = false; @inject(SplashServiceImpl) protected readonly splashService: SplashServiceImpl; @@ -36,6 +53,45 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { return super.start(config); } + protected async launch(params: ElectronMainExecutionParams): Promise { + this.startup = true; + const workspaces: WorkspaceOptions[] | undefined = this.electronStore.get(WORKSPACES); + let useDefault = true; + if (workspaces && workspaces.length > 0) { + for (const workspace of workspaces) { + const file = workspace.file; + if (typeof file === 'string' && await fs.pathExists(file)) { + useDefault = false; + await this.openSketch(workspace); + } + } + } + this.startup = false; + if (useDefault) { + super.launch(params); + } + } + + protected async openSketch(workspace: WorkspaceOptions): Promise { + const options = await this.getLastWindowOptions(); + options.x = workspace.x; + options.y = workspace.y; + options.width = workspace.width; + options.height = workspace.height; + options.isMaximized = workspace.isMaximized; + options.isFullScreen = workspace.isFullScreen; + const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow(options)]); + electronWindow.loadURL(uri.withFragment(encodeURI(workspace.file)).toString(true)); + return electronWindow; + } + + protected avoidOverlap(options: TheiaBrowserWindowOptions): TheiaBrowserWindowOptions { + if (this.startup) { + return options; + } + return super.avoidOverlap(options); + } + protected getTitleBarStyle(): 'native' | 'custom' { return 'native'; } @@ -148,6 +204,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } }); + this.attachClosedWorkspace(electronWindow); this.attachReadyToShow(electronWindow); this.attachSaveWindowState(electronWindow); this.attachGlobalShortcuts(electronWindow); @@ -218,6 +275,44 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } + protected closedWorkspaces: WorkspaceOptions[] = []; + + protected attachClosedWorkspace(window: BrowserWindow): void { + // Since the `before-quit` event is only fired when closing the *last* window + // We need to keep track of recently closed windows/workspaces manually + window.on('close', () => { + const url = window.webContents.getURL(); + const workspace = URI.parse(url).fragment; + if (workspace) { + const workspaceUri = URI.file(workspace); + const bounds = window.getNormalBounds(); + this.closedWorkspaces.push({ + ...bounds, + isMaximized: window.isMaximized(), + isFullScreen: window.isFullScreen(), + file: workspaceUri.fsPath, + time: Date.now() + }) + } + }); + } + + protected onWillQuit(event: Electron.Event): void { + // Only add workspaces which were closed within the last second (1000 milliseconds) + const threshold = Date.now() - 1000; + const visited = new Set(); + const workspaces = this.closedWorkspaces.filter(e => { + if (e.time < threshold || visited.has(e.file)) { + return false; + } + visited.add(e.file); + return true; + }).sort((a, b) => a.file.localeCompare(b.file)); + this.electronStore.set(WORKSPACES, workspaces); + + super.onWillQuit(event); + } + get windows(): BrowserWindow[] { return this._windows.slice(); }