diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 34c8ac28222..3549a06aa48 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as nls from 'vs/nls'; -import * as perf from 'vs/base/common/performance'; +import { release } from 'os'; +import { join } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; +import { getMarks, mark } from 'vs/base/common/performance'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron'; @@ -20,7 +20,7 @@ import { WindowMinimumSize, IWindowSettings, MenuBarVisibility, getTitleBarStyle import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; @@ -189,9 +189,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { - options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png'); + options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png'); } else if (isWindows && !this.environmentService.isBuilt) { - options.icon = path.join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); + options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { @@ -592,9 +592,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], - message: nls.localize('appStalled', "The window is no longer responding"), - detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."), + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + message: localize('appStalled', "The window is no longer responding"), + detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true }, this._win).then(result => { if (!this._win) { @@ -614,17 +614,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { else { let message: string; if (details && details.reason !== 'crashed') { - message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); + message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); } else { - message = nls.localize('appCrashed', "The window has crashed", details?.reason); + message = localize('appCrashed', "The window has crashed", details?.reason); } this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message, - detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), + detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), noLink: true }, this._win).then(result => { if (!this._win) { @@ -690,7 +690,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { + load(config: INativeWindowConfiguration, { isReload, disableExtensions }: { isReload?: boolean, disableExtensions?: boolean } = Object.create(null)): void { // If this window was loaded before from the command line // (as indicated by VSCODE_CLI environment), make sure to @@ -741,7 +741,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Load URL - perf.mark('code/willOpenNewWindow'); + mark('code/willOpenNewWindow'); this._win.loadURL(this.getUrl(configuration)); // Make window visible if it did not open in N seconds because this indicates an error @@ -760,11 +760,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._onLoad.fire(); } - reload(cli?: NativeParsedArgs): void { + async reload(cli?: NativeParsedArgs): Promise { // Copy our current config for reuse const configuration = Object.assign({}, this.currentConfig); + // Validate workspace + configuration.workspace = await this.validateWorkspace(configuration); + // Delete some properties we do not want during reload delete configuration.filesToOpenOrCreate; delete configuration.filesToDiff; @@ -774,17 +777,44 @@ export class CodeWindow extends Disposable implements ICodeWindow { // in extension development mode. These options are all development related. if (this.isExtensionDevelopmentHost && cli) { configuration.verbose = cli.verbose; + configuration.debugId = cli.debugId; configuration['inspect-extensions'] = cli['inspect-extensions']; configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions']; - configuration.debugId = cli.debugId; configuration['extensions-dir'] = cli['extensions-dir']; } configuration.isInitialStartup = false; // since this is a reload // Load config - const disableExtensions = cli ? cli['disable-extensions'] : undefined; - this.load(configuration, true, disableExtensions); + this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); + } + + private async validateWorkspace(configuration: INativeWindowConfiguration): Promise { + + // Multi folder + if (isWorkspaceIdentifier(configuration.workspace)) { + const configPath = configuration.workspace.configPath; + if (configPath.scheme === Schemas.file) { + const workspaceExists = await this.fileService.exists(configPath); + if (!workspaceExists) { + return undefined; + } + } + } + + // Single folder + else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + const uri = configuration.workspace.uri; + if (uri.scheme === Schemas.file) { + const folderExists = await this.fileService.exists(uri); + if (!folderExists) { + return undefined; + } + } + } + + // Workspace is valid + return configuration.workspace; } private getUrl(windowConfiguration: INativeWindowConfiguration): string { @@ -817,14 +847,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.maximized = this._win.isMaximized(); // Dump Perf Counters - windowConfiguration.perfMarks = perf.getMarks(); + windowConfiguration.perfMarks = getMarks(); // Parts splash - windowConfiguration.partsSplashPath = path.join(this.environmentService.userDataPath, 'rapid_render.json'); + windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json'); // OS Info windowConfiguration.os = { - release: os.release() + release: release() }; // Config (combination of process.argv and window configuration) @@ -1175,7 +1205,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (visibility === 'toggle') { if (notify) { - this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); + this.send('vscode:showInfoMessage', localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); } } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 5e8ee189a75..8bd10465032 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -84,7 +84,7 @@ export interface ICodeWindow extends IDisposable { addTabbedWindow(window: ICodeWindow): void; - load(config: INativeWindowConfiguration, isReload?: boolean): void; + load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void; reload(cli?: NativeParsedArgs): void; focus(options?: { force: boolean }): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 3674b2d11d7..a58020842ec 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { statSync, unlink } from 'fs'; +import { statSync } from 'fs'; import { basename, normalize, join, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; @@ -40,6 +40,7 @@ import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware, sanitizeFileP import { CharCode } from 'vs/base/common/charCode'; import { getPathLabel } from 'vs/base/common/labels'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IFileService } from 'vs/platform/files/common/files'; //#region Helper Interfaces @@ -143,7 +144,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @IFileService private readonly fileService: IFileService ) { super(); @@ -312,7 +314,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { - usedWindows[0].whenClosedOrLoaded.then(() => unlink(waitMarkerFileURI.fsPath, () => undefined)); + usedWindows[0].whenClosedOrLoaded.then(() => this.fileService.del(waitMarkerFileURI), () => undefined); } return usedWindows; @@ -650,7 +652,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const pathsToOpen: IPathToOpen[] = []; const pathResolveOptions: IPathResolveOptions = { gotoLineMode: openConfig.gotoLineMode }; for (const pathToOpen of coalesce(openConfig.urisToOpen || [])) { - const path = this.resolveUri(pathToOpen, pathResolveOptions); + const path = this.resolveOpenable(pathToOpen, pathResolveOptions); // Path exists if (path) { @@ -688,9 +690,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const folderUris = cli['folder-uri']; if (folderUris) { for (const rawFolderUri of folderUris) { - const folderUri = this.argToUri(rawFolderUri); + const folderUri = this.cliArgToUri(rawFolderUri); if (folderUri) { - const path = this.resolveUri({ folderUri }, pathResolveOptions); + const path = this.resolveOpenable({ folderUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -702,9 +704,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const fileUris = cli['file-uri']; if (fileUris) { for (const rawFileUri of fileUris) { - const fileUri = this.argToUri(rawFileUri); + const fileUri = this.cliArgToUri(rawFileUri); if (fileUri) { - const path = this.resolveUri(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); + const path = this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -715,7 +717,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // folder or file paths const cliPaths = cli._; for (const cliPath of cliPaths) { - const path = this.resolvePath(cliPath, pathResolveOptions); + const path = this.doResolveFileOpenable(cliPath, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -724,7 +726,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return pathsToOpen; } - private argToUri(arg: string): URI | undefined { + private cliArgToUri(arg: string): URI | undefined { try { const uri = URI.parse(arg); if (!uri.scheme) { @@ -772,7 +774,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Workspaces if (lastSessionWindow.workspace) { - const pathToOpen = this.resolveUri({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + const pathToOpen = this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority }); if (isWorkspacePathToOpen(pathToOpen)) { pathsToOpen.push(pathToOpen); } @@ -780,7 +782,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Folders else if (lastSessionWindow.folderUri) { - const pathToOpen = this.resolveUri({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + const pathToOpen = this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { pathsToOpen.push(pathToOpen); } @@ -812,19 +814,19 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return restoreWindows; } - private resolveUri(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined { + private resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined { - // handle local openables with some extra validation + // handle file:// openables with some extra validation let uri = this.resourceFromOpenable(openable); if (uri.scheme === Schemas.file) { - return this.resolvePath(uri.fsPath, options, isFileToOpen(openable)); + return this.doResolveFileOpenable(openable, options); } - // handle remote openables - return this.resolveRemoteUri(openable, options); + // handle non file:// openables + return this.doResolveRemoteOpenable(openable, options); } - private resolveRemoteUri(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined { + private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { let uri = this.resourceFromOpenable(openable); // open remote if either specified in the cli or if it's a remotehost URI @@ -833,28 +835,23 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // normalize URI uri = removeTrailingPathSeparator(normalizePath(uri)); - // Remote File + // File if (isFileToOpen(openable)) { if (options.gotoLineMode) { const { path, line, column } = parseLineAndColumnAware(uri.path); - return { - fileUri: uri.with({ path }), - lineNumber: line, - columnNumber: column, - remoteAuthority - }; + return { fileUri: uri.with({ path }), lineNumber: line, columnNumber: column, remoteAuthority }; } return { fileUri: uri, remoteAuthority }; } - // Remote Workspace + // Workspace else if (isWorkspaceToOpen(openable)) { return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } - // Remote Folder + // Folder return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } @@ -870,7 +867,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private resolvePath(path: string, options: IPathResolveOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + private doResolveFileOpenable(path: string, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(pathOrOpenable: string | IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let path: string; + let forceOpenWorkspaceAsFile = false; + if (typeof pathOrOpenable === 'string') { + path = pathOrOpenable; + } else { + path = this.resourceFromOpenable(pathOrOpenable).fsPath; + forceOpenWorkspaceAsFile = isFileToOpen(pathOrOpenable); + } // Extract line/col information from path let lineNumber: number | undefined; @@ -884,17 +891,17 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic path = parsedPath.path; } - // Handle remote paths + // With remote: resolve path as remote URI const remoteAuthority = options.remoteAuthority; if (remoteAuthority) { - return this.resolveRemotePath(path, remoteAuthority, forceOpenWorkspaceAsFile); + return this.doResolvePathRemote(path, remoteAuthority, forceOpenWorkspaceAsFile); } - // Handle local paths - return this.resolveLocalPath(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile); + // Without remote: resolve path as local URI + return this.doResolvePathLocal(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile); } - private resolveRemotePath(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + private doResolvePathRemote(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { const first = path.charCodeAt(0); // make absolute @@ -908,7 +915,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic 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 it has a file extension, it's a file or a workspace. By defaults it's a folder. + // guess the file type: + // - if it ends with a slash it's a folder + // - 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) !== CharCode.Slash) { // file name ends with .code-workspace @@ -927,7 +937,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } - private resolveLocalPath(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + private doResolvePathLocal(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { // Ensure the path is normalized and absolute path = sanitizeFilePath(normalize(path), process.env['VSCODE_CWD'] || process.cwd()); @@ -940,21 +950,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (!forceOpenWorkspaceAsFile) { const workspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(URI.file(path)); if (workspace) { - return { - workspace: { id: workspace.id, configPath: workspace.configPath }, - remoteAuthority: workspace.remoteAuthority, - exists: true - }; + return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority, exists: true }; } } // File - return { - fileUri: URI.file(path), - lineNumber, - columnNumber, - exists: true - }; + return { fileUri: URI.file(path), lineNumber, columnNumber, exists: true }; } // Folder (we check for isDirectory() because e.g. paths like /dev/null @@ -1088,7 +1089,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); folderUris = folderUris.filter(folderUriStr => { - const folderUri = this.argToUri(folderUriStr); + const folderUri = this.cliArgToUri(folderUriStr); if (folderUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), folderUri)) { return false; } @@ -1097,7 +1098,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); fileUris = fileUris.filter(fileUriStr => { - const fileUri = this.argToUri(fileUriStr); + const fileUri = this.cliArgToUri(fileUriStr); if (fileUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), fileUri)) { return false; } 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 cf976b05ae8..cc3bdb720ce 100644 --- a/src/vs/platform/windows/test/electron-main/window.test.ts +++ b/src/vs/platform/windows/test/electron-main/window.test.ts @@ -51,7 +51,7 @@ function createTestCodeWindow(options: { lastFocusTime: number, openedFolderUri? 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.'); } + load(config: INativeWindowConfiguration, options: { 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.'); }