From d8a4891005347b287fca620f2f20243e063f5807 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 14 Dec 2020 08:23:23 +0100 Subject: [PATCH] windows - first cut cleanup of windows finding logic --- .../sharedProcess/sharedProcessMain.ts | 2 +- src/vs/code/electron-main/app.ts | 24 ++- .../electron-main/extensionHostDebugIpc.ts | 3 +- .../launch/electron-main/launchMainService.ts | 3 +- .../platform/menubar/electron-main/menubar.ts | 3 +- .../electron-main/nativeHostMainService.ts | 3 +- src/vs/platform/windows/common/windows.ts | 108 ++++++------- .../platform/windows/electron-main/window.ts | 150 ----------------- .../platform/windows/electron-main/windows.ts | 22 ++- .../windows/electron-main/windowsFinder.ts | 79 +++++++++ .../electron-main/windowsMainService.ts | 74 ++++----- .../electron-main/windowsStateStorage.ts | 22 +-- .../windows/{common => node}/windowTracker.ts | 0 .../windows/test/electron-main/window.test.ts | 153 ++++++++---------- .../electron-main/workspacesMainService.ts | 12 +- 15 files changed, 299 insertions(+), 359 deletions(-) delete mode 100644 src/vs/platform/windows/electron-main/window.ts create mode 100644 src/vs/platform/windows/electron-main/windowsFinder.ts rename src/vs/platform/windows/{common => node}/windowTracker.ts (100%) diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 3dcc360b59e..1224377cb0d 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -67,7 +67,7 @@ import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-s import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 6eb685b7926..920833f540a 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -7,7 +7,6 @@ import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindo import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { resolveShellEnv } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; @@ -36,7 +35,7 @@ import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; import { FileProtocolHandler } from 'vs/code/electron-main/protocol'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService'; @@ -80,7 +79,7 @@ import { stripComments } from 'vs/base/common/json'; import { generateUuid } from 'vs/base/common/uuid'; import { VSBuffer } from 'vs/base/common/buffer'; import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { DisplayMainService, IDisplayMainService } from 'vs/platform/display/electron-main/displayMainService'; @@ -89,6 +88,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; +import { once } from 'vs/base/common/functional'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -320,10 +320,10 @@ export class CodeApplication extends Disposable { acceptShellEnv(shellEnv); }); - ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { + ipcMain.handle('vscode:writeNlsFile', async (event, path: unknown, data: unknown) => { const uri = this.validateNlsPath([path]); if (!uri || typeof data !== 'string') { - return Promise.reject('Invalid operation (vscode:writeNlsFile)'); + throw new Error('Invalid operation (vscode:writeNlsFile)'); } return this.fileService.writeFile(uri, VSBuffer.fromString(data)); @@ -332,7 +332,7 @@ export class CodeApplication extends Disposable { ipcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { const uri = this.validateNlsPath(paths); if (!uri) { - return Promise.reject('Invalid operation (vscode:readNlsFile)'); + throw new Error('Invalid operation (vscode:readNlsFile)'); } return (await this.fileService.readFile(uri)).value.toString(); @@ -896,6 +896,18 @@ export class CodeApplication extends Disposable { // Signal phase: after window open this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; + // Windows: install mutex + const win32MutexName = product.win32MutexName; + if (isWindows && win32MutexName) { + try { + const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; + const mutex = new WindowsMutex(win32MutexName); + once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); + } catch (e) { + this.logService.error(e); + } + } + // Remote Authorities protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { callback({ diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index 7cc40f9086d..f0ac255cb2a 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -8,8 +8,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { createServer, AddressInfo } from 'net'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { OpenContext } from 'vs/platform/windows/electron-main/window'; +import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 61e66f0fd6a..97d251557da 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -9,8 +9,7 @@ import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/electron-main/window'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 215be10e210..ad187bf1770 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -8,7 +8,6 @@ import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, KeyboardEvent } from 'electron'; import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -16,7 +15,7 @@ import product from 'vs/platform/product/common/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { mnemonicMenuLabel } from 'vs/base/common/labels'; -import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 57b2a5c8822..9b655201eef 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron'; -import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index ca3e63d96ed..cae49ffdd3f 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -18,38 +18,38 @@ export const WindowMinimumSize = { }; export interface IBaseOpenWindowsOptions { - forceReuseWindow?: boolean; + readonly forceReuseWindow?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { - forceNewWindow?: boolean; - preferNewWindow?: boolean; + readonly forceNewWindow?: boolean; + readonly preferNewWindow?: boolean; - noRecentEntry?: boolean; + readonly noRecentEntry?: boolean; - addMode?: boolean; + readonly addMode?: boolean; - diffMode?: boolean; - gotoLineMode?: boolean; + readonly diffMode?: boolean; + readonly gotoLineMode?: boolean; - waitMarkerFileURI?: URI; + readonly waitMarkerFileURI?: URI; } export interface IAddFoldersRequest { - foldersToAdd: UriComponents[]; + readonly foldersToAdd: UriComponents[]; } export interface IOpenedWindow { - id: number; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - title: string; - filename?: string; - dirty: boolean; + readonly id: number; + readonly workspace?: IWorkspaceIdentifier; + readonly folderUri?: ISingleFolderWorkspaceIdentifier; + readonly title: string; + readonly filename?: string; + readonly dirty: boolean; } export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { - remoteAuthority?: string; + readonly remoteAuthority?: string; } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; @@ -59,15 +59,15 @@ export interface IBaseWindowOpenable { } export interface IWorkspaceToOpen extends IBaseWindowOpenable { - workspaceUri: URI; + readonly workspaceUri: URI; } export interface IFolderToOpen extends IBaseWindowOpenable { - folderUri: URI; + readonly folderUri: URI; } export interface IFileToOpen extends IBaseWindowOpenable { - fileUri: URI; + readonly fileUri: URI; } export function isWorkspaceToOpen(uriToOpen: IWindowOpenable): uriToOpen is IWorkspaceToOpen { @@ -96,25 +96,25 @@ export function getMenuBarVisibility(configurationService: IConfigurationService } export interface IWindowsConfiguration { - window: IWindowSettings; + readonly window: IWindowSettings; } export interface IWindowSettings { - openFilesInNewWindow: 'on' | 'off' | 'default'; - openFoldersInNewWindow: 'on' | 'off' | 'default'; - openWithoutArgumentsInNewWindow: 'on' | 'off'; - restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; - restoreFullscreen: boolean; - zoomLevel: number; - titleBarStyle: 'native' | 'custom'; - autoDetectHighContrast: boolean; - menuBarVisibility: MenuBarVisibility; - newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; - nativeTabs: boolean; - nativeFullScreen: boolean; - enableMenuBarMnemonics: boolean; - closeWhenEmpty: boolean; - clickThroughInactive: boolean; + readonly openFilesInNewWindow: 'on' | 'off' | 'default'; + readonly openFoldersInNewWindow: 'on' | 'off' | 'default'; + readonly openWithoutArgumentsInNewWindow: 'on' | 'off'; + readonly restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; + readonly restoreFullscreen: boolean; + readonly zoomLevel: number; + readonly titleBarStyle: 'native' | 'custom'; + readonly autoDetectHighContrast: boolean; + readonly menuBarVisibility: MenuBarVisibility; + readonly newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; + readonly nativeTabs: boolean; + readonly nativeFullScreen: boolean; + readonly enableMenuBarMnemonics: boolean; + readonly closeWhenEmpty: boolean; + readonly clickThroughInactive: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -153,24 +153,24 @@ export interface IPath extends IPathData { export interface IPathData { // the file path to open within the instance - fileUri?: UriComponents; + readonly fileUri?: UriComponents; // the line number in the file path to open - lineNumber?: number; + readonly lineNumber?: number; // the column number in the file path to open - columnNumber?: number; + readonly columnNumber?: number; // a hint that the file exists. if true, the // file exists, if false it does not. with // undefined the state is unknown. - exists?: boolean; + readonly exists?: boolean; // Specifies if the file should be only be opened if it exists - openOnlyIfExists?: boolean; + readonly openOnlyIfExists?: boolean; // Specifies an optional id to override the editor used to edit the resource, e.g. custom editor. - overrideId?: string; + readonly overrideId?: string; } export interface IPathsToWaitFor extends IPathsToWaitForData { @@ -179,36 +179,36 @@ export interface IPathsToWaitFor extends IPathsToWaitForData { } interface IPathsToWaitForData { - paths: IPathData[]; - waitMarkerFileUri: UriComponents; + readonly paths: IPathData[]; + readonly waitMarkerFileUri: UriComponents; } export interface IOpenFileRequest { - filesToOpenOrCreate?: IPathData[]; - filesToDiff?: IPathData[]; + readonly filesToOpenOrCreate?: IPathData[]; + readonly filesToDiff?: IPathData[]; } /** * Additional context for the request on native only. */ export interface INativeOpenFileRequest extends IOpenFileRequest { - termProgram?: string; - filesToWait?: IPathsToWaitForData; + readonly termProgram?: string; + readonly filesToWait?: IPathsToWaitForData; } export interface INativeRunActionInWindowRequest { - id: string; - from: 'menu' | 'touchbar' | 'mouse'; - args?: any[]; + readonly id: string; + readonly from: 'menu' | 'touchbar' | 'mouse'; + readonly args?: any[]; } export interface INativeRunKeybindingInWindowRequest { - userSettingsLabel: string; + readonly userSettingsLabel: string; } export interface IColorScheme { - dark: boolean; - highContrast: boolean; + readonly dark: boolean; + readonly highContrast: boolean; } export interface IWindowConfiguration { @@ -224,7 +224,7 @@ export interface IWindowConfiguration { } export interface IOSConfiguration { - release: string; + readonly release: string; } export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs { diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts deleted file mode 100644 index 6859b36ab9f..00000000000 --- a/src/vs/platform/windows/electron-main/window.ts +++ /dev/null @@ -1,150 +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 { 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 { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; - -export const enum OpenContext { - - // opening when running from the command line - CLI, - - // macOS only: opening from the dock (also when opening files to a running instance from desktop) - DOCK, - - // opening from the main application window - MENU, - - // opening from a file or folder dialog - DIALOG, - - // opening from the OS's UI - DESKTOP, - - // opening through the API - API -} - -export interface IWindowContext { - openedWorkspace?: IWorkspaceIdentifier; - openedFolderUri?: URI; - - extensionDevelopmentPath?: string[]; - lastFocusTime: number; -} - -export interface IBestWindowOrFolderOptions { - windows: W[]; - newWindow: boolean; - context: OpenContext; - fileUri?: URI; - 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 => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { - return window; - } - } else { - // use the config path instead - if (extUriBiasedIgnorePathCase.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 && extUriBiasedIgnorePathCase.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.find(window => window.lastFocusTime === lastFocusedDate); -} - -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 && extUriBiasedIgnorePathCase.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?.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 && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, uri)) { - return window; - } - - // check for folder path - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, uri)) { - return window; - } - } - return null; -} diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index e77f247e08a..0e04969f308 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowOpenable, IOpenEmptyWindowOptions, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -16,6 +15,27 @@ import { Rectangle, BrowserWindow, WebContents } from 'electron'; import { IDisposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; +export const enum OpenContext { + + // opening when running from the command line + CLI, + + // macOS only: opening from the dock (also when opening files to a running instance from desktop) + DOCK, + + // opening from the main application window + MENU, + + // opening from a file or folder dialog + DIALOG, + + // opening from the OS's UI + DESKTOP, + + // opening through the API + API +} + export interface IWindowState { width?: number; height?: number; diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts new file mode 100644 index 00000000000..58e42035f88 --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceIdentifier, IResolvedWorkspace } from 'vs/platform/workspaces/common/workspaces'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; + +export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): ICodeWindow | undefined { + + // 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); + + // resolved workspace: folders are known and can be compared with + if (resolvedWorkspace) { + if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { + return window; + } + } + + // unresolved: can only compare with workspace location + else { + if (extUriBiasedIgnorePathCase.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 && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedFolderUri)); + if (singleFolderWindowsOnFilePath.length) { + return singleFolderWindowsOnFilePath.sort((windowA, windowB) => -(windowA.openedFolderUri!.path.length - windowB.openedFolderUri!.path.length))[0]; + } + + return undefined; +} + +export function findWindowOnWorkspaceOrFolder(windows: ICodeWindow[], folderOrWorkspaceConfigUri: URI): ICodeWindow | undefined { + + for (const window of windows) { + + // check for workspace config path + if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, folderOrWorkspaceConfigUri)) { + return window; + } + + // check for folder path + if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, folderOrWorkspaceConfigUri)) { + return window; + } + } + + return undefined; +} + + +export function findWindowOnExtensionDevelopmentPath(windows: ICodeWindow[], extensionDevelopmentPaths: string[]): ICodeWindow | undefined { + + const matches = (uriString: string): boolean => { + return extensionDevelopmentPaths.some(path => extUriBiasedIgnorePathCase.isEqual(URI.file(path), URI.file(uriString))); + }; + + for (const window of windows) { + + // match on extension development path. the path can be one or more paths + // so we check if any of the paths match on any of the provided ones + if (window.config?.extensionDevelopmentPath?.some(path => matches(path))) { + return window; + } + } + + return undefined; +} diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 0fe8e924f00..7b2fd10d40f 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -18,12 +18,12 @@ import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMai import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, OpenContext } from 'vs/platform/windows/electron-main/window'; +import { findWindowOnFile, findWindowOnWorkspaceOrFolder, findWindowOnExtensionDevelopmentPath } from 'vs/platform/windows/electron-main/windowsFinder'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; -import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration, OpenContext } 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 { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; @@ -194,20 +194,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); - this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex()); - } - - private installWindowsMutex(): void { - const win32MutexName = product.win32MutexName; - if (isWindows && win32MutexName) { - try { - const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; - const mutex = new WindowsMutex(win32MutexName); - once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); - } catch (e) { - this.logService.error(e); - } - } } private registerListeners(): void { @@ -572,32 +558,40 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // only look at the windows with correct authority const windows = WindowsMainService.WINDOWS.filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); - const bestWindowOrFolder = findBestWindowOrFolderForFile({ - windows, - newWindow: openFilesInNewWindow, - context: openConfig.context, - fileUri: fileToCheck?.fileUri, - localWorkspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null - }); + // figure out a good window to open the files in if any + // with a fallback to the last active window. + // + // in case `openFilesInNewWindow` is enforced, we skip + // this step. + let windowToUseForFiles: ICodeWindow | undefined = undefined; + if (fileToCheck?.fileUri && !openFilesInNewWindow) { + if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) { + windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null); + } + + if (!windowToUseForFiles) { + windowToUseForFiles = this.doGetLastActiveWindow(windows); + } + } // We found a window to open the files in - if (bestWindowOrFolder instanceof CodeWindow) { + if (windowToUseForFiles) { // Window is workspace - if (bestWindowOrFolder.openedWorkspace) { - workspacesToOpen.push({ workspace: bestWindowOrFolder.openedWorkspace, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + if (windowToUseForFiles.openedWorkspace) { + workspacesToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is single folder - else if (bestWindowOrFolder.openedFolderUri) { - foldersToOpen.push({ folderUri: bestWindowOrFolder.openedFolderUri, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + else if (windowToUseForFiles.openedFolderUri) { + foldersToOpen.push({ folderUri: windowToUseForFiles.openedFolderUri, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is empty else { // Do open files - const window = this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen); + const window = this.doOpenFilesInExistingWindow(openConfig, windowToUseForFiles, filesToOpen); usedWindows.push(window); // Reset `filesToOpen` because we handled them and also remember window we used @@ -630,7 +624,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (allWorkspacesToOpen.length > 0) { // Check for existing instances - const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); + const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(WindowsMainService.WINDOWS, workspaceToOpen.workspace.configPath))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; @@ -676,7 +670,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (allFoldersToOpen.length > 0) { // Check for existing instances - const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); + const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(WindowsMainService.WINDOWS, folderToOpen.folderUri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; @@ -1347,7 +1341,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cliArgs = cliArgs.filter(path => { const uri = URI.file(path); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, uri)) { + if (!!findWindowOnWorkspaceOrFolder(WindowsMainService.WINDOWS, uri)) { return false; } @@ -1356,7 +1350,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic folderUris = folderUris.filter(folderUriStr => { const folderUri = this.argToUri(folderUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, folderUri)) { + if (folderUri && !!findWindowOnWorkspaceOrFolder(WindowsMainService.WINDOWS, folderUri)) { return false; } @@ -1365,7 +1359,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic fileUris = fileUris.filter(fileUriStr => { const fileUri = this.argToUri(fileUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, fileUri)) { + if (fileUri && !!findWindowOnWorkspaceOrFolder(WindowsMainService.WINDOWS, fileUri)) { return false; } @@ -1699,11 +1693,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } getLastActiveWindow(): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS); + return this.doGetLastActiveWindow(WindowsMainService.WINDOWS); } private getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS.filter(window => window.remoteAuthority === remoteAuthority)); + return this.doGetLastActiveWindow(WindowsMainService.WINDOWS.filter(window => window.remoteAuthority === remoteAuthority)); + } + + private doGetLastActiveWindow(windows: ICodeWindow[]): ICodeWindow | undefined { + const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); + + return windows.find(window => window.lastFocusTime === lastFocusedDate); } sendToFocused(channel: string, ...args: any[]): void { diff --git a/src/vs/platform/windows/electron-main/windowsStateStorage.ts b/src/vs/platform/windows/electron-main/windowsStateStorage.ts index 165333950bc..6b9a0b38a11 100644 --- a/src/vs/platform/windows/electron-main/windowsStateStorage.ts +++ b/src/vs/platform/windows/electron-main/windowsStateStorage.ts @@ -10,22 +10,22 @@ import { IWindowState, IWindowsState } from 'vs/platform/windows/electron-main/w export type WindowsStateStorageData = object; interface ISerializedWindowsState { - lastActiveWindow?: ISerializedWindowState; - lastPluginDevelopmentHostWindow?: ISerializedWindowState; - openedWindows: ISerializedWindowState[]; + readonly lastActiveWindow?: ISerializedWindowState; + readonly lastPluginDevelopmentHostWindow?: ISerializedWindowState; + readonly openedWindows: ISerializedWindowState[]; } interface ISerializedWindowState { - workspaceIdentifier?: { id: string; configURIPath: string }; - folder?: string; - backupPath?: string; - remoteAuthority?: string; - uiState: IWindowUIState; + readonly workspaceIdentifier?: { id: string; configURIPath: string }; + readonly folder?: string; + readonly backupPath?: string; + readonly remoteAuthority?: string; + readonly uiState: IWindowUIState; // deprecated - folderUri?: UriComponents; - folderPath?: string; - workspace?: { id: string; configPath: string }; + readonly folderUri?: UriComponents; + readonly folderPath?: string; + readonly workspace?: { id: string; configPath: string }; } export function restoreWindowsState(data: WindowsStateStorageData | undefined): IWindowsState { diff --git a/src/vs/platform/windows/common/windowTracker.ts b/src/vs/platform/windows/node/windowTracker.ts similarity index 100% rename from src/vs/platform/windows/common/windowTracker.ts rename to src/vs/platform/windows/node/windowTracker.ts diff --git a/src/vs/platform/windows/test/electron-main/window.test.ts b/src/vs/platform/windows/test/electron-main/window.test.ts index 1beec3358a4..1e0dd0f04c2 100644 --- a/src/vs/platform/windows/test/electron-main/window.test.ts +++ b/src/vs/platform/windows/test/electron-main/window.test.ts @@ -2,14 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile, OpenContext } from 'vs/platform/windows/electron-main/window'; +import { findWindowOnFile } from 'vs/platform/windows/electron-main/windowsFinder'; +import { ICodeWindow, IWindowState } from 'vs/platform/windows/electron-main/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { UriDto } from 'vs/base/common/types'; +import { ICommandAction } from 'vs/platform/actions/common/actions'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; const fixturesFolder = getPathFromAmdModule(require, './fixtures'); @@ -19,22 +27,56 @@ const testWorkspace: IWorkspaceIdentifier = { }; const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); +const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }; -function options(custom?: Partial>): IBestWindowOrFolderOptions { - return { - windows: [], - newWindow: false, - context: OpenContext.CLI, - codeSettingsFolder: '_vscode', - localWorkspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }, - ...custom +function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri?: URI, openedWorkspace?: IWorkspaceIdentifier }): ICodeWindow { + return new class implements ICodeWindow { + onLoad: Event = Event.None; + onReady: Event = Event.None; + onClose: Event = Event.None; + onDestroy: Event = Event.None; + whenClosedOrLoaded: Promise = Promise.resolve(); + id: number = -1; + win: Electron.BrowserWindow = undefined!; + config: INativeWindowConfiguration | undefined; + openedFolderUri = options.openedFolderUri; + openedWorkspace = options.openedWorkspace; + backupPath?: string | undefined; + remoteAuthority?: string | undefined; + isExtensionDevelopmentHost = false; + isExtensionTestHost = false; + lastFocusTime = options.lastFocusTime; + isFullScreen = false; + isReady = true; + hasHiddenTitleBarStyle = false; + + ready(): Promise { throw new Error('Method not implemented.'); } + setReady(): void { throw new Error('Method not implemented.'); } + addTabbedWindow(window: ICodeWindow): void { throw new Error('Method not implemented.'); } + load(config: INativeWindowConfiguration, isReload?: boolean): void { throw new Error('Method not implemented.'); } + reload(cli?: NativeParsedArgs): void { throw new Error('Method not implemented.'); } + focus(options?: { force: boolean; }): void { throw new Error('Method not implemented.'); } + close(): void { throw new Error('Method not implemented.'); } + getBounds(): Electron.Rectangle { throw new Error('Method not implemented.'); } + send(channel: string, ...args: any[]): void { throw new Error('Method not implemented.'); } + sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void { throw new Error('Method not implemented.'); } + toggleFullScreen(): void { throw new Error('Method not implemented.'); } + isMinimized(): boolean { throw new Error('Method not implemented.'); } + setRepresentedFilename(name: string): void { throw new Error('Method not implemented.'); } + getRepresentedFilename(): string | undefined { throw new Error('Method not implemented.'); } + setDocumentEdited(edited: boolean): void { throw new Error('Method not implemented.'); } + isDocumentEdited(): boolean { throw new Error('Method not implemented.'); } + handleTitleDoubleClick(): void { throw new Error('Method not implemented.'); } + updateTouchBar(items: UriDto[][]): void { throw new Error('Method not implemented.'); } + serializeWindowState(): IWindowState { throw new Error('Method not implemented'); } + dispose(): void { } }; } -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[] = [ +const vscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }); +const lastActiveWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 3, openedFolderUri: undefined }); +const noVscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }); +const windows: ICodeWindow[] = [ vscodeFolderWindow, lastActiveWindow, noVscodeFolderWindow, @@ -43,86 +85,27 @@ const windows: IWindowContext[] = [ suite('WindowsFinder', () => { test('New window without folder when no windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options()), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - newWindow: true - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - context: OpenContext.API - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt')) - })), null); - }); - - test('New window without folder when windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - newWindow: true - })), null); - }); - - test('Last active window', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt')) - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [lastActiveWindow, noVscodeFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - context: OpenContext.API - })), lastActiveWindow); + assert.equal(findWindowOnFile([], URI.file('nonexisting'), localWorkspaceResolver), null); + assert.equal(findWindowOnFile([], URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), null); }); test('Existing window with folder', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), noVscodeFolderWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), vscodeFolderWindow); - 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')) - })), window); + assert.equal(findWindowOnFile(windows, URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), noVscodeFolderWindow); + + assert.equal(findWindowOnFile(windows, URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), localWorkspaceResolver), vscodeFolderWindow); + + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }); + assert.equal(findWindowOnFile([window], URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); }); test('More specific existing window wins', () => { - 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')) - })), nestedFolderWindow); + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }); + const nestedFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }); + assert.equal(findWindowOnFile([window, nestedFolderWindow], URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), nestedFolderWindow); }); test('Workspace folder wins', () => { - 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')) - })), window); + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedWorkspace: testWorkspace }); + assert.equal(findWindowOnFile([window], URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); }); }); diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 4663b2f7b3b..95e67c70a28 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -26,7 +26,7 @@ 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/dialogMainService'; -import { findWindowOnWorkspace } from 'vs/platform/windows/electron-main/window'; +import { findWindowOnWorkspaceOrFolder } from 'vs/platform/windows/electron-main/windowsFinder'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); @@ -266,22 +266,22 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return result; } - private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise { - if (!path) { + private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], workspacePath?: URI): Promise { + if (!workspacePath) { return true; } - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, path)) { + if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, workspacePath)) { 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))) { + if (findWindowOnWorkspaceOrFolder(windows, workspacePath)) { const options: MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), noLink: true };