src/vs/platform/windows/electron-main/windowsService.ts (366 lines of code) (raw):

/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import * as os from 'os'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import product from 'vs/platform/product/node/product'; import { IWindowsService, OpenContext, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IDevToolsOptions, INewWindowOptions, IOpenSettings, IURIToOpen } from 'vs/platform/windows/common/windows'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { shell, crashReporter, app, Menu, clipboard } from 'electron'; import { Event } from 'vs/base/common/event'; import { IURLService, IURLHandler } from 'vs/platform/url/common/url'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IWindowsMainService, ISharedProcess, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService, IRecentlyOpened, IRecent } from 'vs/platform/history/common/history'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { Schemas } from 'vs/base/common/network'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { isMacintosh, isLinux, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export class WindowsService extends Disposable implements IWindowsService, IURLHandler { _serviceBrand: ServiceIdentifier<any>; private readonly disposables = this._register(new DisposableStore()); private _activeWindowId: number | undefined; readonly onWindowOpen: Event<number> = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-created', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowBlur: Event<number> = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowMaximize: Event<number> = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-maximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowUnmaximize: Event<number> = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-unmaximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowFocus: Event<number> = Event.any( Event.map(Event.filter(Event.map(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), w => !!w), w => w!.id), Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)) ); readonly onRecentlyOpenedChange: Event<void> = this.historyService.onRecentlyOpenedChange; constructor( private sharedProcess: ISharedProcess, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IURLService urlService: IURLService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IHistoryMainService private readonly historyService: IHistoryMainService, @ILogService private readonly logService: ILogService ) { super(); urlService.registerHandler(this); // remember last active window id Event.latch(Event.any(this.onWindowOpen, this.onWindowFocus)) (id => this._activeWindowId = id, null, this.disposables); } async pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise<void> { this.logService.trace('windowsService#pickFileFolderAndOpen'); this.windowsMainService.pickFileFolderAndOpen(options); } async pickFileAndOpen(options: INativeOpenDialogOptions): Promise<void> { this.logService.trace('windowsService#pickFileAndOpen'); this.windowsMainService.pickFileAndOpen(options); } async pickFolderAndOpen(options: INativeOpenDialogOptions): Promise<void> { this.logService.trace('windowsService#pickFolderAndOpen'); this.windowsMainService.pickFolderAndOpen(options); } async pickWorkspaceAndOpen(options: INativeOpenDialogOptions): Promise<void> { this.logService.trace('windowsService#pickWorkspaceAndOpen'); this.windowsMainService.pickWorkspaceAndOpen(options); } async showMessageBox(windowId: number, options: Electron.MessageBoxOptions): Promise<IMessageBoxResult> { this.logService.trace('windowsService#showMessageBox', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.showMessageBox(options, codeWindow), () => this.windowsMainService.showMessageBox(options))!; } async showSaveDialog(windowId: number, options: Electron.SaveDialogOptions): Promise<string> { this.logService.trace('windowsService#showSaveDialog', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.showSaveDialog(options, codeWindow), () => this.windowsMainService.showSaveDialog(options))!; } async showOpenDialog(windowId: number, options: Electron.OpenDialogOptions): Promise<string[]> { this.logService.trace('windowsService#showOpenDialog', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.showOpenDialog(options, codeWindow), () => this.windowsMainService.showOpenDialog(options))!; } async reloadWindow(windowId: number, args: ParsedArgs): Promise<void> { this.logService.trace('windowsService#reloadWindow', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.reload(codeWindow, args)); } async openDevTools(windowId: number, options?: IDevToolsOptions): Promise<void> { this.logService.trace('windowsService#openDevTools', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.webContents.openDevTools(options)); } async toggleDevTools(windowId: number): Promise<void> { this.logService.trace('windowsService#toggleDevTools', windowId); return this.withWindow(windowId, codeWindow => { const contents = codeWindow.win.webContents; if (isMacintosh && codeWindow.hasHiddenTitleBarStyle() && !codeWindow.isFullScreen() && !contents.isDevToolsOpened()) { contents.openDevTools({ mode: 'undocked' }); // due to https://github.com/electron/electron/issues/3647 } else { contents.toggleDevTools(); } }); } async updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): Promise<void> { this.logService.trace('windowsService#updateTouchBar', windowId); return this.withWindow(windowId, codeWindow => codeWindow.updateTouchBar(items)); } async closeWorkspace(windowId: number): Promise<void> { this.logService.trace('windowsService#closeWorkspace', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.closeWorkspace(codeWindow)); } async enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | undefined> { this.logService.trace('windowsService#enterWorkspace', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.enterWorkspace(codeWindow, path)); } async toggleFullScreen(windowId: number): Promise<void> { this.logService.trace('windowsService#toggleFullScreen', windowId); return this.withWindow(windowId, codeWindow => codeWindow.toggleFullScreen()); } async setRepresentedFilename(windowId: number, fileName: string): Promise<void> { this.logService.trace('windowsService#setRepresentedFilename', windowId); return this.withWindow(windowId, codeWindow => codeWindow.setRepresentedFilename(fileName)); } async addRecentlyOpened(recents: IRecent[]): Promise<void> { this.logService.trace('windowsService#addRecentlyOpened'); this.historyService.addRecentlyOpened(recents); } async removeFromRecentlyOpened(paths: URI[]): Promise<void> { this.logService.trace('windowsService#removeFromRecentlyOpened'); this.historyService.removeFromRecentlyOpened(paths); } async clearRecentlyOpened(): Promise<void> { this.logService.trace('windowsService#clearRecentlyOpened'); this.historyService.clearRecentlyOpened(); } async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> { this.logService.trace('windowsService#getRecentlyOpened', windowId); return this.withWindow(windowId, codeWindow => this.historyService.getRecentlyOpened(codeWindow.config.workspace, codeWindow.config.folderUri, codeWindow.config.filesToOpenOrCreate), () => this.historyService.getRecentlyOpened())!; } async newWindowTab(): Promise<void> { this.logService.trace('windowsService#newWindowTab'); this.windowsMainService.openNewTabbedWindow(OpenContext.API); } async showPreviousWindowTab(): Promise<void> { this.logService.trace('windowsService#showPreviousWindowTab'); Menu.sendActionToFirstResponder('selectPreviousTab:'); } async showNextWindowTab(): Promise<void> { this.logService.trace('windowsService#showNextWindowTab'); Menu.sendActionToFirstResponder('selectNextTab:'); } async moveWindowTabToNewWindow(): Promise<void> { this.logService.trace('windowsService#moveWindowTabToNewWindow'); Menu.sendActionToFirstResponder('moveTabToNewWindow:'); } async mergeAllWindowTabs(): Promise<void> { this.logService.trace('windowsService#mergeAllWindowTabs'); Menu.sendActionToFirstResponder('mergeAllWindows:'); } async toggleWindowTabsBar(): Promise<void> { this.logService.trace('windowsService#toggleWindowTabsBar'); Menu.sendActionToFirstResponder('toggleTabBar:'); } async focusWindow(windowId: number): Promise<void> { this.logService.trace('windowsService#focusWindow', windowId); if (isMacintosh) { return this.withWindow(windowId, codeWindow => codeWindow.win.show()); } else { return this.withWindow(windowId, codeWindow => codeWindow.win.focus()); } } async closeWindow(windowId: number): Promise<void> { this.logService.trace('windowsService#closeWindow', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.close()); } async isFocused(windowId: number): Promise<boolean> { this.logService.trace('windowsService#isFocused', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.isFocused(), () => false)!; } async isMaximized(windowId: number): Promise<boolean> { this.logService.trace('windowsService#isMaximized', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.isMaximized(), () => false)!; } async maximizeWindow(windowId: number): Promise<void> { this.logService.trace('windowsService#maximizeWindow', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.maximize()); } async unmaximizeWindow(windowId: number): Promise<void> { this.logService.trace('windowsService#unmaximizeWindow', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.unmaximize()); } async minimizeWindow(windowId: number): Promise<void> { this.logService.trace('windowsService#minimizeWindow', windowId); return this.withWindow(windowId, codeWindow => codeWindow.win.minimize()); } async onWindowTitleDoubleClick(windowId: number): Promise<void> { this.logService.trace('windowsService#onWindowTitleDoubleClick', windowId); return this.withWindow(windowId, codeWindow => codeWindow.onWindowTitleDoubleClick()); } async setDocumentEdited(windowId: number, flag: boolean): Promise<void> { this.logService.trace('windowsService#setDocumentEdited', windowId); return this.withWindow(windowId, codeWindow => { if (codeWindow.win.isDocumentEdited() !== flag) { codeWindow.win.setDocumentEdited(flag); } }); } async openWindow(windowId: number, urisToOpen: IURIToOpen[], options: IOpenSettings): Promise<void> { this.logService.trace('windowsService#openWindow'); if (!urisToOpen || !urisToOpen.length) { return undefined; } this.windowsMainService.open({ context: OpenContext.API, contextWindowId: windowId, urisToOpen: urisToOpen, cli: options.args ? { ...this.environmentService.args, ...options.args } : this.environmentService.args, forceNewWindow: options.forceNewWindow, forceReuseWindow: options.forceReuseWindow, diffMode: options.diffMode, addMode: options.addMode, gotoLineMode: options.gotoLineMode, noRecentEntry: options.noRecentEntry, waitMarkerFileURI: options.waitMarkerFileURI }); } async openNewWindow(options?: INewWindowOptions): Promise<void> { this.logService.trace('windowsService#openNewWindow ' + JSON.stringify(options)); this.windowsMainService.openNewWindow(OpenContext.API, options); } async openExtensionDevelopmentHostWindow(args: ParsedArgs, env: IProcessEnvironment): Promise<void> { this.logService.trace('windowsService#openExtensionDevelopmentHostWindow ' + JSON.stringify(args)); if (args.extensionDevelopmentPath) { this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context: OpenContext.API, cli: args, userEnv: Object.keys(env).length > 0 ? env : undefined }); } } async getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { this.logService.trace('windowsService#getWindows'); const windows = this.windowsMainService.getWindows(); const result = windows.map(w => ({ id: w.id, workspace: w.openedWorkspace, folderUri: w.openedFolderUri, title: w.win.getTitle(), filename: w.getRepresentedFilename() })); return result; } async getWindowCount(): Promise<number> { this.logService.trace('windowsService#getWindowCount'); return this.windowsMainService.getWindows().length; } async log(severity: string, ...messages: string[]): Promise<void> { let consoleFn = console.log; switch (severity) { case 'error': consoleFn = console.error; break; case 'warn': consoleFn = console.warn; break; case 'info': consoleFn = console.info; break; } consoleFn(...messages); } async showItemInFolder(resource: URI): Promise<void> { this.logService.trace('windowsService#showItemInFolder'); if (resource.scheme === Schemas.file) { shell.showItemInFolder(resource.fsPath); } } async getActiveWindowId(): Promise<number | undefined> { return this._activeWindowId; } async openExternal(url: string): Promise<boolean> { this.logService.trace('windowsService#openExternal'); shell.openExternal(url); return true; } async startCrashReporter(config: Electron.CrashReporterStartOptions): Promise<void> { this.logService.trace('windowsService#startCrashReporter'); crashReporter.start(config); } async quit(): Promise<void> { this.logService.trace('windowsService#quit'); this.windowsMainService.quit(); } async relaunch(options: { addArgs?: string[], removeArgs?: string[] }): Promise<void> { this.logService.trace('windowsService#relaunch'); this.lifecycleService.relaunch(options); } async whenSharedProcessReady(): Promise<void> { this.logService.trace('windowsService#whenSharedProcessReady'); return this.sharedProcess.whenReady(); } async toggleSharedProcess(): Promise<void> { this.logService.trace('windowsService#toggleSharedProcess'); this.sharedProcess.toggle(); } async openAboutDialog(): Promise<void> { this.logService.trace('windowsService#openAboutDialog'); let version = app.getVersion(); if (product.target) { version = `${version} (${product.target} setup)`; } const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; const detail = nls.localize('aboutDetail', "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", version, product.commit || 'Unknown', product.date || 'Unknown', process.versions['electron'], process.versions['chrome'], process.versions['node'], process.versions['v8'], `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` ); const ok = nls.localize('okButton', "OK"); const copy = mnemonicButtonLabel(nls.localize({ key: 'copy', comment: ['&& denotes a mnemonic'] }, "&&Copy")); let buttons: string[]; if (isLinux) { buttons = [copy, ok]; } else { buttons = [ok, copy]; } const result = await this.windowsMainService.showMessageBox({ title: product.nameLong, type: 'info', message: product.nameLong, detail: `\n${detail}`, buttons, noLink: true, defaultId: buttons.indexOf(ok), cancelId: buttons.indexOf(ok) }, this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow()); if (buttons[result.button] === copy) { clipboard.writeText(detail); } } async handleURL(uri: URI): Promise<boolean> { // Catch file URLs if (uri.authority === Schemas.file && !!uri.path) { this.openFileForURI({ fileUri: URI.file(uri.fsPath) }); // using fsPath on a non-file URI... return true; } return false; } private openFileForURI(uri: IURIToOpen): void { const cli = assign(Object.create(null), this.environmentService.args); const urisToOpen = [uri]; this.windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true }); } async resolveProxy(windowId: number, url: string): Promise<string | undefined> { return new Promise(resolve => { const codeWindow = this.windowsMainService.getWindowById(windowId); if (codeWindow) { codeWindow.win.webContents.session.resolveProxy(url, proxy => { resolve(proxy); }); } else { resolve(); } }); } private withWindow<T>(windowId: number, fn: (window: ICodeWindow) => T, fallback?: () => T): T | undefined { const codeWindow = this.windowsMainService.getWindowById(windowId); if (codeWindow) { return fn(codeWindow); } if (fallback) { return fallback(); } return undefined; } }