Prepopulate the Find in Page search text with the currently selected text in the page (#291800)

* Prepopulate Find textbox

* Add warning taken from preload.ts

* Small changes

* Refactor preload script comments for clarity and security emphasis

* Small comment change

* Small comment change

* Update comment

* Use isolated world instead of main world

* Comment update

* Update comment

* Update comment

* PR Feedback

* Small comment
This commit is contained in:
Joaquín Ruales
2026-02-03 22:48:14 -08:00
committed by GitHub
parent 7d015abeb4
commit d3dbc037c0
9 changed files with 112 additions and 5 deletions

View File

@@ -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/**',

View File

@@ -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
]
}

View File

@@ -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}',

View File

@@ -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<void>;
/**
* 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<string>;
/**
* Clear all storage data for the global browser session
*/

View File

@@ -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);
}
}());

View File

@@ -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<string> {
// 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
*/

View File

@@ -243,6 +243,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
return this._getBrowserView(id).stopFindInPage(keepSelection);
}
async getSelectedText(id: string): Promise<string> {
return this._getBrowserView(id).getSelectedText();
}
async clearStorage(id: string): Promise<void> {
return this._getBrowserView(id).clearStorage();
}

View File

@@ -118,6 +118,7 @@ export interface IBrowserViewModel extends IDisposable {
focus(): Promise<void>;
findInPage(text: string, options?: IBrowserViewFindInPageOptions): Promise<void>;
stopFindInPage(keepSelection?: boolean): Promise<void>;
getSelectedText(): Promise<string>;
clearStorage(): Promise<void>;
}
@@ -336,6 +337,10 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel {
return this.browserViewService.stopFindInPage(this.id, keepSelection);
}
async getSelectedText(): Promise<string> {
return this.browserViewService.getSelectedText(this.id);
}
async clearStorage(): Promise<void> {
return this.browserViewService.clearStorage(this.id);
}

View File

@@ -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<void> {
// 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);
}