diff --git a/.eslintrc.json b/.eslintrc.json index f44673cd1cd..d4806b40e4d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -48,6 +48,25 @@ "orientation", "context" ], // non-complete list of globals that are easy to access unintentionally + "no-restricted-syntax": [ + "warn", + { + "selector": "BinaryExpression[operator='instanceof'][right.name='MouseEvent']", + "message": "Use DOM.isMouseEvent() to support multi-window scenarios." + }, + { + "selector": "BinaryExpression[operator='instanceof'][right.name='KeyboardEvent']", + "message": "Use DOM.isKeyboardEvent() to support multi-window scenarios." + }, + { + "selector": "BinaryExpression[operator='instanceof'][right.name='PointerEvent']", + "message": "Use DOM.isPointerEvent() to support multi-window scenarios." + }, + { + "selector": "BinaryExpression[operator='instanceof'][right.name='DragEvent']", + "message": "Use DOM.isDragEvent() to support multi-window scenarios." + } + ], "no-var": "warn", "jsdoc/no-types": "warn", "semi": "off", diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 143a15e1c6c..0dabbd4a499 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -12,7 +12,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import * as event from 'vs/base/common/event'; import * as dompurify from 'vs/base/browser/dompurify/dompurify'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, RemoteAuthorities, Schemas } from 'vs/base/common/network'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -729,6 +729,25 @@ export function getActiveDocument(): Document { return documents.find(doc => doc.hasFocus()) ?? document; } +export function getActiveWindow(): Window & typeof globalThis { + const document = getActiveDocument(); + return document.defaultView?.window ?? window; +} + +function getWindow(e: unknown): Window & typeof globalThis { + const candidateNode = e as Node | undefined; + if (candidateNode?.ownerDocument?.defaultView) { + return candidateNode.ownerDocument.defaultView.window; + } + + const candidateEvent = e as UIEvent | undefined; + if (candidateEvent?.view) { + return candidateEvent.view.window; + } + + return window; +} + export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0], beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement { const style = document.createElement('style'); style.type = 'text/css'; @@ -791,11 +810,24 @@ export function removeCSSRulesContainingSelector(ruleName: string, style: HTMLSt } } -export function isHTMLElement(o: any): o is HTMLElement { - if (typeof HTMLElement === 'object') { - return o instanceof HTMLElement; - } - return o && typeof o === 'object' && o.nodeType === 1 && typeof o.nodeName === 'string'; +export function isMouseEvent(e: unknown): e is MouseEvent { + // eslint-disable-next-line no-restricted-syntax + return e instanceof MouseEvent || e instanceof getWindow(e).MouseEvent; +} + +export function isKeyboardEvent(e: unknown): e is KeyboardEvent { + // eslint-disable-next-line no-restricted-syntax + return e instanceof KeyboardEvent || e instanceof getWindow(e).KeyboardEvent; +} + +export function isPointerEvent(e: unknown): e is PointerEvent { + // eslint-disable-next-line no-restricted-syntax + return e instanceof PointerEvent || e instanceof getWindow(e).PointerEvent; +} + +export function isDragEvent(e: unknown): e is DragEvent { + // eslint-disable-next-line no-restricted-syntax + return e instanceof DragEvent || e instanceof getWindow(e).DragEvent; } export const EventType = { @@ -915,7 +947,7 @@ class FocusTracker extends Disposable implements IFocusTracker { private _refreshStateHandler: () => void; private static hasFocusWithin(element: HTMLElement | Window): boolean { - if (isHTMLElement(element)) { + if (element instanceof HTMLElement) { const shadowRoot = getShadowRoot(element); const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement); return isAncestor(activeElement, element); @@ -1942,3 +1974,58 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia function camelCaseToHyphenCase(str: string) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } + +interface IObserver extends IDisposable { + readonly onDidChangeAttribute: event.Event; +} + +function observeAttributes(element: Element, filter?: string[]): IObserver { + const onDidChangeAttribute = new event.Emitter(); + + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName) { + onDidChangeAttribute.fire(mutation.attributeName); + } + } + }); + + observer.observe(element, { + attributes: true, + attributeFilter: filter + }); + + return { + onDidChangeAttribute: onDidChangeAttribute.event, + dispose: () => { + observer.disconnect(); + onDidChangeAttribute.dispose(); + } + }; +} + +export function copyAttributes(from: Element, to: Element): void { + for (const { name, value } of from.attributes) { + to.setAttribute(name, value); + } +} + +function copyAttribute(from: Element, to: Element, name: string): void { + const value = from.getAttribute(name); + if (value) { + to.setAttribute(name, value); + } else { + to.removeAttribute(name); + } +} + +export function trackAttributes(from: Element, to: Element, filter?: string[]): IDisposable { + copyAttributes(from, to); + + const observer = observeAttributes(from, filter); + + return combinedDisposable( + observer, + observer.onDidChangeAttribute(name => copyAttribute(from, to, name)) + ); +} diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index d2c9f9464ae..04779bd11aa 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -271,7 +271,7 @@ export class ContextView extends Disposable { let around: IView; // Get the element's position and size (to anchor the view) - if (DOM.isHTMLElement(anchor)) { + if (anchor instanceof HTMLElement) { const elementPosition = DOM.getDomNodePagePosition(anchor); // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element @@ -315,30 +315,31 @@ export class ContextView extends Disposable { let top: number; let left: number; + const activeWindow = DOM.getActiveWindow(); if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) { - const verticalAnchor: ILayoutAnchor = { offset: around.top - window.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; + const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; - top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset; + top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset; // if view intersects vertically with anchor, we must avoid the anchor if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) { horizontalAnchor.mode = LayoutAnchorMode.AVOID; } - left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor); + left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor); } else { const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After }; const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN }; - left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor); + left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor); // if view intersects horizontally with anchor, we must avoid the anchor if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) { verticalAnchor.mode = LayoutAnchorMode.AVOID; } - top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset; + top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset; } this.view.classList.remove('top', 'bottom', 'left', 'right'); diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index b10785d730e..8fd8867058a 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; -import { $, addDisposableListener, append, EventHelper, EventType } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, EventHelper, EventType, isMouseEvent } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; @@ -56,7 +56,7 @@ class BaseDropdown extends ActionRunner { for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) { this._register(addDisposableListener(this._label, event, e => { - if (e instanceof MouseEvent && (e.detail > 1 || e.button !== 0)) { + if (isMouseEvent(e) && (e.detail > 1 || e.button !== 0)) { // prevent right click trigger to allow separate context menu (https://github.com/microsoft/vscode/issues/151064) // prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363) return; diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 3bb50780f93..898f8af3175 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDragAndDropData } from 'vs/base/browser/dnd'; -import { asCssValueWithDefault, createStyleSheet, Dimension, EventHelper } from 'vs/base/browser/dom'; +import { asCssValueWithDefault, createStyleSheet, Dimension, EventHelper, getActiveElement, isMouseEvent } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Gesture } from 'vs/base/browser/touch'; @@ -642,7 +642,7 @@ export function isSelectionRangeChangeEvent(event: IListMouseEvent | IListT } function isMouseRightClick(event: UIEvent): boolean { - return event instanceof MouseEvent && event.button === 2; + return isMouseEvent(event) && event.button === 2; } const DefaultMultipleSelectionController = { @@ -712,7 +712,7 @@ export class MouseController implements IDisposable { return; } - if (document.activeElement !== e.browserEvent.target) { + if (getActiveElement() !== e.browserEvent.target) { this.list.domFocus(); } } diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index b00899c50db..d1e52876883 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -192,9 +192,11 @@ export const nodeModulesPath: AppResourcePath = 'vs/../../node_modules'; export const nodeModulesAsarPath: AppResourcePath = 'vs/../../node_modules.asar'; export const nodeModulesAsarUnpackedPath: AppResourcePath = 'vs/../../node_modules.asar.unpacked'; +export const VSCODE_AUTHORITY = 'vscode-app'; + class FileAccessImpl { - private static readonly FALLBACK_AUTHORITY = 'vscode-app'; + private static readonly FALLBACK_AUTHORITY = VSCODE_AUTHORITY; /** * Returns a URI to use in contexts where the browser is responsible diff --git a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts index 40b3d5c46d3..a11a419bf55 100644 --- a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, IpcMainEvent, Menu, MenuItem } from 'electron'; +import { IpcMainEvent, Menu, MenuItem } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { CONTEXT_MENU_CHANNEL, CONTEXT_MENU_CLOSE_CHANNEL, IPopupOptions, ISerializableContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; @@ -12,7 +12,6 @@ export function registerContextMenuListener(): void { const menu = createMenu(event, onClickChannel, items); menu.popup({ - window: BrowserWindow.fromWebContents(event.sender) ?? undefined, x: options ? options.x : undefined, y: options ? options.y : undefined, positioningItem: options ? options.positioningItem : undefined, diff --git a/src/vs/base/parts/ipc/electron-main/ipcMain.ts b/src/vs/base/parts/ipc/electron-main/ipcMain.ts index 6fe43ab0b3a..c72b6e8e644 100644 --- a/src/vs/base/parts/ipc/electron-main/ipcMain.ts +++ b/src/vs/base/parts/ipc/electron-main/ipcMain.ts @@ -6,6 +6,7 @@ import { ipcMain as unsafeIpcMain, IpcMainEvent, IpcMainInvokeEvent } from 'electron'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; +import { VSCODE_AUTHORITY } from 'vs/base/common/network'; type ipcMainListener = (event: IpcMainEvent, ...args: any[]) => void; @@ -127,7 +128,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { return false; // unexpected URL } - if (host !== 'vscode-app') { + if (host !== VSCODE_AUTHORITY) { onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because of a bad origin of '${host}'.`); return false; // unexpected sender } diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index dfb9f0a342b..50ed9cf5e73 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { $, asCssValueWithDefault, h, multibyteAwareBtoa } from 'vs/base/browser/dom'; +import { $, asCssValueWithDefault, h, multibyteAwareBtoa, trackAttributes, copyAttributes } from 'vs/base/browser/dom'; +import { timeout } from 'vs/base/common/async'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; suite('dom', () => { test('hasClass', () => { @@ -18,8 +20,6 @@ suite('dom', () => { assert(!element.classList.contains('bar')); assert(!element.classList.contains('foo')); assert(!element.classList.contains('')); - - }); test('removeClass', () => { @@ -287,5 +287,53 @@ suite('dom', () => { assert.strictEqual(asCssValueWithDefault('var(--my-var, var(--my-var2))', 'blue'), 'var(--my-var, var(--my-var2, blue))'); }); + test('copyAttributes', () => { + const elementSource = document.createElement('div'); + elementSource.setAttribute('foo', 'bar'); + elementSource.setAttribute('bar', 'foo'); + const elementTarget = document.createElement('div'); + copyAttributes(elementSource, elementTarget); + + assert.strictEqual(elementTarget.getAttribute('foo'), 'bar'); + assert.strictEqual(elementTarget.getAttribute('bar'), 'foo'); + }); + + test('trackAttributes (unfiltered)', async () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const elementSource = document.createElement('div'); + const elementTarget = document.createElement('div'); + + const disposable = trackAttributes(elementSource, elementTarget); + + elementSource.setAttribute('foo', 'bar'); + elementSource.setAttribute('bar', 'foo'); + + await timeout(1); + + assert.strictEqual(elementTarget.getAttribute('foo'), 'bar'); + assert.strictEqual(elementTarget.getAttribute('bar'), 'foo'); + + disposable.dispose(); + }); + }); + + test('trackAttributes (filtered)', async () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const elementSource = document.createElement('div'); + const elementTarget = document.createElement('div'); + + const disposable = trackAttributes(elementSource, elementTarget, ['foo']); + + elementSource.setAttribute('foo', 'bar'); + elementSource.setAttribute('bar', 'foo'); + + await timeout(1); + + assert.strictEqual(elementTarget.getAttribute('foo'), 'bar'); + assert.strictEqual(elementTarget.getAttribute('bar'), null); + + disposable.dispose(); + }); + }); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f0b92aa81ed..b35bb6d7135 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -15,7 +15,7 @@ import { Event } from 'vs/base/common/event'; import { stripComments } from 'vs/base/common/json'; import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; +import { Schemas, VSCODE_AUTHORITY } from 'vs/base/common/network'; import { isAbsolute, join, posix } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; import { assertType } from 'vs/base/common/types'; @@ -84,8 +84,8 @@ import { NativeURLService } from 'vs/platform/url/common/urlService'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; -import { IWindowOpenable } from 'vs/platform/window/common/window'; -import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; +import { IWindowOpenable, IWindowSettings, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { ICodeWindow } from 'vs/platform/window/electron-main/window'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; @@ -145,7 +145,7 @@ export class CodeApplication extends Disposable { @IStateService private readonly stateService: IStateService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, - @IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService, + @IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService ) { super(); @@ -176,7 +176,7 @@ export class CodeApplication extends Disposable { return callback(allowedPermissionsInWebview.has(permission)); } - if (details.isMainFrame && details.securityOrigin === 'vscode-file://vscode-app/') { + if (details.isMainFrame && details.securityOrigin === `${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}/`) { return callback(allowedPermissionsInMainFrame.has(permission)); } @@ -188,7 +188,7 @@ export class CodeApplication extends Disposable { return allowedPermissionsInWebview.has(permission); } - if (details.isMainFrame && details.securityOrigin === 'vscode-file://vscode-app/') { + if (details.isMainFrame && details.securityOrigin === `${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}/`) { return allowedPermissionsInMainFrame.has(permission); } @@ -382,16 +382,50 @@ export class CodeApplication extends Disposable { // app.on('web-contents-created', (event, contents) => { + // Block any in-page navigation contents.on('will-navigate', event => { this.logService.error('webContents#will-navigate: Prevented webcontent navigation'); event.preventDefault(); }); - contents.setWindowOpenHandler(({ url }) => { - this.nativeHostMainService?.openExternal(undefined, url); + // Child Window: apply zoom after loading finished and handle --open-devtools + const isChildWindow = contents?.opener?.url.startsWith(`${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}/`); + if (isChildWindow) { + contents.on('dom-ready', () => { + const windowZoomLevel = this.configurationService.getValue('window')?.zoomLevel ?? 0; - return { action: 'deny' }; + contents.setZoomLevel(windowZoomLevel); + contents.setZoomFactor(zoomLevelToZoomFactor(windowZoomLevel)); + }); + + if (this.environmentMainService.args['open-devtools'] === true) { + contents.openDevTools({ mode: 'bottom' }); + } + } + + // All Windows: only allow about:blank child windows to open + // For all other URLs, delegate to the OS. + contents.setWindowOpenHandler(handler => { + + // about:blank windows can open as window witho our default options + if (handler.url === 'about:blank') { + this.logService.trace('webContents#setWindowOpenHandler: Allowing about:blank window to open'); + + return { + action: 'allow', + overrideBrowserWindowOptions: this.mainInstantiationService.invokeFunction(defaultBrowserWindowOptions) + }; + } + + // Any other URL: delegate to OS + else { + this.logService.trace(`webContents#setWindowOpenHandler: Prevented opening window with URL ${handler.url}}`); + + this.nativeHostMainService?.openExternal(undefined, handler.url); + + return { action: 'deny' }; + } }); }); diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index c2cc7511079..cc23b026471 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -464,7 +464,7 @@ class MouseDownOperation extends Disposable { (browserEvent?: MouseEvent | KeyboardEvent) => { const position = this._findMousePosition(this._lastMouseEvent!, false); - if (browserEvent && browserEvent instanceof KeyboardEvent) { + if (dom.isKeyboardEvent(browserEvent)) { // cancel this._viewController.emitMouseDropCanceled(); } else { diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index b94d9d44251..4c3b53a559f 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -40,6 +40,7 @@ class EditorScopedQuickInputService extends QuickInputService { _serviceBrand: undefined, get hasContainer() { return true; }, get container() { return widget.getDomNode(); }, + get activeContainer() { return widget.getDomNode(); }, get dimension() { return editor.getLayoutInfo(); }, get onDidLayout() { return editor.onDidLayoutChange; }, focus: () => editor.focus(), diff --git a/src/vs/editor/standalone/browser/standaloneLayoutService.ts b/src/vs/editor/standalone/browser/standaloneLayoutService.ts index 42b7b0786a3..0bc48a201aa 100644 --- a/src/vs/editor/standalone/browser/standaloneLayoutService.ts +++ b/src/vs/editor/standalone/browser/standaloneLayoutService.ts @@ -29,13 +29,19 @@ class StandaloneLayoutService implements ILayoutService { get container(): HTMLElement { // On a page, multiple editors can be created. Therefore, there are multiple containers, not - // just a single one. Please use `ICodeEditorService` to get the current focused code editor + // just a single one. Please use `activeContainer` to get the current focused code editor // and use its container if necessary. You can also instantiate `EditorScopedLayoutService` // which implements `ILayoutService` but is not a part of the service collection because // it is code editor instance specific. throw new Error(`ILayoutService.container is not available in the standalone editor!`); } + get activeContainer(): HTMLElement { + const activeCodeEditor = this._codeEditorService.getFocusedCodeEditor() ?? this._codeEditorService.getActiveCodeEditor(); + + return activeCodeEditor?.getContainerDomNode() ?? this.container; + } + focus(): void { this._codeEditorService.getFocusedCodeEditor()?.focus(); } diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index e878dad03ee..ea2e31cd603 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, isWebkitWebView } from 'vs/base/browser/browser'; -import { $, addDisposableListener } from 'vs/base/browser/dom'; +import { $, addDisposableListener, getActiveDocument } from 'vs/base/browser/dom'; import { DeferredPromise } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -96,10 +96,10 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer } // Fallback to textarea and execCommand solution + const activeDocument = getActiveDocument(); + const activeElement = activeDocument.activeElement; - const activeElement = document.activeElement; - - const textArea: HTMLTextAreaElement = document.body.appendChild($('textarea', { 'aria-hidden': true })); + const textArea: HTMLTextAreaElement = activeDocument.body.appendChild($('textarea', { 'aria-hidden': true })); textArea.style.height = '1px'; textArea.style.width = '1px'; textArea.style.position = 'absolute'; @@ -108,13 +108,13 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer textArea.focus(); textArea.select(); - document.execCommand('copy'); + activeDocument.execCommand('copy'); if (activeElement instanceof HTMLElement) { activeElement.focus(); } - document.body.removeChild(textArea); + activeDocument.body.removeChild(textArea); return; } diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 0f7e230d155..9b67cd2dfbe 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; -import { $, addDisposableListener, EventType, getActiveElement, isAncestor, isHTMLElement } from 'vs/base/browser/dom'; +import { $, addDisposableListener, EventType, getActiveElement, isAncestor } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Menu } from 'vs/base/browser/ui/menu/menu'; import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; @@ -49,7 +49,7 @@ export class ContextMenuHandler { let menu: Menu | undefined; - const shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined; + const shadowRootElement = delegate.domForShadowRoot instanceof HTMLElement ? delegate.domForShadowRoot : undefined; this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index 817b0750c47..ed305ef3881 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -41,8 +41,8 @@ export class ContextViewService extends Disposable implements IContextViewServic this.setContainer(container, shadowRoot ? ContextViewDOMPosition.FIXED_SHADOW : ContextViewDOMPosition.FIXED); } } else { - if (this.layoutService.hasContainer && this.container !== this.layoutService.container) { - this.container = this.layoutService.container; + if (this.layoutService.hasContainer && this.container !== this.layoutService.activeContainer) { + this.container = this.layoutService.activeContainer; this.setContainer(this.container, ContextViewDOMPosition.ABSOLUTE); } } diff --git a/src/vs/platform/layout/browser/layoutService.ts b/src/vs/platform/layout/browser/layoutService.ts index 73c42d38282..6d578341906 100644 --- a/src/vs/platform/layout/browser/layoutService.ts +++ b/src/vs/platform/layout/browser/layoutService.ts @@ -40,7 +40,7 @@ export interface ILayoutService { * **NOTE**: In the standalone editor case, multiple editors can be created on a page. * Therefore, in the standalone editor case, there are multiple containers, not just * a single one. If you ship code that needs a "container" for the standalone editor, - * please use `ICodeEditorService` to get the current focused code editor and use its + * please use `activeContainer` to get the current focused code editor and use its * container if necessary. You can also instantiate `EditorScopedLayoutService` * which implements `ILayoutService` but is not a part of the service collection because * it is code editor instance specific. @@ -48,6 +48,12 @@ export interface ILayoutService { */ readonly container: HTMLElement; + /** + * Active container of the application. When multiple windows are opened, will return + * the container of the active, focused window. + */ + readonly activeContainer: HTMLElement; + /** * An offset to use for positioning elements inside the container. */ diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index f0d944ddf06..4ada5b87e82 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createStyleSheet } from 'vs/base/browser/dom'; +import { createStyleSheet, isKeyboardEvent } from 'vs/base/browser/dom'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedListOptions, IPagedRenderer, PagedList } from 'vs/base/browser/ui/list/listPaging'; @@ -698,7 +698,7 @@ abstract class ResourceNavigator extends Disposable { ) { super(); - this._register(Event.filter(this.widget.onDidChangeSelection, e => e.browserEvent instanceof KeyboardEvent)(e => this.onSelectionFromKeyboard(e))); + this._register(Event.filter(this.widget.onDidChangeSelection, e => isKeyboardEvent(e.browserEvent))(e => this.onSelectionFromKeyboard(e))); this._register(this.widget.onPointer((e: { browserEvent: MouseEvent; element: T | undefined }) => this.onPointer(e.element, e.browserEvent))); this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent; element: T | undefined }) => this.onMouseDblClick(e.element, e.browserEvent))); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 053f3822d27..eac0530b40c 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -742,6 +742,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const contents = window.win.webContents; contents.toggleDevTools(); } + + for (const browserWindow of BrowserWindow.getAllWindows()) { + if (browserWindow.webContents.getURL() === 'about:blank') { + browserWindow.webContents.toggleDevTools(); + } + } } async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise { diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 4258cd0640a..8cc756bda87 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -938,7 +938,7 @@ export class QuickPick extends QuickInput implements I this._selectedItems = selectedItems as T[]; this.onDidChangeSelectionEmitter.fire(selectedItems as T[]); if (selectedItems.length) { - this.handleAccept(event instanceof MouseEvent && event.button === 1 /* mouse middle click */); + this.handleAccept(dom.isMouseEvent(event) && event.button === 1 /* mouse middle click */); } })); this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index ab87e5ac778..2a07fd4562a 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, BrowserWindowConstructorOptions, Display, Event as ElectronEvent, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; +import { app, BrowserWindow, Display, Event as ElectronEvent, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { join } from 'vs/base/common/path'; import { getMarks, mark } from 'vs/base/common/performance'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; @@ -32,8 +31,8 @@ import { IApplicationStorageMainService, IStorageMainService } from 'vs/platform import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/base/common/themables'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { getMenuBarVisibility, getTitleBarStyle, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, useWindowControlsOverlay, WindowMinimumSize, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; -import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; +import { getMenuBarVisibility, getTitleBarStyle, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, useWindowControlsOverlay } from 'vs/platform/window/common/window'; +import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IWindowState, ICodeWindow, ILoadEvent, WindowMode, WindowError, LoadReason, defaultWindowState } from 'vs/platform/window/electron-main/window'; @@ -44,6 +43,7 @@ import { IStateService } from 'vs/platform/state/node/state'; import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; import { firstOrDefault } from 'vs/base/common/arrays'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export interface IWindowCreationOptions { readonly state: IWindowState; @@ -192,7 +192,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IProductService private readonly productService: IProductService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IStateService private readonly stateService: IStateService + @IStateService private readonly stateService: IStateService, + @IInstantiationService instantiationService: IInstantiationService ) { super(); @@ -209,53 +210,19 @@ export class CodeWindow extends Disposable implements ICodeWindow { const windowSettings = this.configurationService.getValue('window'); - const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { - width: this.windowState.width, - height: this.windowState.height, - x: this.windowState.x, - y: this.windowState.y, - backgroundColor: this.themeMainService.getBackgroundColor(), - minWidth: WindowMinimumSize.WIDTH, - minHeight: WindowMinimumSize.HEIGHT, + const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, { show: !isFullscreenOrMaximized, // reduce flicker by showing later - title: this.productService.nameLong, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', - enableWebSQL: false, - spellcheck: false, - zoomFactor: zoomLevelToZoomFactor(windowSettings?.zoomLevel), - autoplayPolicy: 'user-gesture-required', - // Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624 - // Refs https://github.com/microsoft/vscode/issues/140098 - enableBlinkFeatures: 'HighlightAPI', - sandbox: true - }, - experimentalDarkMode: true - }; - - // Apply icon to window - // Linux: always - // Windows: only when running out of sources, otherwise an icon is set by us on the executable - if (isLinux) { - options.icon = join(this.environmentMainService.appRoot, 'resources/linux/code.png'); - } else if (isWindows && !this.environmentMainService.isBuilt) { - options.icon = join(this.environmentMainService.appRoot, 'resources/win32/code_150x150.png'); - } + } + }); if (isMacintosh && !this.useNativeFullScreen()) { options.fullscreenable = false; // enables simple fullscreen mode } - if (isMacintosh) { - options.acceptFirstMouse = true; // enabled by default - - if (windowSettings?.clickThroughInactive === false) { - options.acceptFirstMouse = false; - } - } - const useNativeTabs = isMacintosh && windowSettings?.nativeTabs === true; if (useNativeTabs) { options.tabbingIdentifier = this.productService.nameShort; // this opts in to sierra tabs diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 1d65cf3b492..41972e7c713 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -3,14 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WebContents } from 'electron'; +import { BrowserWindowConstructorOptions, WebContents } from 'electron'; import { Event } from 'vs/base/common/event'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeWindow } from 'vs/platform/window/electron-main/window'; -import { IOpenEmptyWindowOptions, IWindowOpenable } from 'vs/platform/window/common/window'; +import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeWindow, defaultWindowState } from 'vs/platform/window/electron-main/window'; +import { IOpenEmptyWindowOptions, IWindowOpenable, IWindowSettings, WindowMinimumSize, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { join } from 'vs/base/common/path'; export const IWindowsMainService = createDecorator('windowsMainService'); @@ -103,3 +108,52 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration { } export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } + +export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState = defaultWindowState(), overrides?: BrowserWindowConstructorOptions): BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { + const themeMainService = accessor.get(IThemeMainService); + const productService = accessor.get(IProductService); + const configurationService = accessor.get(IConfigurationService); + const environmentMainService = accessor.get(IEnvironmentMainService); + + const windowSettings = configurationService.getValue('window'); + + const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { + width: windowState.width, + height: windowState.height, + x: windowState.x, + y: windowState.y, + backgroundColor: themeMainService.getBackgroundColor(), + minWidth: WindowMinimumSize.WIDTH, + minHeight: WindowMinimumSize.HEIGHT, + title: productService.nameLong, + ...overrides, + webPreferences: { + enableWebSQL: false, + spellcheck: false, + zoomFactor: zoomLevelToZoomFactor(windowSettings?.zoomLevel), + autoplayPolicy: 'user-gesture-required', + // Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624 + // Refs https://github.com/microsoft/vscode/issues/140098 + enableBlinkFeatures: 'HighlightAPI', + ...overrides?.webPreferences, + sandbox: true + }, + experimentalDarkMode: true + }; + + if (isLinux) { + options.icon = join(environmentMainService.appRoot, 'resources/linux/code.png'); // always on Linux + } else if (isWindows && !environmentMainService.isBuilt) { + options.icon = join(environmentMainService.appRoot, 'resources/win32/code_150x150.png'); // only when running out of sources on Windows + } + + if (isMacintosh) { + options.acceptFirstMouse = true; // enabled by default + + if (windowSettings?.clickThroughInactive === false) { + options.acceptFirstMouse = false; + } + } + + return options; +} diff --git a/src/vs/workbench/api/worker/extensionHostWorker.ts b/src/vs/workbench/api/worker/extensionHostWorker.ts index 6e6f8845da5..86bccef83c3 100644 --- a/src/vs/workbench/api/worker/extensionHostWorker.ts +++ b/src/vs/workbench/api/worker/extensionHostWorker.ts @@ -15,7 +15,7 @@ import * as performance from 'vs/base/common/performance'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/worker/extHost.worker.services'; -import { FileAccess } from 'vs/base/common/network'; +import { FileAccess, Schemas, VSCODE_AUTHORITY } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; //#region --- Define, capture, and override some globals @@ -109,7 +109,7 @@ if ((self).Worker) { const bootstrapFnSource = (function bootstrapFn(workerUrl: string) { function asWorkerBrowserUrl(url: string | URL | TrustedScriptURL): any { if (typeof url === 'string' || url instanceof URL) { - return String(url).replace(/^file:\/\//i, 'vscode-file://vscode-app'); + return String(url).replace(/^file:\/\//i, `${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}`); } return url; } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 6c921e088a9..a0aaa04d13f 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -30,7 +30,6 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ResourceMap } from 'vs/base/common/map'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; -import { isHTMLElement } from 'vs/base/browser/dom'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -409,7 +408,7 @@ class BlurAction extends Action2 { run(): void { const el = document.activeElement; - if (isHTMLElement(el)) { + if (el instanceof HTMLElement) { el.blur(); } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index bef9b44215c..29decb51d82 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; @@ -154,6 +154,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi readonly hasContainer = true; readonly container = document.createElement('div'); + get activeContainer() { + const activeDocument = getActiveDocument(); + if (document === activeDocument) { + return this.container; + } else { + return activeDocument.body.children[0] as HTMLElement; // TODO@bpasero a bit of a hack + } + } private _dimension!: IDimension; get dimension(): IDimension { return this._dimension; } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 886e023514d..f3000e493a7 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/activityaction'; import { localize } from 'vs/nls'; -import { EventType, addDisposableListener, EventHelper, append, $, clearNode, hide, show } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, EventHelper, append, $, clearNode, hide, show, isMouseEvent } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { Action, IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; @@ -66,7 +66,7 @@ export class ViewContainerActivityAction extends ActivityAction { } override async run(event: { preserveFocus: boolean }): Promise { - if (event instanceof MouseEvent && event.button === 2) { + if (isMouseEvent(event) && event.button === 2) { return; // do not run on right click } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1056ed9c09d..7289ba1aca6 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -41,7 +41,7 @@ import { QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReOpenInTextEditorAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction, ToggleEditorTypeAction, SplitEditorToAboveGroupAction, SplitEditorToBelowGroupAction, SplitEditorToFirstGroupAction, SplitEditorToLastGroupAction, SplitEditorToLeftGroupAction, SplitEditorToNextGroupAction, SplitEditorToPreviousGroupAction, SplitEditorToRightGroupAction, NavigateForwardInEditsAction, - NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction + NavigateBackwardsInEditsAction, NavigateForwardInNavigationsAction, NavigateBackwardsInNavigationsAction, NavigatePreviousInNavigationsAction, NavigatePreviousInEditsAction, NavigateToLastNavigationLocationAction, ExperimentalMoveEditorIntoNewWindowAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_EDITOR_GROUP_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, @@ -67,6 +67,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration'; import { ToggleSeparatePinnedTabsAction, ToggleTabsVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; +import product from 'vs/platform/product/common/product'; //#region Editor Registrations @@ -165,12 +166,10 @@ quickAccessRegistry.registerQuickAccessProvider({ //#region Actions & Commands -// Editor Status registerAction2(ChangeLanguageAction); registerAction2(ChangeEOLAction); registerAction2(ChangeEncodingAction); -// Editor Management (new style) registerAction2(NavigateForwardAction); registerAction2(NavigateBackwardsAction); @@ -293,6 +292,11 @@ registerAction2(QuickAccessPreviousRecentlyUsedEditorInGroupAction); registerAction2(QuickAccessLeastRecentlyUsedEditorInGroupAction); registerAction2(QuickAccessPreviousEditorFromHistoryAction); +if (product.quality !== 'stable') { + // TODO@bpasero revisit + registerAction2(ExperimentalMoveEditorIntoNewWindowAction); +} + const quickAccessNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickAccessNavigateNextInEditorPickerId, @@ -315,7 +319,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ registerEditorCommands(); -//#endregion Workbench Actions +//#endregion //#region Menus diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index d6511cea131..a2a26d6f7ad 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -18,7 +18,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; export interface IEditorPartCreationOptions { - restorePreviousState: boolean; + readonly restorePreviousState: boolean; } export const DEFAULT_EDITOR_MIN_DIMENSIONS = new Dimension(220, 70); @@ -88,6 +88,26 @@ export function getEditorPartOptions(configurationService: IConfigurationService return options; } +/** + * A helper to access editor groups across all opened editor parts. + */ +export interface IEditorPartsView { + + /** + * An array of all editor groups across all editor parts. + */ + readonly groups: IEditorGroupView[]; + + /** + * Get the group based on an identifier across all opened + * editor parts. + */ + getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined; +} + +/** + * A helper to access and mutate editor groups within an editor part. + */ export interface IEditorGroupsView { readonly groups: IEditorGroupView[]; @@ -120,7 +140,7 @@ export interface IEditorGroupTitleHeight { /** * The overall height of the editor group title control. */ - total: number; + readonly total: number; /** * The height offset to e.g. use when drawing drop overlays. @@ -128,9 +148,12 @@ export interface IEditorGroupTitleHeight { * decides to have an `offset` that is within the title control * (e.g. when breadcrumbs are enabled). */ - offset: number; + readonly offset: number; } +/** + * A helper to access and mutate an editor group within an editor part. + */ export interface IEditorGroupView extends IDisposable, ISerializableView, IEditorGroup { readonly onDidFocus: Event; @@ -195,7 +218,7 @@ export interface IInternalEditorTitleControlOptions { * A hint to defer updating the title control for perf reasons. * The caller must ensure to update the title control then. */ - skipTitleUpdate?: boolean; + readonly skipTitleUpdate?: boolean; } export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlOptions { @@ -207,12 +230,12 @@ export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlO * not be considered as matching, even if the editor is * opened in one of the sides. */ - supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; + readonly supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; /** * When set to `true`, pass DOM focus into the tab control. */ - focusTabControl?: boolean; + readonly focusTabControl?: boolean; } export interface IInternalEditorCloseOptions extends IInternalEditorTitleControlOptions { @@ -221,12 +244,12 @@ export interface IInternalEditorCloseOptions extends IInternalEditorTitleControl * A hint that the editor is closed due to an error opening. This can be * used to optimize how error toasts are appearing if any. */ - fromError?: boolean; + readonly fromError?: boolean; /** * Additional context as to why an editor is closed. */ - context?: EditorCloseContext; + readonly context?: EditorCloseContext; } export interface IInternalMoveCopyOptions extends IInternalEditorOpenOptions { @@ -234,5 +257,5 @@ export interface IInternalMoveCopyOptions extends IInternalEditorOpenOptions { /** * Whether to close the editor at the source or keep it. */ - keepCopy?: boolean; + readonly keepCopy?: boolean; } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 87f58869e95..2abc360ac74 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -33,7 +33,7 @@ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ActiveEditorAvailableEditorIdsContext, ActiveEditorGroupEmptyContext } from 'vs/workbench/common/contextkeys'; +import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext } from 'vs/workbench/common/contextkeys'; class ExecuteCommandAction extends Action2 { @@ -2420,3 +2420,38 @@ export class ReOpenInTextEditorAction extends Action2 { ], activeEditorPane.group); } } + +export class ExperimentalMoveEditorIntoNewWindowAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.experimentalMoveEditorIntoNewWindowAction', + title: { + value: localize('popEditorOut', "Move Active Editor into a New Window (Experimental)"), + mnemonicTitle: localize({ key: 'miPopEditorOut', comment: ['&& denotes a mnemonic'] }, "&&Move Active Editor into a New Window (Experimental)"), + original: 'Move Active Editor into a New Window (Experimental)' + }, + category: Categories.View, + precondition: ActiveEditorContext, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); + + const activeEditor = editorService.activeEditor; + if (!activeEditor) { + return; + } + + const auxiliaryEditorPart = editorGroupService.createAuxiliaryEditorPart(); + + await auxiliaryEditorPart.activeGroup.openEditor(activeEditor, { + pinned: true, + viewState: activeEditor.toUntyped({ preserveViewState: editorGroupService.activeGroup.id })?.options?.viewState, + }); + await editorGroupService.activeGroup.closeEditor(activeEditor); + } +} diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 3541d64c29e..a02a2e3491d 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -40,6 +40,7 @@ import { extname } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { getActiveElement } from 'vs/base/browser/dom'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -1502,7 +1503,7 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon // First check for a focused list to return the selected items from const list = listService.lastFocusedList; - if (list instanceof List && list.getHTMLElement() === document.activeElement) { + if (list instanceof List && list.getHTMLElement() === getActiveElement()) { const elementToContext = (element: IEditorIdentifier | IEditorGroup) => { if (isEditorGroup(element)) { return { groupId: element.id, editorIndex: undefined }; diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index be6231c1a95..db792c89da9 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -20,10 +20,10 @@ import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CodeDataTransfers, containsDragType, Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, extractTreeDropData, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; -import { fillActiveEditorViewState, IEditorGroupsView, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { fillActiveEditorViewState, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from 'vs/workbench/common/theme'; -import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; @@ -62,8 +62,7 @@ class DropOverlay extends Themable { private readonly enableDropIntoEditor: boolean; constructor( - private groupsView: IEditorGroupsView, - private groupView: IEditorGroupView, + private readonly groupView: IEditorGroupView, @IThemeService themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -231,13 +230,13 @@ class DropOverlay extends Themable { return !!this.groupView.activeEditor?.hasCapability(EditorInputCapabilities.CanDropIntoEditor); } - private findSourceGroupView(): IEditorGroupView | undefined { + private findSourceGroupView(): IEditorGroup | undefined { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { - return this.groupsView.getGroup(data[0].identifier); + return this.editorGroupService.getGroup(data[0].identifier); } } @@ -245,7 +244,7 @@ class DropOverlay extends Themable { else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { - return this.groupsView.getGroup(data[0].identifier.groupId); + return this.editorGroupService.getGroup(data[0].identifier.groupId); } } @@ -256,9 +255,9 @@ class DropOverlay extends Themable { // Determine target group const ensureTargetGroup = () => { - let targetGroup: IEditorGroupView; + let targetGroup: IEditorGroup; if (typeof splitDirection === 'number') { - targetGroup = this.groupsView.addGroup(this.groupView, splitDirection); + targetGroup = this.editorGroupService.addGroup(this.groupView, splitDirection); } else { targetGroup = this.groupView; } @@ -270,19 +269,19 @@ class DropOverlay extends Themable { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { - const sourceGroup = this.groupsView.getGroup(data[0].identifier); + const sourceGroup = this.editorGroupService.getGroup(data[0].identifier); if (sourceGroup) { if (typeof splitDirection !== 'number' && sourceGroup === this.groupView) { return; } // Split to new group - let targetGroup: IEditorGroupView | undefined; + let targetGroup: IEditorGroup | undefined; if (typeof splitDirection === 'number') { if (this.isCopyOperation(event)) { - targetGroup = this.groupsView.copyGroup(sourceGroup, this.groupView, splitDirection); + targetGroup = this.editorGroupService.copyGroup(sourceGroup, this.groupView, splitDirection); } else { - targetGroup = this.groupsView.moveGroup(sourceGroup, this.groupView, splitDirection); + targetGroup = this.editorGroupService.moveGroup(sourceGroup, this.groupView, splitDirection); } } @@ -293,11 +292,11 @@ class DropOverlay extends Themable { mergeGroupOptions = { mode: MergeGroupMode.COPY_EDITORS }; } - this.groupsView.mergeGroup(sourceGroup, this.groupView, mergeGroupOptions); + this.editorGroupService.mergeGroup(sourceGroup, this.groupView, mergeGroupOptions); } if (targetGroup) { - this.groupsView.activateGroup(targetGroup); + this.editorGroupService.activateGroup(targetGroup); } } @@ -311,16 +310,16 @@ class DropOverlay extends Themable { if (Array.isArray(data)) { const draggedEditor = data[0].identifier; - const sourceGroup = this.groupsView.getGroup(draggedEditor.groupId); + const sourceGroup = this.editorGroupService.getGroup(draggedEditor.groupId); if (sourceGroup) { const copyEditor = this.isCopyOperation(event, draggedEditor); - let targetGroup: IEditorGroupView | undefined = undefined; + let targetGroup: IEditorGroup | undefined = undefined; // Optimization: if we move the last editor of an editor group // and we are configured to close empty editor groups, we can // rather move the entire editor group according to the direction if (this.editorGroupService.partOptions.closeEmptyGroups && sourceGroup.count === 1 && typeof splitDirection === 'number' && !copyEditor) { - targetGroup = this.groupsView.moveGroup(sourceGroup, this.groupView, splitDirection); + targetGroup = this.editorGroupService.moveGroup(sourceGroup, this.groupView, splitDirection); } // In any other case do a normal move/copy operation @@ -391,7 +390,7 @@ class DropOverlay extends Themable { } private positionOverlay(mousePosX: number, mousePosY: number, isDraggingGroup: boolean, enableSplitting: boolean): void { - const preferSplitVertically = this.groupsView.partOptions.openSideBySideDirection === 'right'; + const preferSplitVertically = this.editorGroupService.partOptions.openSideBySideDirection === 'right'; const editorControlWidth = this.groupView.element.clientWidth; const editorControlHeight = this.groupView.element.clientHeight - this.getOverlayOffsetHeight(); @@ -531,7 +530,7 @@ class DropOverlay extends Themable { private getOverlayOffsetHeight(): number { // With tabs and opened editors: use the area below tabs as drop target - if (!this.groupView.isEmpty && this.groupsView.partOptions.showTabs) { + if (!this.groupView.isEmpty && this.editorGroupService.partOptions.showTabs) { return this.groupView.titleHeight.offset; } @@ -569,14 +568,6 @@ class DropOverlay extends Themable { } } -export interface IEditorDropTargetDelegate { - - /** - * A helper to figure out if the drop target contains the provided group. - */ - containsGroup?(groupView: IEditorGroupView): boolean; -} - export class EditorDropTarget extends Themable { private _overlay?: DropOverlay; @@ -587,9 +578,9 @@ export class EditorDropTarget extends Themable { private readonly groupTransfer = LocalSelectionTransfer.getInstance(); constructor( - private groupsView: IEditorGroupsView, - private container: HTMLElement, + private readonly container: HTMLElement, private readonly delegate: IEditorDropTargetDelegate, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IThemeService themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService @@ -649,7 +640,7 @@ export class EditorDropTarget extends Themable { if (!this.overlay) { const targetGroupView = this.findTargetGroupView(target); if (targetGroupView) { - this._overlay = this.instantiationService.createInstance(DropOverlay, this.groupsView, targetGroupView); + this._overlay = this.instantiationService.createInstance(DropOverlay, targetGroupView); } } } @@ -671,7 +662,7 @@ export class EditorDropTarget extends Themable { } private findTargetGroupView(child: HTMLElement): IEditorGroupView | undefined { - const groups = this.groupsView.groups; + const groups = this.editorGroupService.groups as IEditorGroupView[]; return groups.find(groupView => isAncestor(child, groupView.element) || this.delegate.containsGroup?.(groupView)); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 2d5528e845e..495b28c2684 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -28,7 +28,7 @@ import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction } from 'vs/base/common/actions'; @@ -58,16 +58,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region factory - static createNew(groupsView: IEditorGroupsView, index: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, groupsView, null, index); + static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex); } - static createFromSerialized(serialized: ISerializedEditorGroupModel, groupsView: IEditorGroupsView, index: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, groupsView, serialized, index); + static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex); } - static createCopy(copyFrom: IEditorGroupView, groupsView: IEditorGroupsView, index: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, groupsView, copyFrom, index); + static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex); } //#endregion @@ -133,8 +133,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { readonly whenRestored = this.whenRestoredPromise.p; constructor( - private groupsView: IEditorGroupsView, from: IEditorGroupView | ISerializedEditorGroupModel | null, + private readonly editorPartsView: IEditorPartsView, + private readonly groupsView: IEditorGroupsView, + private readonly groupsLabel: string, private _index: number, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -198,7 +200,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.appendChild(this.titleContainer); // Title control - this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.groupsView, this, this.model)); + this.titleControl = this._register(this.scopedInstantiationService.createInstance(EditorTitleControl, this.titleContainer, this.editorPartsView, this.groupsView, this, this.model)); // Editor container this.editorContainer = document.createElement('div'); @@ -381,7 +383,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Find target anchor let anchor: HTMLElement | StandardMouseEvent = this.element; - if (e instanceof MouseEvent) { + if (e) { anchor = new StandardMouseEvent(e); } @@ -402,14 +404,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const containerFocusTracker = this._register(trackFocus(this.element)); this._register(containerFocusTracker.onDidFocus(() => { if (this.isEmpty) { - this._onDidFocus.fire(); // only when empty to prevent accident focus + this._onDidFocus.fire(); // only when empty to prevent duplicate events from `editorPane.onDidFocus` } })); // Title Container const handleTitleClickOrTouch = (e: MouseEvent | GestureEvent): void => { let target: HTMLElement; - if (e instanceof MouseEvent) { + if (isMouseEvent(e)) { if (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */)) { return undefined; } @@ -609,7 +611,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private canDispose(editor: EditorInput): boolean { - for (const groupView of this.groupsView.groups) { + for (const groupView of this.editorPartsView.groups) { if (groupView instanceof EditorGroupView && groupView.model.contains(editor, { strictEquals: true, // only if this input is not shared across editor groups supportSideBySide: SideBySideEditor.ANY // include any side of an opened side by side editor @@ -750,6 +752,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } get label(): string { + if (this.groupsLabel) { + return localize('groupLabelLong', "{0}: Group {1}", this.groupsLabel, this._index + 1); + } + return localize('groupLabel', "Group {0}", this._index + 1); } @@ -796,8 +802,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#endregion - //#region IEditorGroup - //#region basics() get id(): GroupIdentifier { diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 25a821e43d7..0f26fa1aa20 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -220,11 +220,11 @@ export class EditorMemento extends Disposable implements IEditorMemento { constructor( readonly id: string, - private key: string, - private memento: MementoObject, - private limit: number, - private editorGroupService: IEditorGroupsService, - private configurationService: ITextResourceConfigurationService + private readonly key: string, + private readonly memento: MementoObject, + private readonly limit: number, + private readonly editorGroupService: IEditorGroupsService, + private readonly configurationService: ITextResourceConfigurationService ) { super(); diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index e333dc4b10e..8860623bde1 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -91,9 +91,9 @@ export class EditorPanes extends Disposable { private readonly editorPanesRegistry = Registry.as(EditorExtensions.EditorPane); constructor( - private editorGroupParent: HTMLElement, - private editorPanesParent: HTMLElement, - private groupView: IEditorGroupView, + private readonly editorGroupParent: HTMLElement, + private readonly editorPanesParent: HTMLElement, + private readonly groupView: IEditorGroupView, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 1c46734e806..98a6ebf94f2 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -3,31 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; import { Dimension, isAncestor, $, EventHelper, addDisposableGenericMouseDownListener } from 'vs/base/browser/dom'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; -import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorGroupsService, IEditorSideGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IAuxiliaryEditorPart, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, ISerializedNode, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid'; import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { IEditorGroupsView, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ISerializedEditorGroupModel, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; -import { EditorDropTarget, IEditorDropTargetDelegate } from 'vs/workbench/browser/parts/editor/editorDropTarget'; -import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; +import { EditorDropTarget } from 'vs/workbench/browser/parts/editor/editorDropTarget'; import { Color } from 'vs/base/common/color'; import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Parts, IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, assertType } from 'vs/base/common/types'; import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; import { DeferredPromise, Promises } from 'vs/base/common/async'; import { findGroup } from 'vs/workbench/services/editor/common/editorGroupFinder'; @@ -35,9 +34,9 @@ import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; interface IEditorPartUIState { - serializedGrid: ISerializedGrid; - activeGroup: GroupIdentifier; - mostRecentActiveGroups: GroupIdentifier[]; + readonly serializedGrid: ISerializedGrid; + readonly activeGroup: GroupIdentifier; + readonly mostRecentActiveGroups: GroupIdentifier[]; } class GridWidgetView implements IView { @@ -80,15 +79,16 @@ class GridWidgetView implements IView { } } -export class EditorPart extends Part implements IEditorGroupsService, IEditorGroupsView, IEditorDropService { - - declare readonly _serviceBrand: undefined; +export class EditorPart extends Part implements IEditorPart { private static readonly EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.state'; private static readonly EDITOR_PART_CENTERED_VIEW_STORAGE_KEY = 'editorpart.centeredview'; //#region Events + private readonly _onDidFocus = this._register(new Emitter()); + readonly onDidFocus = this._onDidFocus.event; + private readonly _onDidLayout = this._register(new Emitter()); readonly onDidLayout = this._onDidLayout.event; @@ -140,13 +140,16 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro private readonly gridWidgetView = this._register(new GridWidgetView()); constructor( + private readonly editorPartsView: IEditorPartsView, + id: string, + private readonly groupsLabel: string, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService ) { - super(Parts.EDITOR_PART, { hasTitle: false }, themeService, storageService, layoutService); + super(id, { hasTitle: false }, themeService, storageService, layoutService); this.registerListeners(); } @@ -175,8 +178,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this._onDidChangeEditorPartOptions.fire({ oldPartOptions, newPartOptions }); } - //#region IEditorGroupsService - private enforcedPartOptions: IEditorPartOptions[] = []; private _partOptions = getEditorPartOptions(this.configurationService, this.themeService); @@ -266,6 +267,10 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } } + hasGroup(identifier: GroupIdentifier): boolean { + return this.groupViews.has(identifier); + } + getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined { return this.groupViews.get(identifier); } @@ -573,11 +578,11 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro // Create group view let groupView: IEditorGroupView; if (from instanceof EditorGroupView) { - groupView = EditorGroupView.createCopy(from, this, this.count, this.instantiationService); + groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.instantiationService); } else if (isSerializedEditorGroupModel(from)) { - groupView = EditorGroupView.createFromSerialized(from, this, this.count, this.instantiationService); + groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.instantiationService); } else { - groupView = EditorGroupView.createNew(this, this.count, this.instantiationService); + groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.instantiationService); } // Keep in map @@ -587,6 +592,8 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro const groupDisposables = new DisposableStore(); groupDisposables.add(groupView.onDidFocus(() => { this.doSetGroupActive(groupView); + + this._onDidFocus.fire(); })); // Track group changes @@ -844,7 +851,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro private assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { let groupView: IEditorGroupView | undefined; if (typeof group === 'number') { - groupView = this.getGroup(group); + groupView = this.editorPartsView.getGroup(group); } else { groupView = group; } @@ -856,16 +863,12 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro return groupView; } - //#endregion + createEditorDropTarget(container: unknown, delegate: IEditorDropTargetDelegate): IDisposable { + assertType(container instanceof HTMLElement); - //#region IEditorDropService - - createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { - return this.instantiationService.createInstance(EditorDropTarget, this, container, delegate); + return this.instantiationService.createInstance(EditorDropTarget, container, delegate); } - //#endregion - //#region Part // TODO @sbatten @joao find something better to prevent editor taking over #79897 @@ -1071,9 +1074,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro onUnexpectedError(new Error(`Error restoring editor grid widget: ${error} (with state: ${JSON.stringify(uiState)})`)); // Clear any state we have from the failing restore - this.groupViews.forEach(group => group.dispose()); - this.groupViews.clear(); - this.mostRecentActiveGroups = []; + this.disposeGroups(); return false; // failure } @@ -1222,11 +1223,21 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro }; } + private disposeGroups(): void { + for (const group of this.groups) { + group.dispose(); + + this._onDidRemoveGroup.fire(group); + } + + this.groupViews.clear(); + this.mostRecentActiveGroups = []; + } + override dispose(): void { // Forward to all groups - this.groupViews.forEach(group => group.dispose()); - this.groupViews.clear(); + this.disposeGroups(); // Grid widget this.gridWidget?.dispose(); @@ -1237,16 +1248,45 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro //#endregion } -class EditorDropService implements IEditorDropService { +export class MainEditorPart extends EditorPart { - declare readonly _serviceBrand: undefined; - - constructor(@IEditorGroupsService private readonly editorPart: EditorPart) { } - - createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { - return this.editorPart.createEditorDropTarget(container, delegate); + constructor( + editorPartsView: IEditorPartsView, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + super(editorPartsView, Parts.EDITOR_PART, '', instantiationService, themeService, configurationService, storageService, layoutService); } } -registerSingleton(IEditorGroupsService, EditorPart, InstantiationType.Eager); -registerSingleton(IEditorDropService, EditorDropService, InstantiationType.Delayed); +export class AuxiliaryEditorPart extends EditorPart implements IAuxiliaryEditorPart { + + private static COUNTER = 0; + + private readonly _onDidClose = this._register(new Emitter()); + readonly onDidClose = this._onDidClose.event; + + constructor( + editorPartsView: IEditorPartsView, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService + ) { + const id = AuxiliaryEditorPart.COUNTER++; + super(editorPartsView, `workbench.parts.auxiliaryEditor.${id}`, localize('auxiliaryEditorPartLabel', "Window {0}", id + 1), instantiationService, themeService, configurationService, storageService, layoutService); + } + + protected override saveState(): void { + return; // TODO@bpasero support auxiliary editor state + } + + async close(): Promise { + // TODO@bpasero this needs full support for closing all editors, handling vetos and showing dialogs + this._onDidClose.fire(); + } +} diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts new file mode 100644 index 00000000000..9147cdac8d7 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -0,0 +1,324 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDimension, getActiveDocument } from 'vs/base/browser/dom'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { GroupIdentifier, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { AuxiliaryEditorPart, EditorPart, MainEditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { IEditorGroupView, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; + +export class EditorParts extends Disposable implements IEditorGroupsService, IEditorPartsView { + + declare readonly _serviceBrand: undefined; + + private readonly mainEditorPart = this._register(this.createMainEditorPart()); + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService + ) { + super(); + + this._register(this.registerEditorPart(this.mainEditorPart)); + } + + protected createMainEditorPart(): MainEditorPart { + return this.instantiationService.createInstance(MainEditorPart, this); + } + + //#region Auxiliary Editor Parts + + createAuxiliaryEditorPart(): IAuxiliaryEditorPart { + const disposables = new DisposableStore(); + + const auxiliaryWindow = disposables.add(this.auxiliaryWindowService.open()); + disposables.add(Event.once(auxiliaryWindow.onDidClose)(() => disposables.dispose())); + + const partContainer = document.createElement('div'); + partContainer.classList.add('part', 'editor'); + partContainer.setAttribute('role', 'main'); + auxiliaryWindow.container.appendChild(partContainer); + + const editorPart = disposables.add(this.instantiationService.createInstance(AuxiliaryEditorPart, this)); + disposables.add(this.registerEditorPart(editorPart)); + disposables.add(Event.once(editorPart.onDidClose)(() => disposables.dispose())); + + editorPart.create(partContainer, { restorePreviousState: false }); + disposables.add(auxiliaryWindow.onDidResize(dimension => editorPart.layout(dimension.width, dimension.height, 0, 0))); + + this._onDidAddGroup.fire(editorPart.activeGroup); + + return editorPart; + } + + //#endregion + + //#region Registration + + private readonly parts = new Set(); + + private registerEditorPart(part: EditorPart): IDisposable { + this.parts.add(part); + + const disposables = this._register(new DisposableStore()); + disposables.add(toDisposable(() => this.parts.delete(part))); + + this.registerEditorPartListeners(part, disposables); + + return disposables; + } + + private registerEditorPartListeners(part: EditorPart, disposables: DisposableStore): void { + disposables.add(part.onDidFocus(() => { + if (this.parts.size > 1) { + this._onDidActiveGroupChange.fire(this.activeGroup); // this can only happen when we have more than 1 editor part + } + })); + + disposables.add(part.onDidChangeActiveGroup(group => this._onDidActiveGroupChange.fire(group))); + disposables.add(part.onDidAddGroup(group => this._onDidAddGroup.fire(group))); + disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group))); + disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group))); + disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group))); + + disposables.add(part.onDidLayout(dimension => this._onDidLayout.fire(dimension))); + disposables.add(part.onDidScroll(() => this._onDidScroll.fire())); + + disposables.add(part.onDidChangeGroupIndex(group => this._onDidChangeGroupIndex.fire(group))); + disposables.add(part.onDidChangeGroupLocked(group => this._onDidChangeGroupLocked.fire(group))); + } + + //#endregion + + //#region Helpers + + get activePart(): EditorPart { + return this.getPartByDocument(getActiveDocument()); + } + + private getPartByDocument(document: Document): EditorPart { + if (this.parts.size > 1) { + for (const part of this.parts) { + if (part.element?.ownerDocument === document) { + return part; + } + } + } + + return this.mainEditorPart; + } + + private getPart(group: IEditorGroupView | GroupIdentifier): EditorPart; + private getPart(element: HTMLElement): EditorPart; + private getPart(groupOrElement: IEditorGroupView | GroupIdentifier | HTMLElement): EditorPart { + if (this.parts.size > 1) { + if (groupOrElement instanceof HTMLElement) { + const element = groupOrElement; + + return this.getPartByDocument(element.ownerDocument); + } else { + const group = groupOrElement; + + let id: GroupIdentifier; + if (typeof group === 'number') { + id = group; + } else { + id = group.id; + } + + for (const part of this.parts) { + if (part.hasGroup(id)) { + return part; + } + } + } + } + + return this.mainEditorPart; + } + + //#endregion + + //#region Events + + private readonly _onDidActiveGroupChange = this._register(new Emitter()); + readonly onDidChangeActiveGroup = this._onDidActiveGroupChange.event; + + private readonly _onDidAddGroup = this._register(new Emitter()); + readonly onDidAddGroup = this._onDidAddGroup.event; + + private readonly _onDidRemoveGroup = this._register(new Emitter()); + readonly onDidRemoveGroup = this._onDidRemoveGroup.event; + + private readonly _onDidMoveGroup = this._register(new Emitter()); + readonly onDidMoveGroup = this._onDidMoveGroup.event; + + private readonly _onDidActivateGroup = this._register(new Emitter()); + readonly onDidActivateGroup = this._onDidActivateGroup.event; + + private readonly _onDidLayout = this._register(new Emitter()); + readonly onDidLayout = this._onDidLayout.event; + + private readonly _onDidScroll = this._register(new Emitter()); + readonly onDidScroll = this._onDidScroll.event; + + private readonly _onDidChangeGroupIndex = this._register(new Emitter()); + readonly onDidChangeGroupIndex = this._onDidChangeGroupIndex.event; + + private readonly _onDidChangeGroupLocked = this._register(new Emitter()); + readonly onDidChangeGroupLocked = this._onDidChangeGroupLocked.event; + + //#endregion + + //#region Editor Groups Service + + get activeGroup(): IEditorGroupView { + return this.activePart.activeGroup; + } + + get sideGroup(): IEditorSideGroup { + return this.activePart.sideGroup; + } + + get groups(): IEditorGroupView[] { + return this.getGroups(); + } + + get count(): number { + return this.groups.length; + } + + getGroups(order = GroupsOrder.CREATION_TIME): IEditorGroupView[] { + if (this.parts.size > 1) { + // TODO@bpasero support non-creation-time group orders across parts + return [...this.parts].map(part => part.getGroups(order)).flat(); + } + + return this.mainEditorPart.getGroups(order); + } + + getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined { + for (const part of this.parts) { + const group = part.getGroup(identifier); + if (group) { + return group; + } + } + + return undefined; + } + + activateGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { + return this.getPart(group).activateGroup(group); + } + + getSize(group: IEditorGroupView | GroupIdentifier): { width: number; height: number } { + return this.getPart(group).getSize(group); + } + + setSize(group: IEditorGroupView | GroupIdentifier, size: { width: number; height: number }): void { + return this.getPart(group).setSize(group, size); + } + + arrangeGroups(arrangement: GroupsArrangement): void { + return this.activePart.arrangeGroups(arrangement); + } + + restoreGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { + return this.getPart(group).restoreGroup(group); + } + + applyLayout(layout: EditorGroupLayout): void { + return this.activePart.applyLayout(layout); + } + + getLayout(): EditorGroupLayout { + return this.activePart.getLayout(); + } + + centerLayout(active: boolean): void { + return this.activePart.centerLayout(active); + } + + isLayoutCentered(): boolean { + return this.activePart.isLayoutCentered(); + } + + get orientation() { + return this.activePart.orientation; + } + + setGroupOrientation(orientation: GroupOrientation): void { + return this.activePart.setGroupOrientation(orientation); + } + + findGroup(scope: IFindGroupScope, source?: IEditorGroupView | GroupIdentifier, wrap?: boolean): IEditorGroupView | undefined { + if (source) { + return this.getPart(source).findGroup(scope, source, wrap); + } + + return this.activePart.findGroup(scope, source, wrap); + } + + addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView { + return this.getPart(location).addGroup(location, direction); + } + + removeGroup(group: IEditorGroupView | GroupIdentifier): void { + return this.getPart(group).removeGroup(group); + } + + moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView { + return this.getPart(group).moveGroup(group, location, direction); + } + + mergeGroup(group: IEditorGroupView | GroupIdentifier, target: IEditorGroupView | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroupView { + return this.getPart(group).mergeGroup(group, target, options); + } + + mergeAllGroups(): IEditorGroupView { + return this.activePart.mergeAllGroups(); + } + + copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView { + return this.getPart(group).copyGroup(group, location, direction); + } + + createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { + return this.getPart(container).createEditorDropTarget(container, delegate); + } + + //#endregion + + //#region TODO@bpasero TO BE INVESTIGATED + + get onDidVisibilityChange() { return this.mainEditorPart.onDidVisibilityChange; } + + get contentDimension() { return this.mainEditorPart.contentDimension; } + get partOptions() { return this.mainEditorPart.partOptions; } + get onDidChangeEditorPartOptions() { return this.mainEditorPart.onDidChangeEditorPartOptions; } + + enforcePartOptions(options: IEditorPartOptions): IDisposable { + return this.mainEditorPart.enforcePartOptions(options); + } + + //#endregion + + //#region Main Editor Part Only + + get isReady() { return this.mainEditorPart.isReady; } + get whenReady() { return this.mainEditorPart.whenReady; } + get whenRestored() { return this.mainEditorPart.whenRestored; } + get hasRestorableState() { return this.mainEditorPart.hasRestorableState; } + + //#endregion +} + +registerSingleton(IEditorGroupsService, EditorParts, InstantiationType.Eager); diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 253820fd98a..e862a401765 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editortabscontrol'; import { localize } from 'vs/nls'; import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd'; -import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; +import { addDisposableListener, Dimension, EventType, isMouseEvent } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, SubmenuAction, ActionRunner } from 'vs/base/common/actions'; @@ -24,7 +24,7 @@ import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { IEditorGroupsView, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, IEditorPartsView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorCommandsContext, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, EditorsOrder, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ResourceContextKey, ActiveEditorPinnedContext, ActiveEditorStickyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ActiveEditorAvailableEditorIdsContext, applyAvailableEditorIds } from 'vs/workbench/common/contextkeys'; @@ -121,10 +121,11 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC private renderDropdownAsChildElement: boolean; constructor( - private parent: HTMLElement, - protected groupsView: IEditorGroupsView, - protected groupView: IEditorGroupView, - protected tabsModel: IReadonlyEditorGroupModel, + private readonly parent: HTMLElement, + protected readonly editorPartsView: IEditorPartsView, + protected readonly groupsView: IEditorGroupsView, + protected readonly groupView: IEditorGroupView, + protected readonly tabsModel: IReadonlyEditorGroupModel, @IContextMenuService protected readonly contextMenuService: IContextMenuService, @IInstantiationService protected instantiationService: IInstantiationService, @IContextKeyService protected readonly contextKeyService: IContextKeyService, @@ -356,7 +357,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC // Find target anchor let anchor: HTMLElement | StandardMouseEvent = node; - if (e instanceof MouseEvent) { + if (isMouseEvent(e)) { anchor = new StandardMouseEvent(e); } diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index f136e7572e2..8794d96e54c 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -8,7 +8,7 @@ import { Dimension, clearNode } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { BreadcrumbsControl, BreadcrumbsControlFactory } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; -import { IEditorGroupsView, IEditorGroupTitleHeight, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupTitleHeight, IEditorGroupView, IEditorPartsView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; import { SingleEditorTabsControl } from 'vs/workbench/browser/parts/editor/singleEditorTabsControl'; @@ -42,10 +42,11 @@ export class EditorTitleControl extends Themable { private get breadcrumbsControl() { return this.breadcrumbsControlFactory?.control; } constructor( - private parent: HTMLElement, - private groupsView: IEditorGroupsView, - private groupView: IEditorGroupView, - private model: IReadonlyEditorGroupModel, + private readonly parent: HTMLElement, + private readonly editorPartsView: IEditorPartsView, + private readonly groupsView: IEditorGroupsView, + private readonly groupView: IEditorGroupView, + private readonly model: IReadonlyEditorGroupModel, @IInstantiationService private instantiationService: IInstantiationService, @IThemeService themeService: IThemeService ) { @@ -59,12 +60,12 @@ export class EditorTitleControl extends Themable { let control: IEditorTabsControl; if (this.groupsView.partOptions.showTabs) { if (this.groupsView.partOptions.pinnedTabsOnSeparateRow) { - control = this.instantiationService.createInstance(MultiRowEditorControl, this.parent, this.groupsView, this.groupView, this.model); + control = this.instantiationService.createInstance(MultiRowEditorControl, this.parent, this.editorPartsView, this.groupsView, this.groupView, this.model); } else { - control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.groupsView, this.groupView, this.model); + control = this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.editorPartsView, this.groupsView, this.groupView, this.model); } } else { - control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.groupsView, this.groupView, this.model); + control = this.instantiationService.createInstance(SingleEditorTabsControl, this.parent, this.editorPartsView, this.groupsView, this.groupView, this.model); } return this.editorTabsControlDisposable.add(control); diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index b6ee5f64f81..65511dc0221 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -32,9 +32,9 @@ import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdenti import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -134,6 +134,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { constructor( parent: HTMLElement, + editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupView: IEditorGroupView, tabsModel: IReadonlyEditorGroupModel, @@ -151,7 +152,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { @ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService, @IEditorResolverService editorResolverService: IEditorResolverService ) { - super(parent, groupsView, groupView, tabsModel, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); + super(parent, editorPartsView, groupsView, groupView, tabsModel, contextMenuService, instantiationService, contextKeyService, keybindingService, notificationService, menuService, quickInputService, themeService, editorResolverService); // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the @@ -452,7 +453,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Find target anchor let anchor: HTMLElement | StandardMouseEvent = tabsContainer; - if (e instanceof MouseEvent) { + if (isMouseEvent(e)) { anchor = new StandardMouseEvent(e); } @@ -857,7 +858,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const handleClickOrTouch = (e: MouseEvent | GestureEvent, preserveFocus: boolean): void => { tab.blur(); // prevent flicker of focus outline on tab until editor got focus - if (e instanceof MouseEvent && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { + if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { if (e.button === 1) { e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) } @@ -1971,7 +1972,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private originatesFromTabActionBar(e: MouseEvent | GestureEvent): boolean { let element: HTMLElement; - if (e instanceof MouseEvent) { + if (isMouseEvent(e)) { element = (e.target || e.srcElement) as HTMLElement; } else { element = (e as GestureEvent).initialTarget as HTMLElement; @@ -1996,7 +1997,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); if (Array.isArray(data)) { - const sourceGroup = this.groupsView.getGroup(data[0].identifier); + const sourceGroup = this.editorPartsView.getGroup(data[0].identifier); if (sourceGroup) { const mergeGroupOptions: IMergeGroupOptions = { index: targetEditorIndex }; if (!this.isMoveOperation(e, sourceGroup.id)) { @@ -2016,7 +2017,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const draggedEditor = data[0].identifier; - const sourceGroup = this.groupsView.getGroup(draggedEditor.groupId); + const sourceGroup = this.editorPartsView.getGroup(draggedEditor.groupId); if (sourceGroup) { // Move editor to target position and index diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 6186237e26d..1a6b5ca985e 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -5,7 +5,7 @@ import { Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsView, IEditorGroupView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, IEditorPartsView, IInternalEditorOpenOptions } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorTabsControl } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { MultiEditorTabsControl } from 'vs/workbench/browser/parts/editor/multiEditorTabsControl'; import { IEditorPartOptions } from 'vs/workbench/common/editor'; @@ -21,19 +21,20 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont private readonly unstickyEditorTabsControl: IEditorTabsControl; constructor( - private parent: HTMLElement, - private groupsView: IEditorGroupsView, - private groupView: IEditorGroupView, - private model: IReadonlyEditorGroupModel, - @IInstantiationService protected instantiationService: IInstantiationService + private readonly parent: HTMLElement, + editorPartsView: IEditorPartsView, + private readonly groupsView: IEditorGroupsView, + private readonly groupView: IEditorGroupView, + private readonly model: IReadonlyEditorGroupModel, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); const stickyModel = this._register(new StickyEditorGroupModel(this.model)); const unstickyModel = this._register(new UnstickyEditorGroupModel(this.model)); - this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.groupsView, this.groupView, stickyModel)); - this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, this.groupsView, this.groupView, unstickyModel)); + this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, stickyModel)); + this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, unstickyModel)); this.handlePinnedTabsSeparateRowToolbars(); } diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 8fae627d8f6..d1a308a9f49 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -5,6 +5,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; +import { addDisposableListener } from 'vs/base/browser/dom'; import { Event, Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { RunOnceScheduler, runWhenIdle, timeout } from 'vs/base/common/async'; import { isFirefox, isSafari, isChrome, PixelRatio } from 'vs/base/browser/browser'; @@ -74,14 +75,14 @@ export class Workbench extends Layout { private registerErrorHandler(logService: ILogService): void { // Listen on unhandled rejection events - window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + this._register(addDisposableListener(window, 'unhandledrejection', event => { // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent onUnexpectedError(event.reason); // Prevent the printing of this event to the console event.preventDefault(); - }); + })); // Install handler for unexpected errors setUnexpectedErrorHandler(error => this.handleUnexpectedError(error, logService)); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index 2c380e39f74..c6398302aef 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -16,7 +16,7 @@ import { localize } from 'vs/nls'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { IRange, Range } from 'vs/editor/common/core/range'; import { SplitView, Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; -import { Dimension } from 'vs/base/browser/dom'; +import { Dimension, isKeyboardEvent } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -278,7 +278,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { this._disposables.add(this._tree.onDidChangeSelection(e => { const [element] = e.elements; // don't close on click - if (element && e.browserEvent instanceof KeyboardEvent) { + if (element && isKeyboardEvent(e.browserEvent)) { this.dispose(); this._editorService.openEditor({ resource: element.item.uri, diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index fc1a0120f48..35d1c155a1e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -170,7 +170,7 @@ export class BreakpointsView extends ViewPane { return; } - if (e.browserEvent instanceof MouseEvent && e.browserEvent.button === 1) { // middle click + if (dom.isMouseEvent(e.browserEvent) && e.browserEvent.button === 1) { // middle click return; } @@ -180,9 +180,9 @@ export class BreakpointsView extends ViewPane { if (e.element instanceof InstructionBreakpoint) { const disassemblyView = await this.editorService.openEditor(DisassemblyViewInput.instance); // Focus on double click - (disassemblyView as DisassemblyView).goToInstructionAndOffset(e.element.instructionReference, e.element.offset, e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2); + (disassemblyView as DisassemblyView).goToInstructionAndOffset(e.element.instructionReference, e.element.offset, dom.isMouseEvent(e.browserEvent) && e.browserEvent.detail === 2); } - if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { + if (dom.isMouseEvent(e.browserEvent) && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { // double click this.renderInputBox({ breakpoint: e.element, type: 'name' }); } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 6cf4c61e3cc..7bd1e7ca57e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener, isKeyboardEvent } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { distinct, flatten } from 'vs/base/common/arrays'; @@ -335,7 +335,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { const onKeyUp = new DomEmitter(document, 'keyup'); const listener = Event.any(this.hostService.onDidChangeFocus, onKeyUp.event)(keyupEvent => { let standardKeyboardEvent = undefined; - if (keyupEvent instanceof KeyboardEvent) { + if (isKeyboardEvent(keyupEvent)) { standardKeyboardEvent = new StandardKeyboardEvent(keyupEvent); } if (!standardKeyboardEvent || standardKeyboardEvent.keyCode === KeyCode.Alt) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index f8811f8833c..b9627230820 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -279,7 +279,7 @@ export class ExtensionsTree extends WorkbenchAsyncDataTree { - if (event.browserEvent && event.browserEvent instanceof KeyboardEvent) { + if (dom.isKeyboardEvent(event.browserEvent)) { extensionsWorkdbenchService.open(event.elements[0].extension, { sideByside: false }); } })); diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index af0be566a8b..581f71ad2e6 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -37,6 +37,7 @@ import { OpenRecentAction } from 'vs/workbench/browser/actions/windowActions'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { isMouseEvent } from 'vs/base/browser/dom'; const explorerViewIcon = registerIcon('explorer-view-icon', Codicon.files, localize('explorerViewIcon', 'View icon of the explorer view.')); const openEditorsViewIcon = registerIcon('open-editors-view-icon', Codicon.book, localize('openEditorsIcon', 'View icon of the open editors view.')); @@ -183,7 +184,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { return this.instantiationService.createInstance(ExplorerView, { ...options, delegate: { willOpenElement: e => { - if (!(e instanceof MouseEvent)) { + if (!isMouseEvent(e)) { return; // only delay when user clicks } @@ -206,7 +207,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { } }, didOpenElement: e => { - if (!(e instanceof MouseEvent)) { + if (!isMouseEvent(e)) { return; // only delay when user clicks } diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 98947a52841..0ad3bfa636a 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -23,7 +23,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { extractEditorsAndFilesDropData } from 'vs/platform/dnd/browser/dnd'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { isWeb } from 'vs/base/common/platform'; -import { triggerDownload } from 'vs/base/browser/dom'; +import { isDragEvent, triggerDownload } from 'vs/base/browser/dom'; import { ILogService } from 'vs/platform/log/common/log'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -105,7 +105,7 @@ export class BrowserFileUpload { } private toTransfer(source: DragEvent | FileList): IWebkitDataTransfer { - if (source instanceof DragEvent) { + if (isDragEvent(source)) { return source.dataTransfer as unknown as IWebkitDataTransfer; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 55039bcddd0..d3d8d5c7e59 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -477,7 +477,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { } // Do not react if the user is expanding selection via keyboard. // Check if the item was previously also selected, if yes the user is simply expanding / collapsing current selection #66589. - const shiftDown = e.browserEvent instanceof KeyboardEvent && e.browserEvent.shiftKey; + const shiftDown = DOM.isKeyboardEvent(e.browserEvent) && e.browserEvent.shiftKey; if (!shiftDown) { if (element.isDirectory || this.explorerService.isEditable(undefined)) { // Do not react if user is clicking on explorer items while some are being edited #70276 @@ -574,12 +574,12 @@ export class ExplorerView extends ViewPane implements IExplorerView { let anchor = e.anchor; // Adjust for compressed folders (except when mouse is used) - if (DOM.isHTMLElement(anchor)) { + if (anchor instanceof HTMLElement) { if (stat) { const controller = this.renderer.getCompressedNavigationController(stat); if (controller) { - if (e.browserEvent instanceof KeyboardEvent || isCompressedFolderName(e.browserEvent.target)) { + if (DOM.isKeyboardEvent(e.browserEvent) || isCompressedFolderName(e.browserEvent.target)) { anchor = controller.labels[controller.index]; } else { controller.last(); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index ee80e9d0858..64d9b2cf1a7 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -127,7 +127,6 @@ export class OpenEditorsView extends ViewPane { const index = this.getIndex(group, e.editor); switch (e.kind) { case GroupModelChangeKind.EDITOR_ACTIVE: - case GroupModelChangeKind.GROUP_ACTIVE: this.focusActiveEditor(); break; case GroupModelChangeKind.GROUP_INDEX: @@ -160,6 +159,7 @@ export class OpenEditorsView extends ViewPane { updateWholeList(); })); this._register(this.editorGroupService.onDidMoveGroup(() => updateWholeList())); + this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.focusActiveEditor())); this._register(this.editorGroupService.onDidRemoveGroup(group => { dispose(groupDisposables.get(group.id)); updateWholeList(); @@ -278,7 +278,7 @@ export class OpenEditorsView extends ViewPane { if (!e.element) { return; } else if (e.element instanceof OpenEditor) { - if (e.browserEvent instanceof MouseEvent && e.browserEvent.button === 1) { + if (dom.isMouseEvent(e.browserEvent) && e.browserEvent.button === 1) { return; // middle click already handled above: closes the editor } @@ -378,7 +378,7 @@ export class OpenEditorsView extends ViewPane { if (!preserveActivateGroup) { this.editorGroupService.activateGroup(element.group); // needed for https://github.com/microsoft/vscode/issues/6672 } - const targetGroup = options.sideBySide ? this.editorGroupService.sideGroup : this.editorGroupService.activeGroup; + const targetGroup = options.sideBySide ? this.editorGroupService.sideGroup : element.group; targetGroup.openEditor(element.editor, options); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 159a26de442..45d2bfd68b1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -35,7 +35,6 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { NOTEBOOK_EDITOR_ID, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { NotebookPerfMarks } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; -import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; @@ -81,7 +80,6 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @IStorageService storageService: IStorageService, @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IEditorDropService private readonly _editorDropService: IEditorDropService, @INotebookEditorService private readonly _notebookWidgetService: INotebookEditorService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IFileService private readonly _fileService: IFileService, @@ -308,7 +306,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._widgetDisposableStore.add(this._widget.value!.onDidFocusWidget(() => this._onDidFocusWidget.fire())); this._widgetDisposableStore.add(this._widget.value!.onDidBlurWidget(() => this._onDidBlurWidget.fire())); - this._widgetDisposableStore.add(this._editorDropService.createEditorDropTarget(this._widget.value!.getDomNode(), { + this._widgetDisposableStore.add(this._editorGroupService.createEditorDropTarget(this._widget.value!.getDomNode(), { containsGroup: (group) => this.group?.id === group.id })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 0cca842ad38..3b944f822a9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -62,6 +62,7 @@ import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Iterable } from 'vs/base/common/iterator'; import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown, accessibleViewOnLastLine } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { isKeyboardEvent, isMouseEvent, isPointerEvent } from 'vs/base/browser/dom'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -1204,13 +1205,13 @@ export function registerTerminalActions() { const workspaceContextService = accessor.get(IWorkspaceContextService); const commandService = accessor.get(ICommandService); const folders = workspaceContextService.getWorkspace().folders; - if (eventOrOptions && eventOrOptions instanceof MouseEvent && (eventOrOptions.altKey || eventOrOptions.ctrlKey)) { + if (eventOrOptions && isMouseEvent(eventOrOptions) && (eventOrOptions.altKey || eventOrOptions.ctrlKey)) { await c.service.createTerminal({ location: { splitActiveTerminal: true } }); return; } if (c.service.isProcessSupportRegistered) { - eventOrOptions = !eventOrOptions || eventOrOptions instanceof MouseEvent ? {} : eventOrOptions; + eventOrOptions = !eventOrOptions || isMouseEvent(eventOrOptions) ? {} : eventOrOptions; let instance: ITerminalInstance | undefined; if (folders.length <= 1) { @@ -1707,7 +1708,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { throw new Error(`Could not find terminal profile "${eventOrOptionsOrProfile.profileName}"`); } options = { config }; - } else if (eventOrOptionsOrProfile instanceof MouseEvent || eventOrOptionsOrProfile instanceof PointerEvent || eventOrOptionsOrProfile instanceof KeyboardEvent) { + } else if (isMouseEvent(eventOrOptionsOrProfile) || isPointerEvent(eventOrOptionsOrProfile) || isKeyboardEvent(eventOrOptionsOrProfile)) { event = eventOrOptionsOrProfile; options = profile ? { config: profile } : undefined; } else { diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 5b3a558fba2..2ea8ddac151 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -752,7 +752,7 @@ class TestingExplorerViewModel extends Disposable { })); this._register(this.tree.onDidChangeSelection(evt => { - if (evt.browserEvent instanceof MouseEvent && (evt.browserEvent.altKey || evt.browserEvent.shiftKey)) { + if (dom.isMouseEvent(evt.browserEvent) && (evt.browserEvent.altKey || evt.browserEvent.shiftKey)) { return; // don't focus when alt-clicking to multi select } diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts index 367641ca680..8d8fb299078 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/typeHierarchy'; -import { Dimension } from 'vs/base/browser/dom'; +import { Dimension, isKeyboardEvent } from 'vs/base/browser/dom'; import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ITreeNode, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree'; @@ -279,7 +279,7 @@ export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget { this._disposables.add(this._tree.onDidChangeSelection(e => { const [element] = e.elements; // don't close on click - if (element && e.browserEvent instanceof KeyboardEvent) { + if (element && isKeyboardEvent(e.browserEvent)) { this.dispose(); this._editorService.openEditor({ resource: element.item.uri, diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index a373e1f2e27..f6f1404ff2a 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -21,7 +21,6 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IOverlayWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/webviewWindowDragMonitor'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; -import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -56,20 +55,19 @@ export class WebviewEditor extends EditorPane { @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService, - @IEditorDropService private readonly _editorDropService: IEditorDropService, @IHostService private readonly _hostService: IHostService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { super(WebviewEditor.ID, telemetryService, themeService, storageService); this._register(Event.any( - editorGroupsService.onDidScroll, - editorGroupsService.onDidAddGroup, - editorGroupsService.onDidRemoveGroup, - editorGroupsService.onDidMoveGroup, + _editorGroupsService.onDidScroll, + _editorGroupsService.onDidAddGroup, + _editorGroupsService.onDidRemoveGroup, + _editorGroupsService.onDidMoveGroup, )(() => { if (this.webview && this._visible) { this.synchronizeWebviewContainerDimensions(this.webview); @@ -186,7 +184,7 @@ export class WebviewEditor extends EditorPane { this._webviewVisibleDisposables.clear(); // Webviews are not part of the normal editor dom, so we have to register our own drag and drop handler on them. - this._webviewVisibleDisposables.add(this._editorDropService.createEditorDropTarget(input.webview.container, { + this._webviewVisibleDisposables.add(this._editorGroupsService.createEditorDropTarget(input.webview.container, { containsGroup: (group) => this.group?.id === group.id })); diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 2ae1057f9ee..f7188e6f8e6 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -133,16 +133,16 @@ export class NativeWindow extends Disposable { private registerListeners(): void { // Layout - this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e))); + this._register(addDisposableListener(window, EventType.RESIZE, () => this.layoutService.layout())); // React to editor input changes this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); // Prevent opening a real URL inside the window for (const event of [EventType.DRAG_OVER, EventType.DROP]) { - window.document.body.addEventListener(event, (e: DragEvent) => { + this._register(addDisposableListener(window.document.body, event, (e: DragEvent) => { EventHelper.stop(e); - }); + })); } // Support `runAction` event @@ -543,12 +543,6 @@ export class NativeWindow extends Disposable { } } - private onWindowResize(e: UIEvent): void { - if (e.target === window) { - this.layoutService.layout(); - } - } - private updateDocumentEdited(documentEdited: true | undefined): void { let setDocumentEdited: boolean; if (typeof documentEdited === 'boolean') { @@ -830,11 +824,6 @@ export class NativeWindow extends Disposable { private setupOpenHandlers(): void { - // Block window.open() calls - window.open = function (): Window | null { - throw new Error('Prevented call to window.open(). Use IOpenerService instead!'); - }; - // Handle external open() calls this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts new file mode 100644 index 00000000000..48e8e2179f5 --- /dev/null +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Dimension, EventHelper, EventType, addDisposableListener, copyAttributes, getClientArea, position, registerWindow, size, trackAttributes } from 'vs/base/browser/dom'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { assertIsDefined } from 'vs/base/common/types'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { isWeb } from 'vs/base/common/platform'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export const IAuxiliaryWindowService = createDecorator('auxiliaryWindowService'); + +export interface IAuxiliaryWindowService { + + readonly _serviceBrand: undefined; + + open(): IAuxiliaryWindow; +} + +export interface IAuxiliaryWindow extends IDisposable { + + readonly onDidResize: Event; + readonly onDidClose: Event; + + readonly container: HTMLElement; +} + +export class AuxiliaryWindowService implements IAuxiliaryWindowService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILifecycleService private readonly lifecycleService: ILifecycleService + ) { } + + open(): IAuxiliaryWindow { + const disposables = new DisposableStore(); + + const auxiliaryWindow = assertIsDefined(window.open('about:blank')?.window); + disposables.add(registerWindow(auxiliaryWindow)); + disposables.add(toDisposable(() => auxiliaryWindow.close())); + + this.blockMethods(auxiliaryWindow); + + this.applyMeta(auxiliaryWindow); + this.applyCSS(auxiliaryWindow, disposables); + + const container = this.applyHTML(auxiliaryWindow, disposables); + + const { onDidResize, onDidClose } = this.registerListeners(auxiliaryWindow, container, disposables); + + disposables.add(Event.once(this.lifecycleService.onDidShutdown)(() => disposables.dispose())); + disposables.add(Event.once(onDidClose.event)(() => disposables.dispose())); + + return { + container, + onDidResize: onDidResize.event, + onDidClose: onDidClose.event, + dispose: () => disposables.dispose() + }; + } + + private applyMeta(auxiliaryWindow: Window): void { + const metaCharset = auxiliaryWindow.document.head.appendChild(document.createElement('meta')); + metaCharset.setAttribute('charset', 'utf-8'); + + const originalCSPMetaTag = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); + if (originalCSPMetaTag) { + const csp = auxiliaryWindow.document.head.appendChild(document.createElement('meta')); + copyAttributes(originalCSPMetaTag, csp); + } + } + + private applyCSS(auxiliaryWindow: Window, disposables: DisposableStore): void { + + // Clone all style elements and stylesheet links from the window to the child window + for (const element of document.head.querySelectorAll('link[rel="stylesheet"], style')) { + auxiliaryWindow.document.head.appendChild(element.cloneNode(true)); + } + + // Running out of sources: listen to new stylesheets as they + // are being added to the main window and apply to child window + if (!this.environmentService.isBuilt) { + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + if (mutation.type === 'childList') { + for (const node of mutation.addedNodes) { + if (node instanceof HTMLElement && node.tagName.toLowerCase() === 'style') { + auxiliaryWindow.document.head.appendChild(node.cloneNode(true)); + } + } + } + } + }); + + observer.observe(document.head, { childList: true }); + disposables.add(toDisposable(() => observer.disconnect())); + } + } + + private applyHTML(auxiliaryWindow: Window, disposables: DisposableStore): HTMLElement { + + // Create workbench container and apply classes + const container = document.createElement('div'); + auxiliaryWindow.document.body.append(container); + + // Track attributes + disposables.add(trackAttributes(document.documentElement, auxiliaryWindow.document.documentElement)); + disposables.add(trackAttributes(document.body, auxiliaryWindow.document.body)); + disposables.add(trackAttributes(this.layoutService.container, container, ['class'])); // only class attribute + + return container; + } + + private registerListeners(auxiliaryWindow: Window & typeof globalThis, container: HTMLElement, disposables: DisposableStore) { + const onDidClose = disposables.add(new Emitter()); + disposables.add(addDisposableListener(auxiliaryWindow, 'unload', () => { + onDidClose.fire(); + })); + + disposables.add(addDisposableListener(auxiliaryWindow, 'unhandledrejection', e => { + onUnexpectedError(e.reason); + e.preventDefault(); + })); + + const onDidResize = disposables.add(new Emitter()); + disposables.add(addDisposableListener(auxiliaryWindow, EventType.RESIZE, () => { + const dimension = getClientArea(auxiliaryWindow.document.body); + position(container, 0, 0, 0, 0, 'relative'); + size(container, dimension.width, dimension.height); + + onDidResize.fire(dimension); + })); + + if (isWeb) { + disposables.add(addDisposableListener(this.layoutService.container, EventType.DROP, e => EventHelper.stop(e, true))); // Prevent default navigation on drop + disposables.add(addDisposableListener(container, EventType.WHEEL, e => e.preventDefault(), { passive: false })); // Prevent the back/forward gestures in macOS + disposables.add(addDisposableListener(this.layoutService.container, EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); // Prevent native context menus in web + } else { + disposables.add(addDisposableListener(auxiliaryWindow.document.body, EventType.DRAG_OVER, (e: DragEvent) => EventHelper.stop(e))); // Prevent drag feedback on + disposables.add(addDisposableListener(auxiliaryWindow.document.body, EventType.DROP, (e: DragEvent) => EventHelper.stop(e))); // Prevent default navigation on drop + } + + return { onDidResize, onDidClose }; + } + + private blockMethods(auxiliaryWindow: Window): void { + auxiliaryWindow.document.createElement = function () { + throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); + }; + } +} + +registerSingleton(IAuxiliaryWindowService, AuxiliaryWindowService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index f72156fa7b8..9a0d942078d 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -107,7 +107,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService let y: number | undefined; let zoom = getZoomFactor(); - if (dom.isHTMLElement(anchor)) { + if (anchor instanceof HTMLElement) { const elementPosition = dom.getDomNodePagePosition(anchor); // When drawing context menus, we adjust the pixel position for native menus using zoom level diff --git a/src/vs/workbench/services/editor/browser/editorDropService.ts b/src/vs/workbench/services/editor/browser/editorDropService.ts deleted file mode 100644 index c8d04f4ab35..00000000000 --- a/src/vs/workbench/services/editor/browser/editorDropService.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorDropTargetDelegate } from 'vs/workbench/browser/parts/editor/editorDropTarget'; - -export const IEditorDropService = createDecorator('editorDropService'); - -export interface IEditorDropService { - - readonly _serviceBrand: undefined; - - /** - * Allows to register a drag and drop target for editors. - */ - createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable; -} diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index f997b3ecfbd..dde1c5723e5 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -432,7 +432,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { return this.getEditors(EditorsOrder.SEQUENTIAL).map(({ editor }) => editor); } - getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly IEditorIdentifier[] { + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): IEditorIdentifier[] { switch (order) { // MRU diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 12a0b1cab6f..1b6299a0a52 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -37,8 +37,8 @@ export const enum GroupLocation { } export interface IFindGroupScope { - direction?: GroupDirection; - location?: GroupLocation; + readonly direction?: GroupDirection; + readonly location?: GroupLocation; } export const enum GroupsArrangement { @@ -69,13 +69,13 @@ export interface GroupLayoutArgument { * If provided, their sum must be 1 to be applied * per row or column. */ - size?: number; + readonly size?: number; /** * Editor groups will be laid out orthogonal to the * parent orientation. */ - groups?: GroupLayoutArgument[]; + readonly groups?: GroupLayoutArgument[]; } export interface EditorGroupLayout { @@ -83,12 +83,12 @@ export interface EditorGroupLayout { /** * The initial orientation of the editor groups at the root. */ - orientation: GroupOrientation; + readonly orientation: GroupOrientation; /** * The editor groups at the root of the layout. */ - groups: GroupLayoutArgument[]; + readonly groups: GroupLayoutArgument[]; } export const enum MergeGroupMode { @@ -98,34 +98,34 @@ export const enum MergeGroupMode { export interface IMergeGroupOptions { mode?: MergeGroupMode; - index?: number; + readonly index?: number; } export interface ICloseEditorOptions { - preserveFocus?: boolean; + readonly preserveFocus?: boolean; } export type ICloseEditorsFilter = { - except?: EditorInput; - direction?: CloseDirection; - savedOnly?: boolean; - excludeSticky?: boolean; + readonly except?: EditorInput; + readonly direction?: CloseDirection; + readonly savedOnly?: boolean; + readonly excludeSticky?: boolean; }; export interface ICloseAllEditorsOptions { - excludeSticky?: boolean; + readonly excludeSticky?: boolean; } export interface IEditorReplacement { - editor: EditorInput; - replacement: EditorInput; - options?: IEditorOptions; + readonly editor: EditorInput; + readonly replacement: EditorInput; + readonly options?: IEditorOptions; /** * Skips asking the user for confirmation and doesn't * save the document. Only use this if you really need to! */ - forceReplaceDirty?: boolean; + readonly forceReplaceDirty?: boolean; } export function isEditorReplacement(replacement: unknown): replacement is IEditorReplacement { @@ -163,9 +163,19 @@ export interface IEditorSideGroup { openEditor(editor: EditorInput, options?: IEditorOptions): Promise; } -export interface IEditorGroupsService { +export interface IEditorDropTargetDelegate { - readonly _serviceBrand: undefined; + /** + * A helper to figure out if the drop target contains the provided group. + */ + containsGroup?(groupView: IEditorGroup): boolean; +} + +/** + * An editor part is a viewer of editor groups. There can be multiple editor + * parts opened in multiple windows. + */ +export interface IEditorPart { /** * An event for when the active editor group changes. The active editor @@ -419,6 +429,39 @@ export interface IEditorGroupsService { * Enforce editor part options temporarily. */ enforcePartOptions(options: IEditorPartOptions): IDisposable; + + /** + * Allows to register a drag and drop target for editors + * on the provided `container`. + */ + createEditorDropTarget(container: unknown /* HTMLElement */, delegate: IEditorDropTargetDelegate): IDisposable; +} + +export interface IAuxiliaryEditorPart extends IEditorPart { + + /** + * Close this auxiliary editor part and free up associated resources. + */ + close(): Promise; +} + +/** + * The main service to interact with editor groups across all opened editor parts. + */ +export interface IEditorGroupsService extends IEditorPart { + + readonly _serviceBrand: undefined; + + /** + * Provides access to the currently active editor part. + */ + readonly activePart: IEditorPart; + + /** + * Opens a new window with a full editor part instantiated + * in there. + */ + createAuxiliaryEditorPart(): IAuxiliaryEditorPart; } export const enum OpenEditorContext { diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 2648fb5da20..200a6e33a42 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -168,6 +168,7 @@ suite('EditorGroupsService', () => { part.removeGroup(downGroup); assert.ok(!part.getGroup(downGroup.id)); + assert.ok(!part.hasGroup(downGroup.id)); assert.strictEqual(didDispose, true); assert.strictEqual(groupRemovedCounter, 1); assert.strictEqual(part.groups.length, 2); @@ -250,8 +251,11 @@ suite('EditorGroupsService', () => { assert.strictEqual(restoredPart.groups.length, 3); assert.ok(restoredPart.getGroup(rootGroup.id)); + assert.ok(restoredPart.hasGroup(rootGroup.id)); assert.ok(restoredPart.getGroup(rightGroup.id)); + assert.ok(restoredPart.hasGroup(rightGroup.id)); assert.ok(restoredPart.getGroup(downGroup.id)); + assert.ok(restoredPart.hasGroup(downGroup.id)); restoredPart.clearState(); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 62a4f171945..8e0a663bb82 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -52,7 +52,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; @@ -92,7 +92,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEditorService'; -import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { MainEditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditor } from 'vs/editor/common/editorCommon'; import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; @@ -166,6 +166,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { IHoverOptions, IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; +import { EditorParts } from 'vs/workbench/browser/parts/editor/editorParts'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -590,6 +591,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { hasContainer = true; container: HTMLElement = window.document.body; + activeContainer: HTMLElement = window.document.body; onDidChangeZenMode: Event = Event.None; onDidChangeCenteredLayout: Event = Event.None; @@ -856,9 +858,14 @@ export class TestEditorGroupsService implements IEditorGroupsService { copyGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); } centerLayout(active: boolean): void { } isLayoutCentered(): boolean { return false; } + createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { return Disposable.None; } partOptions!: IEditorPartOptions; enforcePartOptions(options: IEditorPartOptions): IDisposable { return Disposable.None; } + + readonly activePart = this; + registerEditorPart(part: any): IDisposable { return Disposable.None; } + createAuxiliaryEditorPart(): IAuxiliaryEditorPart { throw new Error('Method not implemented.'); } } export class TestEditorGroupView implements IEditorGroupView { @@ -934,6 +941,8 @@ export class TestEditorGroupView implements IEditorGroupView { export class TestEditorGroupAccessor implements IEditorGroupsView { + label: string = ''; + groups: IEditorGroupView[] = []; activeGroup!: IEditorGroupView; @@ -1702,7 +1711,11 @@ export class TestSingletonFileEditorInput extends TestFileEditorInput { override get capabilities(): EditorInputCapabilities { return EditorInputCapabilities.Singleton; } } -export class TestEditorPart extends EditorPart { +export class TestEditorPart extends MainEditorPart implements IEditorGroupsService { + + declare readonly _serviceBrand: undefined; + + readonly activePart = this; testSaveState(): void { return super.saveState(); @@ -1719,10 +1732,30 @@ export class TestEditorPart extends EditorPart { delete profileMemento[key]; } } + + registerEditorPart(part: IEditorPart): IDisposable { + return Disposable.None; + } + + createAuxiliaryEditorPart(): IAuxiliaryEditorPart { + throw new Error('Method not implemented.'); + } } export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { - const part = disposables.add(instantiationService.createInstance(TestEditorPart)); + + class TestEditorParts extends EditorParts { + + testMainPart!: TestEditorPart; + + protected override createMainEditorPart(): MainEditorPart { + this.testMainPart = instantiationService.createInstance(TestEditorPart, this); + + return this.testMainPart; + } + } + + const part = disposables.add(instantiationService.createInstance(TestEditorParts)).testMainPart; part.create(document.createElement('div')); part.layout(1080, 800, 0, 0); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index b4ad16270af..689ac705148 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -42,7 +42,7 @@ import 'vs/workbench/api/browser/viewsExtensionPoint'; //#region --- workbench parts import 'vs/workbench/browser/parts/editor/editor.contribution'; -import 'vs/workbench/browser/parts/editor/editorPart'; +import 'vs/workbench/browser/parts/editor/editorParts'; import 'vs/workbench/browser/parts/paneCompositePart'; import 'vs/workbench/browser/parts/banner/bannerPart'; import 'vs/workbench/browser/parts/statusbar/statusbarPart'; @@ -114,6 +114,7 @@ import 'vs/workbench/services/textMate/browser/textMateTokenizationFeature.contr import 'vs/workbench/services/userActivity/common/userActivityService'; import 'vs/workbench/services/userActivity/browser/userActivityBrowser'; import 'vs/workbench/services/issue/browser/issueTroubleshoot'; +import 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';