mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Support additional actions in integrated browser (#287653)
* Add setting to redirect simple browser * Open in external browser * Open settings * Move to new window * clean * Add element shortcut * Menu item updates * PR feedback
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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<string>([
|
||||
'localhost',
|
||||
@@ -31,6 +33,27 @@ const enabledHosts = new Set<string>([
|
||||
|
||||
const openerId = 'simpleBrowser.open';
|
||||
|
||||
/**
|
||||
* Checks if the integrated browser should be used instead of the simple browser
|
||||
*/
|
||||
async function shouldUseIntegratedBrowser(): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration();
|
||||
if (!config.get<boolean>(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<void> {
|
||||
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'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void> {
|
||||
if (this._model) {
|
||||
this.group.pinEditor(this.input); // pin editor on navigation
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user