diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index fae9a6441c6..cfa48b71053 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -384,8 +384,7 @@ export class CodeApplication extends Disposable { } // Setup Auth Handler - const authHandler = appInstantiationService.createInstance(ProxyAuthHandler); - this._register(authHandler); + this._register(new ProxyAuthHandler()); // Open Windows const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)); diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index 248a47c3033..45229b3bdd8 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { BrowserWindow, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron'; @@ -22,18 +21,21 @@ type Credentials = { password: string; }; -export class ProxyAuthHandler { +export class ProxyAuthHandler extends Disposable { _serviceBrand: undefined; private retryCount = 0; - private disposables: IDisposable[] = []; - constructor( - @IWindowsMainService private readonly windowsMainService: IWindowsMainService - ) { + constructor() { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { const onLogin = Event.fromNodeEventEmitter(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb })); - onLogin(this.onLogin, this, this.disposables); + this._register(onLogin(this.onLogin, this)); } private onLogin({ event, authInfo, cb }: LoginEvent): void { @@ -61,10 +63,9 @@ export class ProxyAuthHandler { } }; - const focusedWindow = this.windowsMainService.getFocusedWindow(); - + const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) { - opts.parent = focusedWindow.win; + opts.parent = focusedWindow; opts.modal = true; } @@ -89,8 +90,4 @@ export class ProxyAuthHandler { win.close(); }); } - - dispose(): void { - this.disposables = dispose(this.disposables); - } } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index f48a831a40f..c41ef9be0fb 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -20,20 +20,20 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, IPathsToWaitFor, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/code/node/windowsFinder'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/platform/windows/node/window'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IEnterWorkspaceResult, IRecent } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { dirExists } from 'vs/base/node/pfs'; -import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources'; +import { getComparisonKey, isEqual, normalizePath, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage'; import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; @@ -167,8 +167,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService { private readonly windowsState: IWindowsState; private lastClosedWindowState?: IWindowState; - private readonly workspacesManager: WorkspacesManager; - private readonly _onWindowReady = this._register(new Emitter()); readonly onWindowReady: CommonEvent = this._onWindowReady.event; @@ -203,8 +201,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService { this.windowsState.openedWindows = []; } - this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this, dialogMainService); - this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex()); } @@ -259,6 +255,11 @@ export class WindowsManager extends Disposable implements IWindowsMainService { this.lastClosedWindowState = undefined; } }); + + // Signal a window is ready after having entered a workspace + this._register(this.workspacesMainService.onWorkspaceEntered(event => { + this._onWindowReady.fire(event.window); + })); } // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: @@ -857,7 +858,8 @@ export class WindowsManager extends Disposable implements IWindowsMainService { path.label = pathToOpen.label; pathsToOpen.push(path); } else { - const uri = resourceFromURIToOpen(pathToOpen); + const uri = this.resourceFromURIToOpen(pathToOpen); + // Warn about the invalid URI or path let message, detail; if (uri.scheme === Schemas.file) { @@ -1020,7 +1022,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService { return undefined; } - let uri = resourceFromURIToOpen(toOpen); + let uri = this.resourceFromURIToOpen(toOpen); if (uri.scheme === Schemas.file) { return this.parsePath(uri.fsPath, options, isFileToOpen(toOpen)); } @@ -1069,6 +1071,18 @@ export class WindowsManager extends Disposable implements IWindowsMainService { }; } + private resourceFromURIToOpen(openable: IWindowOpenable): URI { + if (isWorkspaceToOpen(openable)) { + return openable.workspaceUri; + } + + if (isFolderToOpen(openable)) { + return openable.folderUri; + } + + return openable.fileUri; + } + private parsePath(anyPath: string, options: IPathParseOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { if (!anyPath) { return undefined; @@ -1183,7 +1197,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService { return { openFolderInNewWindow: !!openFolderInNewWindow, openFilesInNewWindow }; } - openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): void { + openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[] { // Reload an existing extension development host window on the same path // We currently do not allow more than one extension development window @@ -1193,7 +1207,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService { this.reload(existingWindow, openConfig.cli); existingWindow.focus(); // make sure it gets focus and is restored - return; + return [existingWindow]; } let folderUris = openConfig.cli['folder-uri'] || []; let fileUris = openConfig.cli['file-uri'] || []; @@ -1284,7 +1298,8 @@ export class WindowsManager extends Disposable implements IWindowsMainService { noRecentEntry: true, waitMarkerFileURI: openConfig.waitMarkerFileURI }; - this.open(openArgs); + + return this.open(openArgs); } private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { @@ -1578,23 +1593,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService { }); } - async enterWorkspace(win: ICodeWindow, path: URI): Promise { - const result = await this.workspacesManager.enterWorkspace(win, path); - - return result ? this.doEnterWorkspace(win, result) : undefined; - } - - private doEnterWorkspace(win: ICodeWindow, result: IEnterWorkspaceResult): IEnterWorkspaceResult { - - // Mark as recently opened - this.workspacesHistoryMainService.addRecentlyOpened([{ workspace: result.workspace }]); - - // Trigger Eevent to indicate load of workspace into window - this._onWindowReady.fire(win); - - return result; - } - focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow { const lastActive = this.getLastActiveWindow(); if (lastActive) { @@ -1611,7 +1609,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService { return getLastActiveWindow(WindowsManager.WINDOWS); } - getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { + private getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { return getLastActiveWindow(WindowsManager.WINDOWS.filter(window => window.remoteAuthority === remoteAuthority)); } @@ -1628,10 +1626,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService { return this.open({ context, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); } - openNewTabbedWindow(context: OpenContext): ICodeWindow[] { - return this.open({ context, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true }); - } - waitForWindowCloseOrLoad(windowId: number): Promise { return new Promise(resolve => { function handler(id: number) { @@ -1666,7 +1660,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService { } } - getFocusedWindow(): ICodeWindow | undefined { + private getFocusedWindow(): ICodeWindow | undefined { const win = BrowserWindow.getFocusedWindow(); if (win) { return this.getWindowById(win.id); @@ -1860,88 +1854,3 @@ export class WindowsManager extends Disposable implements IWindowsMainService { } } } - -class WorkspacesManager { - - constructor( - private readonly workspacesMainService: IWorkspacesMainService, - private readonly backupMainService: IBackupMainService, - private readonly windowsMainService: IWindowsMainService, - private readonly dialogMainService: IDialogMainService - ) { } - - async enterWorkspace(window: ICodeWindow, path: URI): Promise { - if (!window || !window.win || !window.isReady) { - return null; // return early if the window is not ready or disposed - } - - const isValid = await this.isValidTargetWorkspacePath(window, path); - if (!isValid) { - return null; // return early if the workspace is not valid - } - - return this.doOpenWorkspace(window, getWorkspaceIdentifier(path)); - } - - private async isValidTargetWorkspacePath(window: ICodeWindow, path?: URI): Promise { - if (!path) { - return true; - } - - if (window.openedWorkspace && isEqual(window.openedWorkspace.configPath, path)) { - return false; // window is already opened on a workspace with that path - } - - // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(this.windowsMainService.getWindows(), getWorkspaceIdentifier(path))) { - const options: MessageBoxOptions = { - title: product.nameLong, - type: 'info', - buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", resourcesBasename(path)), - detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), - noLink: true - }; - - await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - - return false; - } - - return true; // OK - } - - private doOpenWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult { - window.focus(); - - // Register window for backups and migrate current backups over - let backupPath: string | undefined; - if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); - } - - // if the window was opened on an untitled workspace, delete it. - if (window.openedWorkspace && this.workspacesMainService.isUntitledWorkspace(window.openedWorkspace)) { - this.workspacesMainService.deleteUntitledWorkspaceSync(window.openedWorkspace); - } - - // Update window configuration properly based on transition to workspace - window.config.folderUri = undefined; - window.config.workspace = workspace; - window.config.backupPath = backupPath; - - return { workspace, backupPath }; - } -} - -function resourceFromURIToOpen(openable: IWindowOpenable): URI { - if (isWorkspaceToOpen(openable)) { - return openable.workspaceUri; - } - - if (isFolderToOpen(openable)) { - return openable.folderUri; - } - - return openable.fileUri; -} diff --git a/src/vs/code/electron-main/windowsStateStorage.ts b/src/vs/code/electron-main/windowsStateStorage.ts index 16eac66cc20..176f67d8749 100644 --- a/src/vs/code/electron-main/windowsStateStorage.ts +++ b/src/vs/code/electron-main/windowsStateStorage.ts @@ -35,12 +35,15 @@ export function restoreWindowsState(data: WindowsStateStorageData | undefined): if (windowsState.lastActiveWindow) { result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); } + if (windowsState.lastPluginDevelopmentHostWindow) { result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); } + if (Array.isArray(windowsState.openedWindows)) { result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); } + return result; } @@ -49,9 +52,11 @@ function restoreWindowState(windowState: ISerializedWindowState): IWindowState { if (windowState.backupPath) { result.backupPath = windowState.backupPath; } + if (windowState.remoteAuthority) { result.remoteAuthority = windowState.remoteAuthority; } + if (windowState.folder) { result.folderUri = URI.parse(windowState.folder); } else if (windowState.folderUri) { @@ -59,11 +64,13 @@ function restoreWindowState(windowState: ISerializedWindowState): IWindowState { } else if (windowState.folderPath) { result.folderUri = URI.file(windowState.folderPath); } + if (windowState.workspaceIdentifier) { result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; } else if (windowState.workspace) { result.workspace = { id: windowState.workspace.id, configPath: URI.file(windowState.workspace.configPath) }; } + return result; } @@ -83,4 +90,4 @@ function serializeWindowState(windowState: IWindowState): ISerializedWindowState remoteAuthority: windowState.remoteAuthority, uiState: windowState.uiState }; -} \ No newline at end of file +} diff --git a/src/vs/code/node/windowsFinder.ts b/src/vs/code/node/windowsFinder.ts deleted file mode 100644 index 03e836c4827..00000000000 --- a/src/vs/code/node/windowsFinder.ts +++ /dev/null @@ -1,131 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as platform from 'vs/base/common/platform'; -import * as extpath from 'vs/base/common/extpath'; -import { OpenContext } from 'vs/platform/windows/common/windows'; -import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { URI } from 'vs/base/common/uri'; -import { isEqual, isEqualOrParent } from 'vs/base/common/resources'; - -export interface ISimpleWindow { - openedWorkspace?: IWorkspaceIdentifier; - openedFolderUri?: URI; - - extensionDevelopmentPath?: string[]; - lastFocusTime: number; -} - -export interface IBestWindowOrFolderOptions { - windows: W[]; - newWindow: boolean; - context: OpenContext; - fileUri?: URI; - userHome?: string; - codeSettingsFolder?: string; - localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null; -} - -export function findBestWindowOrFolderForFile({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions): W | undefined { - if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { - const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver); - if (windowOnFilePath) { - return windowOnFilePath; - } - } - return !newWindow ? getLastActiveWindow(windows) : undefined; -} - -function findWindowOnFilePath(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null { - - // First check for windows with workspaces that have a parent folder of the provided path opened - for (const window of windows) { - const workspace = window.openedWorkspace; - if (workspace) { - const resolvedWorkspace = localWorkspaceResolver(workspace); - if (resolvedWorkspace) { - // workspace could be resolved: It's in the local file system - if (resolvedWorkspace.folders.some(folder => isEqualOrParent(fileUri, folder.uri))) { - return window; - } - } else { - // use the config path instead - if (isEqualOrParent(fileUri, workspace.configPath)) { - return window; - } - } - } - } - - // Then go with single folder windows that are parent of the provided file path - const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && isEqualOrParent(fileUri, window.openedFolderUri)); - if (singleFolderWindowsOnFilePath.length) { - return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri!.path.length - b.openedFolderUri!.path.length))[0]; - } - - return null; -} - -export function getLastActiveWindow(windows: W[]): W | undefined { - const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); - - return windows.filter(window => window.lastFocusTime === lastFocusedDate)[0]; -} - -export function findWindowOnWorkspace(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W | null { - if (isSingleFolderWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on folder - if (isSingleFolderWorkspaceIdentifier(workspace)) { - if (window.openedFolderUri && isEqual(window.openedFolderUri, workspace)) { - return window; - } - } - } - } else if (isWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on workspace - if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) { - return window; - } - } - } - return null; -} - -export function findWindowOnExtensionDevelopmentPath(windows: W[], extensionDevelopmentPaths: string[]): W | null { - - const matches = (uriString: string): boolean => { - return extensionDevelopmentPaths.some(p => extpath.isEqual(p, uriString, !platform.isLinux /* ignorecase */)); - }; - - for (const window of windows) { - // match on extension development path. The path can be one or more paths or uri strings, using paths.isEqual is not 100% correct but good enough - const currPaths = window.extensionDevelopmentPath; - if (currPaths && currPaths.some(p => matches(p))) { - return window; - } - } - - return null; -} - -export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI | undefined): W | null { - if (!uri) { - return null; - } - for (const window of windows) { - // check for workspace config path - if (window.openedWorkspace && isEqual(window.openedWorkspace.configPath, uri)) { - return window; - } - - // check for folder path - if (window.openedFolderUri && isEqual(window.openedFolderUri, uri)) { - return window; - } - } - return null; -} diff --git a/src/vs/code/test/node/fixtures/no_vscode_folder/file.txt b/src/vs/code/test/node/fixtures/no_vscode_folder/file.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/vs/code/test/node/fixtures/vscode_folder/_vscode/settings.json b/src/vs/code/test/node/fixtures/vscode_folder/_vscode/settings.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/vs/code/test/node/fixtures/vscode_folder/file.txt b/src/vs/code/test/node/fixtures/vscode_folder/file.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/vs/code/test/node/fixtures/vscode_folder/nested_vscode_folder/_vscode/settings.json b/src/vs/code/test/node/fixtures/vscode_folder/nested_vscode_folder/_vscode/settings.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/vs/code/test/node/fixtures/vscode_home_folder/_vscode/settings.json b/src/vs/code/test/node/fixtures/vscode_home_folder/_vscode/settings.json deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/src/vs/code/test/node/fixtures/vscode_home_folder/_vscode/settings.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/src/vs/code/test/node/fixtures/vscode_home_folder/file.txt b/src/vs/code/test/node/fixtures/vscode_home_folder/file.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 6069c84a5ca..89f5763e30f 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -63,7 +63,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions { - const activeWindow = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + const activeWindow = BrowserWindow.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); if (activeWindow) { return activeWindow.id; } @@ -252,7 +252,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions { - this.windowsMainService.openNewTabbedWindow(OpenContext.API); + this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true }); } async showPreviousWindowTab(): Promise { diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index c127f8da240..dc60e0a59f1 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -141,6 +141,7 @@ export class LaunchMainService implements ILaunchMainService { } } + // Open new Window if (openNewWindow) { usedWindows = this.windowsMainService.open({ context, @@ -150,8 +151,18 @@ export class LaunchMainService implements ILaunchMainService { forceEmpty: true, waitMarkerFileURI }); - } else { - usedWindows = [this.windowsMainService.focusLastActive(args, context)]; + } + + // Focus existing window or open if none opened + else { + const lastActive = this.windowsMainService.getLastActiveWindow(); + if (lastActive) { + lastActive.focus(); + + usedWindows = [lastActive]; + } else { + usedWindows = this.windowsMainService.open({ context, cli: args, forceEmpty: true }); + } } } diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index b2e7cb2888f..1dc526cd658 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -364,8 +364,8 @@ export class Menubar { const quit = new MenuItem(this.likeAction('workbench.action.quit', { label: nls.localize('miQuit', "Quit {0}", product.nameLong), click: () => { if ( - this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open - !!this.windowsMainService.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/Microsoft/vscode/issues/39191) + this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open + !!BrowserWindow.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/Microsoft/vscode/issues/39191) this.windowsMainService.getLastActiveWindow()!.isMinimized() // allow to quit when window has no focus but is minimized (https://github.com/Microsoft/vscode/issues/63000) ) { this.windowsMainService.quit(); @@ -548,7 +548,7 @@ export class Menubar { label: this.mnemonicLabel(nls.localize('miCheckForUpdates', "Check for &&Updates...")), click: () => setTimeout(() => { this.reportMenuActionTelemetry('CheckForUpdate'); - const focusedWindow = this.windowsMainService.getFocusedWindow(); + const focusedWindow = BrowserWindow.getFocusedWindow(); const context = focusedWindow ? { windowId: focusedWindow.id } : null; this.updateService.checkForUpdates(context); }, 0) @@ -688,15 +688,16 @@ export class Menubar { private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void { return () => { + // No Active Window - const activeWindow = this.windowsMainService.getFocusedWindow(); + const activeWindow = BrowserWindow.getFocusedWindow(); if (!activeWindow) { return contextSpecificHandlers.inNoWindow(); } // DevTools focused - if (activeWindow.win.webContents.isDevToolsFocused()) { - return contextSpecificHandlers.inDevTools(activeWindow.win.webContents.devToolsWebContents); + if (activeWindow.webContents.isDevToolsFocused()) { + return contextSpecificHandlers.inDevTools(activeWindow.webContents.devToolsWebContents); } // Finally execute command in Window @@ -710,14 +711,15 @@ export class Menubar { // https://github.com/Microsoft/vscode/issues/11928 // Still allow to run when the last active window is minimized though for // https://github.com/Microsoft/vscode/issues/63000 - let activeWindow = this.windowsMainService.getFocusedWindow(); - if (!activeWindow) { + let activeBrowserWindow = BrowserWindow.getFocusedWindow(); + if (!activeBrowserWindow) { const lastActiveWindow = this.windowsMainService.getLastActiveWindow(); if (lastActiveWindow && lastActiveWindow.isMinimized()) { - activeWindow = lastActiveWindow; + activeBrowserWindow = lastActiveWindow.win; } } + const activeWindow = activeBrowserWindow ? this.windowsMainService.getWindowById(activeBrowserWindow.id) : undefined; if (activeWindow) { this.logService.trace('menubar#runActionInRenderer', invocation); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 02b9440c32e..5dd270d8c25 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -9,7 +9,7 @@ import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; import { Rectangle, BrowserWindow } from 'electron'; @@ -89,32 +89,31 @@ export interface IWindowsMainService { _serviceBrand: undefined; - // events readonly onWindowReady: Event; readonly onWindowsCountChanged: Event; readonly onWindowClose: Event; - // methods - reload(win: ICodeWindow, cli?: ParsedArgs): void; - enterWorkspace(win: ICodeWindow, path: URI): Promise; - closeWorkspace(win: ICodeWindow): void; open(openConfig: IOpenConfiguration): ICodeWindow[]; - openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): void; + openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[]; + openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; + pickFileFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; pickFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; pickFileAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; pickWorkspaceAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; - focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow; - getLastActiveWindow(): ICodeWindow | undefined; - waitForWindowCloseOrLoad(windowId: number): Promise; - openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[]; - openNewTabbedWindow(context: OpenContext): ICodeWindow[]; + sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void; - getFocusedWindow(): ICodeWindow | undefined; + + getLastActiveWindow(): ICodeWindow | undefined; + getWindowById(windowId: number): ICodeWindow | undefined; getWindows(): ICodeWindow[]; getWindowCount(): number; + + waitForWindowCloseOrLoad(windowId: number): Promise; + reload(win: ICodeWindow, cli?: ParsedArgs): void; + closeWorkspace(win: ICodeWindow): void; quit(): void; } diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts index b2120a4a043..27f3860a58a 100644 --- a/src/vs/platform/windows/node/window.ts +++ b/src/vs/platform/windows/node/window.ts @@ -3,8 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IOpenWindowOptions } from 'vs/platform/windows/common/windows'; +import { OpenContext, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; +import * as platform from 'vs/base/common/platform'; +import * as extpath from 'vs/base/common/extpath'; +import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { isEqual, isEqualOrParent } from 'vs/base/common/resources'; export interface INativeOpenWindowOptions extends IOpenWindowOptions { diffMode?: boolean; @@ -12,3 +16,123 @@ export interface INativeOpenWindowOptions extends IOpenWindowOptions { gotoLineMode?: boolean; waitMarkerFileURI?: URI; } + +export interface IWindowContext { + openedWorkspace?: IWorkspaceIdentifier; + openedFolderUri?: URI; + + extensionDevelopmentPath?: string[]; + lastFocusTime: number; +} + +export interface IBestWindowOrFolderOptions { + windows: W[]; + newWindow: boolean; + context: OpenContext; + fileUri?: URI; + userHome?: string; + codeSettingsFolder?: string; + localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null; +} + +export function findBestWindowOrFolderForFile({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions): W | undefined { + if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { + const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver); + if (windowOnFilePath) { + return windowOnFilePath; + } + } + return !newWindow ? getLastActiveWindow(windows) : undefined; +} + +function findWindowOnFilePath(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null { + + // First check for windows with workspaces that have a parent folder of the provided path opened + for (const window of windows) { + const workspace = window.openedWorkspace; + if (workspace) { + const resolvedWorkspace = localWorkspaceResolver(workspace); + if (resolvedWorkspace) { + // workspace could be resolved: It's in the local file system + if (resolvedWorkspace.folders.some(folder => isEqualOrParent(fileUri, folder.uri))) { + return window; + } + } else { + // use the config path instead + if (isEqualOrParent(fileUri, workspace.configPath)) { + return window; + } + } + } + } + + // Then go with single folder windows that are parent of the provided file path + const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && isEqualOrParent(fileUri, window.openedFolderUri)); + if (singleFolderWindowsOnFilePath.length) { + return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri!.path.length - b.openedFolderUri!.path.length))[0]; + } + + return null; +} + +export function getLastActiveWindow(windows: W[]): W | undefined { + const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); + + return windows.filter(window => window.lastFocusTime === lastFocusedDate)[0]; +} + +export function findWindowOnWorkspace(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W | null { + if (isSingleFolderWorkspaceIdentifier(workspace)) { + for (const window of windows) { + // match on folder + if (isSingleFolderWorkspaceIdentifier(workspace)) { + if (window.openedFolderUri && isEqual(window.openedFolderUri, workspace)) { + return window; + } + } + } + } else if (isWorkspaceIdentifier(workspace)) { + for (const window of windows) { + // match on workspace + if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) { + return window; + } + } + } + return null; +} + +export function findWindowOnExtensionDevelopmentPath(windows: W[], extensionDevelopmentPaths: string[]): W | null { + + const matches = (uriString: string): boolean => { + return extensionDevelopmentPaths.some(p => extpath.isEqual(p, uriString, !platform.isLinux /* ignorecase */)); + }; + + for (const window of windows) { + // match on extension development path. The path can be one or more paths or uri strings, using paths.isEqual is not 100% correct but good enough + const currPaths = window.extensionDevelopmentPath; + if (currPaths && currPaths.some(p => matches(p))) { + return window; + } + } + + return null; +} + +export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI | undefined): W | null { + if (!uri) { + return null; + } + for (const window of windows) { + // check for workspace config path + if (window.openedWorkspace && isEqual(window.openedWorkspace.configPath, uri)) { + return window; + } + + // check for folder path + if (window.openedFolderUri && isEqual(window.openedFolderUri, uri)) { + return window; + } + } + return null; +} diff --git a/src/vs/code/test/node/windowsFinder.test.ts b/src/vs/platform/windows/test/node/window.test.ts similarity index 80% rename from src/vs/code/test/node/windowsFinder.test.ts rename to src/vs/platform/windows/test/node/window.test.ts index 7327dd74013..efda0b1afbc 100644 --- a/src/vs/code/test/node/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/node/window.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { findBestWindowOrFolderForFile, ISimpleWindow, IBestWindowOrFolderOptions } from 'vs/code/node/windowsFinder'; +import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile } from 'vs/platform/windows/node/window'; import { OpenContext } from 'vs/platform/windows/common/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; @@ -20,7 +20,7 @@ const testWorkspace: IWorkspaceIdentifier = { const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath); -function options(custom?: Partial>): IBestWindowOrFolderOptions { +function options(custom?: Partial>): IBestWindowOrFolderOptions { return { windows: [], newWindow: false, @@ -31,10 +31,10 @@ function options(custom?: Partial>): I }; } -const vscodeFolderWindow: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; -const lastActiveWindow: ISimpleWindow = { lastFocusTime: 3, openedFolderUri: undefined }; -const noVscodeFolderWindow: ISimpleWindow = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; -const windows: ISimpleWindow[] = [ +const vscodeFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; +const lastActiveWindow: IWindowContext = { lastFocusTime: 3, openedFolderUri: undefined }; +const noVscodeFolderWindow: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; +const windows: IWindowContext[] = [ vscodeFolderWindow, lastActiveWindow, noVscodeFolderWindow, @@ -102,7 +102,7 @@ suite('WindowsFinder', () => { windows, fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) })), vscodeFolderWindow); - const window: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; + const window: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) @@ -110,8 +110,8 @@ suite('WindowsFinder', () => { }); test('More specific existing window wins', () => { - const window: ISimpleWindow = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; - const nestedFolderWindow: ISimpleWindow = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; + const window: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; + const nestedFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window, nestedFolderWindow], fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) @@ -119,7 +119,7 @@ suite('WindowsFinder', () => { }); test('Workspace folder wins', () => { - const window: ISimpleWindow = { lastFocusTime: 1, openedWorkspace: testWorkspace }; + const window: IWindowContext = { lastFocusTime: 1, openedWorkspace: testWorkspace }; assert.equal(findBestWindowOrFolderForFile(options({ windows: [window], fileUri: URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')) diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index f3351a88115..974a8c87527 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -31,7 +31,7 @@ export interface IWorkspacesService { _serviceBrand: undefined; // Management - enterWorkspace(path: URI): Promise; + enterWorkspace(path: URI): Promise; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; getWorkspaceIdentifier(workspacePath: URI): Promise; diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index a3684c14d53..b44ba7b754e 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -23,6 +23,7 @@ import { getSimpleWorkspaceLabel } from 'vs/platform/label/common/label'; import { exists } from 'vs/base/node/pfs'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Disposable } from 'vs/base/common/lifecycle'; export const IWorkspacesHistoryMainService = createDecorator('workspacesHistoryMainService'); @@ -40,7 +41,7 @@ export interface IWorkspacesHistoryMainService { updateWindowsJumpList(): void; } -export class WorkspacesHistoryMainService implements IWorkspacesHistoryMainService { +export class WorkspacesHistoryMainService extends Disposable implements IWorkspacesHistoryMainService { private static readonly MAX_TOTAL_RECENT_ENTRIES = 100; @@ -60,18 +61,27 @@ export class WorkspacesHistoryMainService implements IWorkspacesHistoryMainServi private readonly _onRecentlyOpenedChange = new Emitter(); readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; - private macOSRecentDocumentsUpdater: ThrottledDelayer; + private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( @IStateService private readonly stateService: IStateService, @ILogService private readonly logService: ILogService, @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ILifecycleMainService lifecycleMainService: ILifecycleMainService + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { - this.macOSRecentDocumentsUpdater = new ThrottledDelayer(800); + super(); - lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList()); + this.registerListeners(); + } + + private registerListeners(): void { + + // Install window jump list after opening window + this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList()); + + // Add to history when entering workspace + this._register(this.workspacesMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); } private handleWindowsJumpList(): void { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index e5714c52e65..eb861920576 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { join, dirname } from 'vs/base/common/path'; import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; @@ -17,31 +17,43 @@ import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; -import { originalFSPath, isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import { originalFSPath, isEqualOrParent, joinPath, isEqual, basename } from 'vs/base/common/resources'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { localize } from 'vs/nls'; +import product from 'vs/platform/product/common/product'; +import { MessageBoxOptions, BrowserWindow } from 'electron'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { findWindowOnWorkspace } from 'vs/platform/windows/node/window'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); +export interface IWorkspaceEnteredEvent { + window: ICodeWindow; + workspace: IWorkspaceIdentifier; +} + export interface IWorkspacesMainService { _serviceBrand: undefined; readonly onUntitledWorkspaceDeleted: Event; + readonly onWorkspaceEntered: Event; + enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; + + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; - - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - + deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; - - deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; - + resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; getWorkspaceIdentifier(workspacePath: URI): Promise; } @@ -59,9 +71,14 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; + private readonly _onWorkspaceEntered = this._register(new Emitter()); + readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; + constructor( @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IBackupMainService private readonly backupMainService: IBackupMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService ) { super(); @@ -236,6 +253,74 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain } return untitledWorkspaces; } + + async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { + if (!window || !window.win || !window.isReady) { + return null; // return early if the window is not ready or disposed + } + + const isValid = await this.isValidTargetWorkspacePath(window, windows, path); + if (!isValid) { + return null; // return early if the workspace is not valid + } + + const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); + + // Emit as event + this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); + + return result; + } + + private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise { + if (!path) { + return true; + } + + if (window.openedWorkspace && isEqual(window.openedWorkspace.configPath, path)) { + return false; // window is already opened on a workspace with that path + } + + // Prevent overwriting a workspace that is currently opened in another window + if (findWindowOnWorkspace(windows, getWorkspaceIdentifier(path))) { + const options: MessageBoxOptions = { + title: product.nameLong, + type: 'info', + buttons: [localize('ok', "OK")], + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + + await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + + return false; + } + + return true; // OK + } + + private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult { + window.focus(); + + // Register window for backups and migrate current backups over + let backupPath: string | undefined; + if (!window.config.extensionDevelopmentPath) { + backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); + } + + // if the window was opened on an untitled workspace, delete it. + if (window.openedWorkspace && this.isUntitledWorkspace(window.openedWorkspace)) { + this.deleteUntitledWorkspaceSync(window.openedWorkspace); + } + + // Update window configuration properly based on transition to workspace + window.config.folderUri = undefined; + window.config.workspace = workspace; + window.config.backupPath = backupPath; + + return { workspace, backupPath }; + } } function getWorkspaceId(configPath: URI): string { diff --git a/src/vs/platform/workspaces/electron-main/workspacesService.ts b/src/vs/platform/workspaces/electron-main/workspacesService.ts index 86ef92cf2b6..41f4cb982c2 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesService.ts @@ -23,13 +23,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions { + async enterWorkspace(windowId: number, path: URI): Promise { const window = this.windowsMainService.getWindowById(windowId); if (window) { - return this.windowsMainService.enterWorkspace(window, path); + return this.workspacesMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); } - return undefined; + return null; } createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 7a4c711c604..e7baa0d80bd 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -18,6 +18,7 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { dirname, joinPath } from 'vs/base/common/resources'; +import { TestBackupMainService, TestDialogMainService } from 'vs/workbench/test/workbenchTestServices'; suite('WorkspacesMainService', () => { const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice'); @@ -43,7 +44,7 @@ suite('WorkspacesMainService', () => { let service: WorkspacesMainService; setup(async () => { - service = new WorkspacesMainService(environmentService, logService); + service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService()); // Delete any existing backups completely and then re-create it. await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); diff --git a/src/vs/workbench/services/workspaces/browser/workspacesService.ts b/src/vs/workbench/services/workspaces/browser/workspacesService.ts index 96c681e7976..24240836def 100644 --- a/src/vs/workbench/services/workspaces/browser/workspacesService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspacesService.ts @@ -113,7 +113,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS //#region Workspace Management - enterWorkspace(path: URI): Promise { + enterWorkspace(path: URI): Promise { throw new Error('Untitled workspaces are currently unsupported in Web'); } diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 5dc75ce4f00..35dcbd85dde 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -88,6 +88,9 @@ import product from 'vs/platform/product/common/product'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; +import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; +import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -1381,3 +1384,79 @@ export class TestElectronService implements IElectronService { async resolveProxy(url: string): Promise { return undefined; } async openExtensionDevelopmentHostWindow(args: minimist.ParsedArgs, env: IProcessEnvironment): Promise { } } + +export class TestBackupMainService implements IBackupMainService { + _serviceBrand: undefined; + + isHotExitEnabled(): boolean { + throw new Error('Method not implemented.'); + } + + getWorkspaceBackups(): IWorkspaceBackupInfo[] { + throw new Error('Method not implemented.'); + } + + getFolderBackupPaths(): URI[] { + throw new Error('Method not implemented.'); + } + + getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] { + throw new Error('Method not implemented.'); + } + + registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string | undefined): string { + throw new Error('Method not implemented.'); + } + + registerFolderBackupSync(folderUri: URI): string { + throw new Error('Method not implemented.'); + } + + registerEmptyWindowBackupSync(backupFolder?: string | undefined, remoteAuthority?: string | undefined): string { + throw new Error('Method not implemented.'); + } + + unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void { + throw new Error('Method not implemented.'); + } + + unregisterFolderBackupSync(folderUri: URI): void { + throw new Error('Method not implemented.'); + } + + unregisterEmptyWindowBackupSync(backupFolder: string): void { + throw new Error('Method not implemented.'); + } +} + +export class TestDialogMainService implements IDialogMainService { + _serviceBrand: undefined; + + pickFileFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } + + pickFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } + + pickFile(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } + + pickWorkspace(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } + + showMessageBox(options: Electron.MessageBoxOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } + + showSaveDialog(options: Electron.SaveDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } + + showOpenDialog(options: Electron.OpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { + throw new Error('Method not implemented.'); + } +}