sussudio
Version:
An unofficial VS Code Internal API
888 lines • 70.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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