diff --git a/build/checker/layersChecker.ts b/build/checker/layersChecker.ts index 87341dcffd0..174ec273780 100644 --- a/build/checker/layersChecker.ts +++ b/build/checker/layersChecker.ts @@ -61,6 +61,12 @@ const RULES: IRule[] = [ disallowedTypes: NATIVE_TYPES, }, + // Browser view preload script + { + target: '**/vs/platform/browserView/electron-browser/preload-browserView.ts', + disallowedTypes: NATIVE_TYPES, + }, + // Common { target: '**/vs/**/common/**', diff --git a/build/checker/tsconfig.electron-browser.json b/build/checker/tsconfig.electron-browser.json index 2cbe3d3bd33..80828443aa0 100644 --- a/build/checker/tsconfig.electron-browser.json +++ b/build/checker/tsconfig.electron-browser.json @@ -16,6 +16,7 @@ "../../src/**/test/**", "../../src/**/fixtures/**", "../../src/vs/base/parts/sandbox/electron-browser/preload.ts", // Preload scripts for Electron sandbox - "../../src/vs/base/parts/sandbox/electron-browser/preload-aux.ts" // have limited access to node.js APIs + "../../src/vs/base/parts/sandbox/electron-browser/preload-aux.ts", // have limited access to node.js APIs + "../../src/vs/platform/browserView/electron-browser/preload-browserView.ts" // Browser view preload script ] } diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 8bc20da0c12..0ccfb520c8a 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -68,6 +68,7 @@ const vscodeResourceIncludes = [ // Electron Preload 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', 'out-build/vs/base/parts/sandbox/electron-browser/preload-aux.js', + 'out-build/vs/platform/browserView/electron-browser/preload-browserView.js', // Node Scripts 'out-build/vs/base/node/{terminateProcess.sh,cpuUsage.sh,ps.sh}', diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index df136bd178e..f22fd39e70b 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -117,6 +117,11 @@ export enum BrowserViewStorageScope { export const ipcBrowserViewChannelName = 'browserView'; +/** + * This should match the isolated world ID defined in `preload-browserView.ts`. + */ +export const browserViewIsolatedWorldId = 999; + export interface IBrowserViewService { /** * Dynamic events that return an Event for a specific browser view ID. @@ -246,6 +251,14 @@ export interface IBrowserViewService { */ stopFindInPage(id: string, keepSelection?: boolean): Promise; + /** + * Get the currently selected text in the browser view. + * Returns immediately with empty string if the page is still loading. + * @param id The browser view identifier + * @returns The selected text, or empty string if no selection or page is loading + */ + getSelectedText(id: string): Promise; + /** * Clear all storage data for the global browser session */ diff --git a/src/vs/platform/browserView/electron-browser/preload-browserView.ts b/src/vs/platform/browserView/electron-browser/preload-browserView.ts new file mode 100644 index 00000000000..29832f220ff --- /dev/null +++ b/src/vs/platform/browserView/electron-browser/preload-browserView.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable no-restricted-globals */ + +/** + * Preload script for pages loaded in Integrated Browser + * + * It runs in an isolated context that Electron calls an "isolated world". + * Specifically the isolated world with worldId 999, which shows in DevTools as "Electron Isolated Context". + * Despite being isolated, it still runs on the same page as the JS from the actual loaded website + * which runs on the so-called "main world" (worldId 0. In DevTools as "top"). + * + * Learn more: see Electron docs for Security, contextBridge, and Context Isolation. + */ +(function () { + + const { contextBridge } = require('electron'); + + // ####################################################################### + // ### ### + // ### !!! DO NOT USE GET/SET PROPERTIES ANYWHERE HERE !!! ### + // ### !!! UNLESS THE ACCESS IS WITHOUT SIDE EFFECTS !!! ### + // ### (https://github.com/electron/electron/issues/25516) ### + // ### ### + // ####################################################################### + const globals = { + /** + * Get the currently selected text in the page. + */ + getSelectedText(): string { + try { + // Even if the page has overridden window.getSelection, our call here will still reach the original + // implementation. That's because Electron proxies functions, such as getSelectedText here, that are + // exposed to a different context via exposeInIsolatedWorld or exposeInMainWorld. + return window.getSelection()?.toString() ?? ''; + } catch { + return ''; + } + } + }; + + try { + // Use `contextBridge` APIs to expose globals to the same isolated world where this preload script runs (worldId 999). + // The globals object will be recursively frozen (and for functions also proxied) by Electron to prevent + // modification within the given context. + contextBridge.exposeInIsolatedWorld(999, 'browserViewAPI', globals); + } catch (error) { + console.error(error); + } +}()); diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index ab70aa81dea..3154da6ccb1 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { WebContentsView, webContents } from 'electron'; +import { FileAccess } from '../../../base/common/network.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation } from '../common/browserView.js'; +import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; @@ -96,6 +97,7 @@ export class BrowserView extends Disposable { sandbox: true, webviewTag: false, session: viewSession, + preload: FileAccess.asFileUri('vs/platform/browserView/electron-browser/preload-browserView.js').fsPath, // TODO@kycutler: Remove this once https://github.com/electron/electron/issues/42578 is fixed type: 'browserView' @@ -535,6 +537,23 @@ export class BrowserView extends Disposable { this._view.webContents.stopFindInPage(keepSelection ? 'keepSelection' : 'clearSelection'); } + /** + * Get the currently selected text in the browser view. + * Returns immediately with empty string if the page is still loading. + */ + async getSelectedText(): Promise { + // we don't want to wait for the page to finish loading, which executeJavaScript normally does. + if (this._view.webContents.isLoading()) { + return ''; + } + try { + // Uses our preloaded contextBridge-exposed API. + return await this._view.webContents.executeJavaScriptInIsolatedWorld(browserViewIsolatedWorldId, [{ code: 'window.browserViewAPI?.getSelectedText?.() ?? ""' }]); + } catch { + return ''; + } + } + /** * Clear all storage data for this browser view's session */ diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index 66f9d2bb825..7932a442087 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -243,6 +243,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this._getBrowserView(id).stopFindInPage(keepSelection); } + async getSelectedText(id: string): Promise { + return this._getBrowserView(id).getSelectedText(); + } + async clearStorage(id: string): Promise { return this._getBrowserView(id).clearStorage(); } diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index a292a3a1ba2..732fa1974e4 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -118,6 +118,7 @@ export interface IBrowserViewModel extends IDisposable { focus(): Promise; findInPage(text: string, options?: IBrowserViewFindInPageOptions): Promise; stopFindInPage(keepSelection?: boolean): Promise; + getSelectedText(): Promise; clearStorage(): Promise; } @@ -336,6 +337,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { return this.browserViewService.stopFindInPage(this.id, keepSelection); } + async getSelectedText(): Promise { + return this.browserViewService.getSelectedText(this.id); + } + async clearStorage(): Promise { return this.browserViewService.clearStorage(this.id); } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index af25fe124da..445507485ca 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -600,10 +600,15 @@ export class BrowserEditor extends EditorPane { } /** - * Show the find widget + * Show the find widget, optionally pre-populated with selected text from the browser view */ - showFind(): void { - this._findWidget.value.reveal(); + async showFind(): Promise { + // Get selected text from the browser view to pre-populate the search box. + const selectedText = await this._model?.getSelectedText(); + + // Only use the selected text if it doesn't contain newlines (single line selection) + const textToReveal = selectedText && !/[\r\n]/.test(selectedText) ? selectedText : undefined; + this._findWidget.value.reveal(textToReveal); this._findWidget.value.layout(this._findWidgetContainer.clientWidth); }