UNPKG

sussudio

Version:

An unofficial VS Code Internal API

888 lines 70.1 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { app, BrowserWindow } from 'electron'; import { Promises } from "../../../base/node/pfs.mjs"; import { hostname, release } from 'os'; import { coalesce, distinct, firstOrDefault } from "../../../base/common/arrays.mjs"; import { CancellationToken } from "../../../base/common/cancellation.mjs"; import { Emitter, Event } from "../../../base/common/event.mjs"; import { isWindowsDriveLetter, parseLineAndColumnAware, sanitizeFilePath, toSlashes } from "../../../base/common/extpath.mjs"; import { once } from "../../../base/common/functional.mjs"; import { getPathLabel, mnemonicButtonLabel } from "../../../base/common/labels.mjs"; import { Disposable, DisposableStore } from "../../../base/common/lifecycle.mjs"; import { Schemas } from "../../../base/common/network.mjs"; import { basename, join, normalize, posix } from "../../../base/common/path.mjs"; import { getMarks, mark } from "../../../base/common/performance.mjs"; import { isMacintosh, isWindows, OS } from "../../../base/common/platform.mjs"; import { cwd } from "../../../base/common/process.mjs"; import { extUriBiasedIgnorePathCase, isEqualAuthority, normalizePath, originalFSPath, removeTrailingPathSeparator } from "../../../base/common/resources.mjs"; import { assertIsDefined, withNullAsUndefined } from "../../../base/common/types.mjs"; import { URI } from "../../../base/common/uri.mjs"; import { localize } from "../../../nls.mjs"; import { IBackupMainService } from "../../backup/electron-main/backup.mjs"; import { IConfigurationService } from "../../configuration/common/configuration.mjs"; import { IDialogMainService } from "../../dialogs/electron-main/dialogMainService.mjs"; import { IEnvironmentMainService } from "../../environment/electron-main/environmentMainService.mjs"; import { FileType, IFileService } from "../../files/common/files.mjs"; import { IInstantiationService } from "../../instantiation/common/instantiation.mjs"; import { ILifecycleMainService } from "../../lifecycle/electron-main/lifecycleMainService.mjs"; import { ILogService } from "../../log/common/log.mjs"; import product from "../../product/common/product.mjs"; import { IProductService } from "../../product/common/productService.mjs"; import { IProtocolMainService } from "../../protocol/electron-main/protocol.mjs"; import { getRemoteAuthority } from "../../remote/common/remoteHosts.mjs"; import { IStateMainService } from "../../state/electron-main/state.mjs"; import { isFileToOpen, isFolderToOpen, isWorkspaceToOpen } from "../../window/common/window.mjs"; import { CodeWindow } from "./windowImpl.mjs"; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from "./windowsFinder.mjs"; import { WindowsStateHandler } from "./windowsStateHandler.mjs"; import { hasWorkspaceFileExtension, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier } from "../../workspace/common/workspace.mjs"; import { createEmptyWorkspaceIdentifier, getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from "../../workspaces/node/workspaces.mjs"; import { IWorkspacesHistoryMainService } from "../../workspaces/electron-main/workspacesHistoryMainService.mjs"; import { IWorkspacesManagementMainService } from "../../workspaces/electron-main/workspacesManagementMainService.mjs"; import { IThemeMainService } from "../../theme/electron-main/themeMainService.mjs"; import { IPolicyService } from "../../policy/common/policy.mjs"; import { IUserDataProfilesMainService } from "../../userDataProfile/electron-main/userDataProfile.mjs"; function isWorkspacePathToOpen(path) { return isWorkspaceIdentifier(path?.workspace); } function isSingleFolderWorkspacePathToOpen(path) { return isSingleFolderWorkspaceIdentifier(path?.workspace); } //#endregion let WindowsMainService = class WindowsMainService extends Disposable { machineId; initialUserEnv; logService; stateMainService; policyService; environmentMainService; userDataProfilesMainService; lifecycleMainService; backupMainService; configurationService; workspacesHistoryMainService; workspacesManagementMainService; instantiationService; dialogMainService; fileService; productService; protocolMainService; themeMainService; static WINDOWS = []; _onDidOpenWindow = this._register(new Emitter()); onDidOpenWindow = this._onDidOpenWindow.event; _onDidSignalReadyWindow = this._register(new Emitter()); onDidSignalReadyWindow = this._onDidSignalReadyWindow.event; _onDidDestroyWindow = this._register(new Emitter()); onDidDestroyWindow = this._onDidDestroyWindow.event; _onDidChangeWindowsCount = this._register(new Emitter()); onDidChangeWindowsCount = this._onDidChangeWindowsCount.event; _onDidTriggerSystemContextMenu = this._register(new Emitter()); onDidTriggerSystemContextMenu = this._onDidTriggerSystemContextMenu.event; windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateMainService, this.lifecycleMainService, this.logService, this.configurationService)); constructor(machineId, initialUserEnv, logService, stateMainService, policyService, environmentMainService, userDataProfilesMainService, lifecycleMainService, backupMainService, configurationService, workspacesHistoryMainService, workspacesManagementMainService, instantiationService, dialogMainService, fileService, productService, protocolMainService, themeMainService) { super(); this.machineId = machineId; this.initialUserEnv = initialUserEnv; this.logService = logService; this.stateMainService = stateMainService; this.policyService = policyService; this.environmentMainService = environmentMainService; this.userDataProfilesMainService = userDataProfilesMainService; this.lifecycleMainService = lifecycleMainService; this.backupMainService = backupMainService; this.configurationService = configurationService; this.workspacesHistoryMainService = workspacesHistoryMainService; this.workspacesManagementMainService = workspacesManagementMainService; this.instantiationService = instantiationService; this.dialogMainService = dialogMainService; this.fileService = fileService; this.productService = productService; this.protocolMainService = protocolMainService; this.themeMainService = themeMainService; this.registerListeners(); } registerListeners() { // Signal a window is ready after having entered a workspace this._register(this.workspacesManagementMainService.onDidEnterWorkspace(event => this._onDidSignalReadyWindow.fire(event.window))); // Update valid roots in protocol service for extension dev windows this._register(this.onDidSignalReadyWindow(window => { if (window.config?.extensionDevelopmentPath || window.config?.extensionTestsPath) { const disposables = new DisposableStore(); disposables.add(Event.any(window.onDidClose, window.onDidDestroy)(() => disposables.dispose())); // Allow access to extension development path if (window.config.extensionDevelopmentPath) { for (const extensionDevelopmentPath of window.config.extensionDevelopmentPath) { disposables.add(this.protocolMainService.addValidFileRoot(extensionDevelopmentPath)); } } // Allow access to extension tests path if (window.config.extensionTestsPath) { disposables.add(this.protocolMainService.addValidFileRoot(window.config.extensionTestsPath)); } } })); } openEmptyWindow(openConfig, options) { const cli = this.environmentMainService.args; const remoteAuthority = options?.remoteAuthority || undefined; const forceEmpty = true; const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority }); } openExistingWindow(window, openConfig) { // Bring window to front window.focus(); // Handle --wait this.handleWaitMarkerFile(openConfig, [window]); } async open(openConfig) { this.logService.trace('windowsManager#open'); if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window } const foldersToAdd = []; const foldersToOpen = []; const workspacesToOpen = []; const untitledWorkspacesToRestore = []; const emptyWindowsWithBackupsToRestore = []; let filesToOpen; let emptyToOpen = 0; // Identify things to open from open config const pathsToOpen = await this.getPathsToOpen(openConfig); this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); for (const path of pathsToOpen) { if (isSingleFolderWorkspacePathToOpen(path)) { if (openConfig.addMode) { // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. foldersToAdd.push(path); } else { foldersToOpen.push(path); } } else if (isWorkspacePathToOpen(path)) { workspacesToOpen.push(path); } else if (path.fileUri) { if (!filesToOpen) { filesToOpen = { filesToOpenOrCreate: [], filesToDiff: [], filesToMerge: [], remoteAuthority: path.remoteAuthority }; } filesToOpen.filesToOpenOrCreate.push(path); } else if (path.backupPath) { emptyWindowsWithBackupsToRestore.push({ backupFolder: basename(path.backupPath), remoteAuthority: path.remoteAuthority }); } else { emptyToOpen++; } } // When run with --diff, take the first 2 files to open as files to diff if (openConfig.diffMode && filesToOpen && filesToOpen.filesToOpenOrCreate.length >= 2) { filesToOpen.filesToDiff = filesToOpen.filesToOpenOrCreate.slice(0, 2); filesToOpen.filesToOpenOrCreate = []; } // When run with --merge, take the first 4 files to open as files to merge if (openConfig.mergeMode && filesToOpen && filesToOpen.filesToOpenOrCreate.length === 4) { filesToOpen.filesToMerge = filesToOpen.filesToOpenOrCreate.slice(0, 4); filesToOpen.filesToOpenOrCreate = []; filesToOpen.filesToDiff = []; } // When run with --wait, make sure we keep the paths to wait for if (filesToOpen && openConfig.waitMarkerFileURI) { filesToOpen.filesToWait = { paths: coalesce([...filesToOpen.filesToDiff, filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */, ...filesToOpen.filesToOpenOrCreate]), waitMarkerFileUri: openConfig.waitMarkerFileURI }; } // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) if (openConfig.initialStartup) { // Untitled workspaces are always restored untitledWorkspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspaces()); workspacesToOpen.push(...untitledWorkspacesToRestore); // Empty windows with backups are always restored emptyWindowsWithBackupsToRestore.push(...this.backupMainService.getEmptyWindowBackups()); } else { emptyWindowsWithBackupsToRestore.length = 0; } // Open based on config const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, emptyToOpen, filesToOpen, foldersToAdd); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, emptyToOpen: ${emptyToOpen})`); // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { // 1.) focus window we opened files in always with highest priority if (filesOpenedInWindow) { filesOpenedInWindow.focus(); } // Otherwise, find a good window based on open params else { const focusLastActive = this.windowsStateHandler.state.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; // 2.) focus last active window if we are not instructed to open any paths if (focusLastActive) { const lastActiveWindow = usedWindows.filter(window => this.windowsStateHandler.state.lastActiveWindow && window.backupPath === this.windowsStateHandler.state.lastActiveWindow.backupPath); if (lastActiveWindow.length) { lastActiveWindow[0].focus(); focusLastOpened = false; focusLastWindow = false; } } // 3.) if instructed to open paths, focus last window which is not restored if (focusLastOpened) { for (let i = usedWindows.length - 1; i >= 0; i--) { const usedWindow = usedWindows[i]; if ((usedWindow.openedWorkspace && untitledWorkspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace (usedWindow.backupPath && emptyWindowsWithBackupsToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window ) { continue; } usedWindow.focus(); focusLastWindow = false; break; } } // 4.) finally, always ensure to have at least last used window focused if (focusLastWindow) { usedWindows[usedWindows.length - 1].focus(); } } } // Remember in recent document list (unless this opens for extension development) // Also do not add paths when files are opened for diffing or merging, only if opened individually const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0; const isMerge = filesToOpen && filesToOpen.filesToMerge.length > 0; if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !isMerge && !openConfig.noRecentEntry) { const recents = []; for (const pathToOpen of pathsToOpen) { if (isWorkspacePathToOpen(pathToOpen) && !pathToOpen.transient /* never add transient workspaces to history */) { recents.push({ label: pathToOpen.label, workspace: pathToOpen.workspace, remoteAuthority: pathToOpen.remoteAuthority }); } else if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { recents.push({ label: pathToOpen.label, folderUri: pathToOpen.workspace.uri, remoteAuthority: pathToOpen.remoteAuthority }); } else if (pathToOpen.fileUri) { recents.push({ label: pathToOpen.label, fileUri: pathToOpen.fileUri, remoteAuthority: pathToOpen.remoteAuthority }); } } this.workspacesHistoryMainService.addRecentlyOpened(recents); } // Handle --wait this.handleWaitMarkerFile(openConfig, usedWindows); return usedWindows; } handleWaitMarkerFile(openConfig, usedWindows) { // If we got started with --wait from the CLI, we need to signal to the outside when the window // used for the edit operation is closed or loaded to a different folder so that the waiting // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === 0 /* OpenContext.CLI */ && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { (async () => { await usedWindows[0].whenClosedOrLoaded; try { await this.fileService.del(waitMarkerFileURI); } catch (error) { // ignore - could have been deleted from the window already } })(); } } async doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, filesToOpen, foldersToAdd) { // Keep track of used windows and remember // if files have been opened in one of them const usedWindows = []; let filesOpenedInWindow = undefined; function addUsedWindow(window, openedFiles) { usedWindows.push(window); if (openedFiles) { filesOpenedInWindow = window; filesToOpen = undefined; // reset `filesToOpen` since files have been opened } } // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); // Handle folders to add by looking for the last active workspace (not on initial startup) if (!openConfig.initialStartup && foldersToAdd.length > 0) { const authority = foldersToAdd[0].remoteAuthority; const lastActiveWindow = this.getLastActiveWindowForAuthority(authority); if (lastActiveWindow) { addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri))); } } // Handle files to open/diff/merge or to create when we dont open a folder and we do not restore any // folder/untitled from hot-exit by trying to open them in the window that fits best const potentialNewWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; if (filesToOpen && potentialNewWindowsCount === 0) { // Find suitable window or folder path to open files in const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0] || filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */; // only look at the windows with correct authority const windows = this.getWindows().filter(window => filesToOpen && isEqualAuthority(window.remoteAuthority, filesToOpen.remoteAuthority)); // 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 = undefined; if (fileToCheck?.fileUri && !openFilesInNewWindow) { if (openConfig.context === 4 /* OpenContext.DESKTOP */ || openConfig.context === 0 /* OpenContext.CLI */ || openConfig.context === 1 /* OpenContext.DOCK */) { windowToUseForFiles = await findWindowOnFile(windows, fileToCheck.fileUri, async (workspace) => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspace(workspace.configPath) : undefined); } if (!windowToUseForFiles) { windowToUseForFiles = this.doGetLastActiveWindow(windows); } } // We found a window to open the files in if (windowToUseForFiles) { // Window is workspace if (isWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { workspacesToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is single folder else if (isSingleFolderWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { foldersToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is empty else { addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowToUseForFiles, filesToOpen), true); } } // Finally, if no window or folder is found, just open the files in an empty window else { addUsedWindow(await this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, filesToOpen, forceNewWindow: true, remoteAuthority: filesToOpen.remoteAuthority, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, forceProfile: openConfig.forceProfile, forceTempProfile: openConfig.forceTempProfile }), true); } } // Handle workspaces to open (instructed and to restore) const allWorkspacesToOpen = distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates if (allWorkspacesToOpen.length > 0) { // Check for existing instances const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), workspaceToOpen.workspace.configPath))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones for (const workspaceToOpen of allWorkspacesToOpen) { if (windowsOnWorkspace.some(window => window.openedWorkspace && window.openedWorkspace.id === workspaceToOpen.workspace.id)) { continue; // ignore folders that are already open } const remoteAuthority = workspaceToOpen.remoteAuthority; const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined; // Do open folder addUsedWindow(await this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } } // Handle folders to open (instructed and to restore) const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.workspace.uri)); // prevent duplicates if (allFoldersToOpen.length > 0) { // Check for existing instances const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), folderToOpen.workspace.uri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, windowOnFolderPath.remoteAuthority) ? filesToOpen : undefined; // Do open files addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones for (const folderToOpen of allFoldersToOpen) { if (windowsOnFolderPath.some(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderToOpen.workspace.uri))) { continue; // ignore folders that are already open } const remoteAuthority = folderToOpen.remoteAuthority; const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined; // Do open folder addUsedWindow(await this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } } // Handle empty to restore const allEmptyToRestore = distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates if (allEmptyToRestore.length > 0) { for (const emptyWindowBackupInfo of allEmptyToRestore) { const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined; addUsedWindow(await this.doOpenEmpty(openConfig, true, remoteAuthority, filesToOpenInWindow, emptyWindowBackupInfo), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } } // Handle empty to open (only if no other window opened) if (usedWindows.length === 0 || filesToOpen) { if (filesToOpen && !emptyToOpen) { emptyToOpen++; } const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : openConfig.remoteAuthority; for (let i = 0; i < emptyToOpen; i++) { addUsedWindow(await this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen), !!filesToOpen); // any other window to open must open in new window then openFolderInNewWindow = true; } } return { windows: distinct(usedWindows), filesOpenedInWindow }; } doOpenFilesInExistingWindow(configuration, window, filesToOpen) { this.logService.trace('windowsManager#doOpenFilesInExistingWindow', { filesToOpen }); window.focus(); // make sure window has focus const params = { filesToOpenOrCreate: filesToOpen?.filesToOpenOrCreate, filesToDiff: filesToOpen?.filesToDiff, filesToMerge: filesToOpen?.filesToMerge, filesToWait: filesToOpen?.filesToWait, termProgram: configuration?.userEnv?.['TERM_PROGRAM'] }; window.sendWhenReady('vscode:openFiles', CancellationToken.None, params); return window; } doAddFoldersToExistingWindow(window, foldersToAdd) { this.logService.trace('windowsManager#doAddFoldersToExistingWindow', { foldersToAdd }); window.focus(); // make sure window has focus const request = { foldersToAdd }; window.sendWhenReady('vscode:addFolders', CancellationToken.None, request); return window; } doOpenEmpty(openConfig, forceNewWindow, remoteAuthority, filesToOpen, emptyWindowBackupInfo) { this.logService.trace('windowsManager#doOpenEmpty', { restore: !!emptyWindowBackupInfo, remoteAuthority, filesToOpen, forceNewWindow }); let windowToUse; if (!forceNewWindow && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/97172 } return this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, filesToOpen, windowToUse, emptyWindowBackupInfo, forceProfile: openConfig.forceProfile, forceTempProfile: openConfig.forceTempProfile }); } doOpenFolderOrWorkspace(openConfig, folderOrWorkspace, forceNewWindow, filesToOpen, windowToUse) { this.logService.trace('windowsManager#doOpenFolderOrWorkspace', { folderOrWorkspace, filesToOpen }); if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/49587 } return this.openInBrowserWindow({ workspace: folderOrWorkspace.workspace, userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, remoteAuthority: folderOrWorkspace.remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, filesToOpen, windowToUse, forceProfile: openConfig.forceProfile, forceTempProfile: openConfig.forceTempProfile }); } async getPathsToOpen(openConfig) { let pathsToOpen; let isCommandLineOrAPICall = false; let restoredWindows = false; // Extract paths: from API if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { pathsToOpen = await this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } // Check for force empty else if (openConfig.forceEmpty) { pathsToOpen = [Object.create(null)]; } // Extract paths: from CLI else if (openConfig.cli._.length || openConfig.cli['folder-uri'] || openConfig.cli['file-uri']) { pathsToOpen = await this.doExtractPathsFromCLI(openConfig.cli); if (pathsToOpen.length === 0) { pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to open from command line } isCommandLineOrAPICall = true; } // Extract paths: from previous session else { pathsToOpen = await this.doGetPathsFromLastSession(); if (pathsToOpen.length === 0) { pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore } restoredWindows = true; } // Convert multiple folders into workspace (if opened via API or CLI) // This will ensure to open these folders in one window instead of multiple // If we are in `addMode`, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)); if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; if (foldersToOpen.every(folderToOpen => isEqualAuthority(folderToOpen.remoteAuthority, remoteAuthority))) { // only if all folder have the same authority const workspace = await this.workspacesManagementMainService.createUntitledWorkspace(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); // Add workspace and remove folders thereby pathsToOpen.push({ workspace, remoteAuthority }); pathsToOpen = pathsToOpen.filter(path => !isSingleFolderWorkspacePathToOpen(path)); } } } // Check for `window.startup` setting to include all windows // from the previous session if this is the initial startup and we have // not restored windows already otherwise. // Use `unshift` to ensure any new window to open comes last // for proper focus treatment. if (openConfig.initialStartup && !restoredWindows && this.configurationService.getValue('window')?.restoreWindows === 'preserve') { const lastSessionPaths = await this.doGetPathsFromLastSession(); pathsToOpen.unshift(...lastSessionPaths.filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); } return pathsToOpen; } async doExtractPathsFromAPI(openConfig) { const pathResolveOptions = { gotoLineMode: openConfig.gotoLineMode, remoteAuthority: openConfig.remoteAuthority }; const pathsToOpen = await Promise.all(coalesce(openConfig.urisToOpen || []).map(async (pathToOpen) => { const path = await this.resolveOpenable(pathToOpen, pathResolveOptions); // Path exists if (path) { path.label = pathToOpen.label; return path; } // Path does not exist: show a warning box const uri = this.resourceFromOpenable(pathToOpen); const options = { title: this.productService.nameLong, type: 'info', buttons: [mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))], defaultId: 0, message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), detail: uri.scheme === Schemas.file ? localize('pathNotExistDetail', "The path '{0}' does not exist on this computer.", getPathLabel(uri, { os: OS, tildify: this.environmentMainService })) : localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString(true)), noLink: true }; this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); return undefined; })); return coalesce(pathsToOpen); } async doExtractPathsFromCLI(cli) { const pathsToOpen = []; const pathResolveOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined, forceOpenWorkspaceAsFile: // special case diff / merge mode to force open // workspace as file // https://github.com/microsoft/vscode/issues/149731 cli.diff && cli._.length === 2 || cli.merge && cli._.length === 4 }; // folder uris const folderUris = cli['folder-uri']; if (folderUris) { const resolvedFolderUris = await Promise.all(folderUris.map(rawFolderUri => { const folderUri = this.cliArgToUri(rawFolderUri); if (!folderUri) { return undefined; } return this.resolveOpenable({ folderUri }, pathResolveOptions); })); pathsToOpen.push(...coalesce(resolvedFolderUris)); } // file uris const fileUris = cli['file-uri']; if (fileUris) { const resolvedFileUris = await Promise.all(fileUris.map(rawFileUri => { const fileUri = this.cliArgToUri(rawFileUri); if (!fileUri) { return undefined; } return this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); })); pathsToOpen.push(...coalesce(resolvedFileUris)); } // folder or file paths const resolvedCliPaths = await Promise.all(cli._.map(cliPath => { return pathResolveOptions.remoteAuthority ? this.doResolveRemotePath(cliPath, pathResolveOptions) : this.doResolveFilePath(cliPath, pathResolveOptions); })); pathsToOpen.push(...coalesce(resolvedCliPaths)); return pathsToOpen; } cliArgToUri(arg) { try { const uri = URI.parse(arg); if (!uri.scheme) { this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); return undefined; } return uri; } catch (e) { this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); } return undefined; } async doGetPathsFromLastSession() { const restoreWindowsSetting = this.getRestoreWindowsSetting(); switch (restoreWindowsSetting) { // none: no window to restore case 'none': return []; // one: restore last opened workspace/folder or empty window // all: restore all windows // folders: restore last opened folders only case 'one': case 'all': case 'preserve': case 'folders': { // Collect previously opened windows const lastSessionWindows = []; if (restoreWindowsSetting !== 'one') { lastSessionWindows.push(...this.windowsStateHandler.state.openedWindows); } if (this.windowsStateHandler.state.lastActiveWindow) { lastSessionWindows.push(this.windowsStateHandler.state.lastActiveWindow); } const pathsToOpen = await Promise.all(lastSessionWindows.map(async (lastSessionWindow) => { // Workspaces if (lastSessionWindow.workspace) { const pathToOpen = await this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority, rejectTransientWorkspaces: true /* https://github.com/microsoft/vscode/issues/119695 */ }); if (isWorkspacePathToOpen(pathToOpen)) { return pathToOpen; } } // Folders else if (lastSessionWindow.folderUri) { const pathToOpen = await this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { return pathToOpen; } } // Empty window, potentially editors open to be restored else if (restoreWindowsSetting !== 'folders' && lastSessionWindow.backupPath) { return { backupPath: lastSessionWindow.backupPath, remoteAuthority: lastSessionWindow.remoteAuthority }; } return undefined; })); return coalesce(pathsToOpen); } } } getRestoreWindowsSetting() { let restoreWindows; if (this.lifecycleMainService.wasRestarted) { restoreWindows = 'all'; // always reopen all windows when an update was applied } else { const windowConfig = this.configurationService.getValue('window'); restoreWindows = windowConfig?.restoreWindows || 'all'; // by default restore all windows if (!['preserve', 'all', 'folders', 'one', 'none'].includes(restoreWindows)) { restoreWindows = 'all'; // by default restore all windows } } return restoreWindows; } async resolveOpenable(openable, options = Object.create(null)) { // handle file:// openables with some extra validation const uri = this.resourceFromOpenable(openable); if (uri.scheme === Schemas.file) { if (isFileToOpen(openable)) { options = { ...options, forceOpenWorkspaceAsFile: true }; } return this.doResolveFilePath(uri.fsPath, options); } // handle non file:// openables return this.doResolveRemoteOpenable(openable, options); } doResolveRemoteOpenable(openable, options) { let uri = this.resourceFromOpenable(openable); // use remote authority from vscode const remoteAuthority = getRemoteAuthority(uri) || options.remoteAuthority; // normalize URI uri = removeTrailingPathSeparator(normalizePath(uri)); // File if (isFileToOpen(openable)) { if (options.gotoLineMode) { const { path, line, column } = parseLineAndColumnAware(uri.path); return { fileUri: uri.with({ path }), options: { selection: line ? { startLineNumber: line, startColumn: column || 1 } : undefined }, remoteAuthority }; } return { fileUri: uri, remoteAuthority }; } // Workspace else if (isWorkspaceToOpen(openable)) { return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } // Folder return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } resourceFromOpenable(openable) { if (isWorkspaceToOpen(openable)) { return openable.workspaceUri; } if (isFolderToOpen(openable)) { return openable.folderUri; } return openable.fileUri; } async doResolveFilePath(path, options) { // Extract line/col information from path let lineNumber; let columnNumber; if (options.gotoLineMode) { ({ path, line: lineNumber, column: columnNumber } = parseLineAndColumnAware(path)); } // Ensure the path is normalized and absolute path = sanitizeFilePath(normalize(path), cwd()); try { const pathStat = await Promises.stat(path); // File if (pathStat.isFile()) { // Workspace (unless disabled via flag) if (!options.forceOpenWorkspaceAsFile) { const workspace = await this.workspacesManagementMainService.resolveLocalWorkspace(URI.file(path)); if (workspace) { // If the workspace is transient and we are to ignore // transient workspaces, reject it. if (workspace.transient && options.rejectTransientWorkspaces) { return undefined; } return { workspace: { id: workspace.id, configPath: workspace.configPath }, type: FileType.File, exists: true, remoteAuthority: workspace.remoteAuthority, transient: workspace.transient }; } } return { fileUri: URI.file(path), type: FileType.File, exists: true, options: { selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined } }; } // Folder else if (pathStat.isDirectory()) { return { workspace: getSingleFolderWorkspaceIdentifier(URI.file(path), pathStat), type: FileType.Directory, exists: true }; } // Special device: in POSIX environments, we may get /dev/null passed // in (for example git uses it to signal one side of a diff does not // exist). In that special case, treat it like a file to support this // scenario () else if (!isWindows && path === '/dev/null') { return { fileUri: URI.file(path), type: FileType.File, exists: true }; } } catch (error) { const fileUri = URI.file(path); // since file does not seem to exist anymore, remove from recent this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // assume this is a file that does not yet exist if (options.ignoreFileNotFound) { return { fileUri, type: FileType.File, exists: false }; } } return undefined; } doResolveRemotePath(path, options) { const first = path.charCodeAt(0); const remoteAuthority = options.remoteAuthority; // Extract line/col information from path let lineNumber; let columnNumber; if (options.gotoLineMode) { ({ path, line: lineNumber, column: columnNumber } = parseLineAndColumnAware(path)); } // make absolute if (first !== 47 /* CharCode.Slash */) { if (isWindowsDriveLetter(first) && path.charCodeAt(path.charCodeAt(1)) === 58 /* CharCode.Colon */) { path = toSlashes(path); } path = `/${path}`; } const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: path }); // guess the file type: // - if it ends with a slash it's a folder // - if in goto line mode or if it has a file extension, it's a file or a workspace // - by defaults it's a folder if (path.charCodeAt(path.length - 1) !== 47 /* CharCode.Slash */) { // file name ends with .code-workspace if (hasWorkspaceFileExtension(path)) { if (options.forceOpenWorkspaceAsFile) { return { fileUri: uri, options: { selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined }, remoteAuthority: options.remoteAuthority }; } return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } // file name starts with a dot or has an file extension else if (options.gotoLineMode || posix.basename(path).indexOf('.') !== -1) { return { fileUri: uri, options: { selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined }, remoteAuthority }; } } return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } shouldOpenNewWindow(openConfig) { // let the user settings override how folders are open in a new window or same window unless we are forced const windowConfig = this.configurationService.getValue('window'); const openFolderInNewWindowConfig = windowConfig?.openFoldersInNewWindow || 'default' /* default */; const openFilesInNewWindowConfig = windowConfig?.openFilesInNewWindow || 'off' /* default */; let openFolderInNewWindow = (openConfig.preferNewWindow || openConfig.forceNewWindow) && !openConfig