diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 79802e73668..8b361f66f61 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -51,6 +51,15 @@ "default": true, "title": "Focus Lock Indicator Enabled", "description": "%configuration.focusLockIndicator.enabled.description%" + }, + "simpleBrowser.useIntegratedBrowser": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.useIntegratedBrowser.description%", + "scope": "application", + "tags": [ + "experimental" + ] } } } diff --git a/extensions/simple-browser/package.nls.json b/extensions/simple-browser/package.nls.json index 496dc28dfdd..3b6b41530fa 100644 --- a/extensions/simple-browser/package.nls.json +++ b/extensions/simple-browser/package.nls.json @@ -1,5 +1,6 @@ { "displayName": "Simple Browser", "description": "A very basic built-in webview for displaying web content.", - "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser." + "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser.", + "configuration.useIntegratedBrowser.description": "When enabled, the `simpleBrowser.show` command will open URLs in the integrated browser instead of the Simple Browser webview. **Note:** This setting is experimental and only available on desktop." } diff --git a/extensions/simple-browser/src/extension.ts b/extensions/simple-browser/src/extension.ts index 885afe28712..6eb0bb0837f 100644 --- a/extensions/simple-browser/src/extension.ts +++ b/extensions/simple-browser/src/extension.ts @@ -14,6 +14,8 @@ declare class URL { const openApiCommand = 'simpleBrowser.api.open'; const showCommand = 'simpleBrowser.show'; +const integratedBrowserCommand = 'workbench.action.browser.open'; +const useIntegratedBrowserSetting = 'simpleBrowser.useIntegratedBrowser'; const enabledHosts = new Set([ 'localhost', @@ -31,6 +33,27 @@ const enabledHosts = new Set([ const openerId = 'simpleBrowser.open'; +/** + * Checks if the integrated browser should be used instead of the simple browser + */ +async function shouldUseIntegratedBrowser(): Promise { + const config = vscode.workspace.getConfiguration(); + if (!config.get(useIntegratedBrowserSetting, false)) { + return false; + } + + // Verify that the integrated browser command is available + const commands = await vscode.commands.getCommands(true); + return commands.includes(integratedBrowserCommand); +} + +/** + * Opens a URL in the integrated browser + */ +async function openInIntegratedBrowser(url?: string): Promise { + await vscode.commands.executeCommand(integratedBrowserCommand, url); +} + export function activate(context: vscode.ExtensionContext) { const manager = new SimpleBrowserManager(context.extensionUri); @@ -43,6 +66,10 @@ export function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.commands.registerCommand(showCommand, async (url?: string) => { + if (await shouldUseIntegratedBrowser()) { + return openInIntegratedBrowser(url); + } + if (!url) { url = await vscode.window.showInputBox({ placeHolder: vscode.l10n.t("https://example.com"), @@ -55,11 +82,15 @@ export function activate(context: vscode.ExtensionContext) { } })); - context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, (url: vscode.Uri, showOptions?: { + context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, async (url: vscode.Uri, showOptions?: { preserveFocus?: boolean; viewColumn: vscode.ViewColumn; }) => { - manager.show(url, showOptions); + if (await shouldUseIntegratedBrowser()) { + await openInIntegratedBrowser(url.toString(true)); + } else { + manager.show(url, showOptions); + } })); context.subscriptions.push(vscode.window.registerExternalUriOpener(openerId, { @@ -74,10 +105,14 @@ export function activate(context: vscode.ExtensionContext) { return vscode.ExternalUriOpenerPriority.None; }, - openExternalUri(resolveUri: vscode.Uri) { - return manager.show(resolveUri, { - viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active - }); + async openExternalUri(resolveUri: vscode.Uri) { + if (await shouldUseIntegratedBrowser()) { + await openInIntegratedBrowser(resolveUri.toString(true)); + } else { + return manager.show(resolveUri, { + viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active + }); + } } }, { schemes: ['http', 'https'], diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 41d1c8e9522..a6e0856069e 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -8,7 +8,7 @@ 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 } from '../common/browserView.js'; -import { EVENT_KEY_CODE_MAP, KeyCode, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; +import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; @@ -17,19 +17,17 @@ import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryW import { ILogService } from '../../log/common/log.js'; import { isMacintosh } from '../../../base/common/platform.js'; -const nativeShortcutKeys = new Set(['KeyA', 'KeyC', 'KeyV', 'KeyX', 'KeyZ']); -function shouldIgnoreNativeShortcut(input: Electron.Input): boolean { - const isControlInput = isMacintosh ? input.meta : input.control; - const isAltOnlyInput = input.alt && !input.control && !input.meta; - - // Ignore Alt-only inputs (often used for accented characters or menu accelerators) - if (isAltOnlyInput) { - return true; - } - - // Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste) - return isControlInput && nativeShortcutKeys.has(input.code); -} +/** Key combinations that are used in system-level shortcuts. */ +const nativeShortcuts = new Set([ + KeyMod.CtrlCmd | KeyCode.KeyA, + KeyMod.CtrlCmd | KeyCode.KeyC, + KeyMod.CtrlCmd | KeyCode.KeyV, + KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyV, + KeyMod.CtrlCmd | KeyCode.KeyX, + ...(isMacintosh ? [] : [KeyMod.CtrlCmd | KeyCode.KeyY]), + KeyMod.CtrlCmd | KeyCode.KeyZ, + KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ +]); /** * Represents a single browser view instance with its WebContentsView and all associated logic. @@ -237,28 +235,8 @@ export class BrowserView extends Disposable { // Key down events - listen for raw key input events webContents.on('before-input-event', async (event, input) => { if (input.type === 'keyDown' && !this._isSendingKeyEvent) { - if (shouldIgnoreNativeShortcut(input)) { - return; - } - const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0; - const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown; - const hasCommandModifier = input.control || input.alt || input.meta; - const isNonEditingKey = - keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 || - keyCode >= KeyCode.AudioVolumeMute; - - if (hasCommandModifier || isNonEditingKey) { + if (this.tryHandleCommand(input)) { event.preventDefault(); - this._onDidKeyCommand.fire({ - key: input.key, - keyCode: eventKeyCode, - code: input.code, - ctrlKey: input.control || false, - shiftKey: input.shift || false, - altKey: input.alt || false, - metaKey: input.meta || false, - repeat: input.isAutoRepeat || false - }); } } }); @@ -467,6 +445,54 @@ export class BrowserView extends Disposable { super.dispose(); } + /** + * Potentially handle an input event as a VS Code command. + * Returns `true` if the event was forwarded to VS Code and should not be handled natively. + */ + private tryHandleCommand(input: Electron.Input): boolean { + const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0; + const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown; + + const isArrowKey = keyCode >= KeyCode.LeftArrow && keyCode <= KeyCode.DownArrow; + const isNonEditingKey = + keyCode === KeyCode.Escape || + keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 || + keyCode >= KeyCode.AudioVolumeMute; + + // Ignore most Alt-only inputs (often used for accented characters or menu accelerators) + const isAltOnlyInput = input.alt && !input.control && !input.meta; + if (isAltOnlyInput && !isNonEditingKey && !isArrowKey) { + return false; + } + + // Only reroute if there's a command modifier or it's a non-editing key + const hasCommandModifier = input.control || input.alt || input.meta; + if (!hasCommandModifier && !isNonEditingKey) { + return false; + } + + // Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste) + const isControlInput = isMacintosh ? input.meta : input.control; + const modifiedKeyCode = keyCode | + (isControlInput ? KeyMod.CtrlCmd : 0) | + (input.shift ? KeyMod.Shift : 0) | + (input.alt ? KeyMod.Alt : 0); + if (nativeShortcuts.has(modifiedKeyCode)) { + return false; + } + + this._onDidKeyCommand.fire({ + key: input.key, + keyCode: eventKeyCode, + code: input.code, + ctrlKey: input.control || false, + shiftKey: input.shift || false, + altKey: input.alt || false, + metaKey: input.meta || false, + repeat: input.isAutoRepeat || false + }); + return true; + } private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 554cc6279ad..fb9a48cda26 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -101,7 +101,7 @@ class BrowserNavigationBar extends Disposable { { hoverDelegate, highlightToggledItems: true, - toolbarOptions: { primaryGroup: 'actions' }, + toolbarOptions: { primaryGroup: (group) => group.startsWith('actions'), useSeparatorsInPrimaryActions: true }, menuOptions: { shouldForwardArgs: true } } )); @@ -431,6 +431,10 @@ export class BrowserEditor extends EditorPane { this.updateVisibility(); } + getUrl(): string | undefined { + return this._model?.url; + } + async navigateToUrl(url: string): Promise { if (this._model) { this.group.pinEditor(this.input); // pin editor on navigation diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index d57048492af..6b8991df48f 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -16,6 +16,8 @@ import { BrowserViewUri } from '../../../../platform/browserView/common/browserV import { IBrowserViewWorkbenchService } from '../common/browserView.js'; import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; // Context key expression to check if browser editor is active const BROWSER_EDITOR_ACTIVE = ContextKeyExpr.equals('activeEditor', BrowserEditor.ID); @@ -55,7 +57,7 @@ class GoBackAction extends Action2 { group: 'navigation', order: 1, }, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_BACK), + precondition: CONTEXT_BROWSER_CAN_GO_BACK, keybinding: { when: BROWSER_EDITOR_ACTIVE, weight: KeybindingWeight.WorkbenchContrib, @@ -87,9 +89,9 @@ class GoForwardAction extends Action2 { id: MenuId.BrowserNavigationToolbar, group: 'navigation', order: 2, - when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD) + when: CONTEXT_BROWSER_CAN_GO_FORWARD }, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD), + precondition: CONTEXT_BROWSER_CAN_GO_FORWARD, keybinding: { when: BROWSER_EDITOR_ACTIVE, weight: KeybindingWeight.WorkbenchContrib, @@ -123,7 +125,7 @@ class ReloadAction extends Action2 { order: 3, }, keybinding: { - when: CONTEXT_BROWSER_FOCUSED, // Keybinding is only active when focus is within the browser editor + when: CONTEXT_BROWSER_FOCUSED, weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over debug primary: KeyCode.F5, secondary: [KeyMod.CtrlCmd | KeyCode.KeyR], @@ -155,7 +157,16 @@ class AddElementToChatAction extends Action2 { group: 'actions', order: 1, when: ChatContextKeys.enabled - } + }, + keybinding: [{ + when: BROWSER_EDITOR_ACTIVE, + weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over terminal + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC, + }, { + when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape + }] }); } @@ -174,14 +185,18 @@ class ToggleDevToolsAction extends Action2 { id: ToggleDevToolsAction.ID, title: localize2('browser.toggleDevToolsAction', 'Toggle Developer Tools'), category: BrowserCategory, - icon: Codicon.tools, + icon: Codicon.console, f1: false, toggled: ContextKeyExpr.equals(CONTEXT_BROWSER_DEVTOOLS_OPEN.key, true), menu: { id: MenuId.BrowserActionsToolbar, - group: 'actions', - order: 2, - when: BROWSER_EDITOR_ACTIVE + group: '1_developer', + order: 1, + }, + keybinding: { + when: BROWSER_EDITOR_ACTIVE, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F12 } }); } @@ -193,6 +208,35 @@ class ToggleDevToolsAction extends Action2 { } } +class OpenInExternalBrowserAction extends Action2 { + static readonly ID = 'workbench.action.browser.openExternal'; + + constructor() { + super({ + id: OpenInExternalBrowserAction.ID, + title: localize2('browser.openExternalAction', 'Open in External Browser'), + category: BrowserCategory, + icon: Codicon.linkExternal, + f1: false, + menu: { + id: MenuId.BrowserActionsToolbar, + group: '2_export', + order: 1 + } + }); + } + + async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise { + if (browserEditor instanceof BrowserEditor) { + const url = browserEditor.getUrl(); + if (url) { + const openerService = accessor.get(IOpenerService); + await openerService.open(url, { openExternal: true }); + } + } + } +} + class ClearGlobalBrowserStorageAction extends Action2 { static readonly ID = 'workbench.action.browser.clearGlobalStorage'; @@ -205,7 +249,7 @@ class ClearGlobalBrowserStorageAction extends Action2 { f1: true, menu: { id: MenuId.BrowserActionsToolbar, - group: 'storage', + group: '3_settings', order: 1, when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Global) } @@ -230,7 +274,7 @@ class ClearWorkspaceBrowserStorageAction extends Action2 { f1: true, menu: { id: MenuId.BrowserActionsToolbar, - group: 'storage', + group: '3_settings', order: 2, when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Workspace) } @@ -243,6 +287,30 @@ class ClearWorkspaceBrowserStorageAction extends Action2 { } } +class OpenBrowserSettingsAction extends Action2 { + static readonly ID = 'workbench.action.browser.openSettings'; + + constructor() { + super({ + id: OpenBrowserSettingsAction.ID, + title: localize2('browser.openSettingsAction', 'Open Browser Settings'), + category: BrowserCategory, + icon: Codicon.settingsGear, + f1: false, + menu: { + id: MenuId.BrowserActionsToolbar, + group: '3_settings', + order: 3 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const preferencesService = accessor.get(IPreferencesService); + await preferencesService.openSettings({ query: '@id:workbench.browser.*,chat.sendElementsToChat.*' }); + } +} + // Register actions registerAction2(OpenIntegratedBrowserAction); registerAction2(GoBackAction); @@ -250,5 +318,7 @@ registerAction2(GoForwardAction); registerAction2(ReloadAction); registerAction2(AddElementToChatAction); registerAction2(ToggleDevToolsAction); +registerAction2(OpenInExternalBrowserAction); registerAction2(ClearGlobalBrowserStorageAction); registerAction2(ClearWorkspaceBrowserStorageAction); +registerAction2(OpenBrowserSettingsAction);