Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserView/electron-main/browserView.ts
4776 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { WebContentsView, webContents } from 'electron';
7
import { Disposable } from '../../../base/common/lifecycle.js';
8
import { Emitter, Event } from '../../../base/common/event.js';
9
import { VSBuffer } from '../../../base/common/buffer.js';
10
import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions } from '../common/browserView.js';
11
import { EVENT_KEY_CODE_MAP, KeyCode, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js';
12
import { IThemeMainService } from '../../theme/electron-main/themeMainService.js';
13
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
14
import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js';
15
import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js';
16
import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js';
17
import { ILogService } from '../../log/common/log.js';
18
import { isMacintosh } from '../../../base/common/platform.js';
19
20
const nativeShortcutKeys = new Set(['KeyA', 'KeyC', 'KeyV', 'KeyX', 'KeyZ']);
21
function shouldIgnoreNativeShortcut(input: Electron.Input): boolean {
22
const isControlInput = isMacintosh ? input.meta : input.control;
23
const isAltOnlyInput = input.alt && !input.control && !input.meta;
24
25
// Ignore Alt-only inputs (often used for accented characters or menu accelerators)
26
if (isAltOnlyInput) {
27
return true;
28
}
29
30
// Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste)
31
return isControlInput && nativeShortcutKeys.has(input.code);
32
}
33
34
/**
35
* Represents a single browser view instance with its WebContentsView and all associated logic.
36
* This class encapsulates all operations and events for a single browser view.
37
*/
38
export class BrowserView extends Disposable {
39
private readonly _view: WebContentsView;
40
private readonly _faviconRequestCache = new Map<string, Promise<string>>();
41
42
private _lastScreenshot: VSBuffer | undefined = undefined;
43
private _lastFavicon: string | undefined = undefined;
44
private _lastError: IBrowserViewLoadError | undefined = undefined;
45
46
private _window: IBaseWindow | undefined;
47
private _isSendingKeyEvent = false;
48
49
private readonly _onDidNavigate = this._register(new Emitter<IBrowserViewNavigationEvent>());
50
readonly onDidNavigate: Event<IBrowserViewNavigationEvent> = this._onDidNavigate.event;
51
52
private readonly _onDidChangeLoadingState = this._register(new Emitter<IBrowserViewLoadingEvent>());
53
readonly onDidChangeLoadingState: Event<IBrowserViewLoadingEvent> = this._onDidChangeLoadingState.event;
54
55
private readonly _onDidChangeFocus = this._register(new Emitter<IBrowserViewFocusEvent>());
56
readonly onDidChangeFocus: Event<IBrowserViewFocusEvent> = this._onDidChangeFocus.event;
57
58
private readonly _onDidChangeDevToolsState = this._register(new Emitter<IBrowserViewDevToolsStateEvent>());
59
readonly onDidChangeDevToolsState: Event<IBrowserViewDevToolsStateEvent> = this._onDidChangeDevToolsState.event;
60
61
private readonly _onDidKeyCommand = this._register(new Emitter<IBrowserViewKeyDownEvent>());
62
readonly onDidKeyCommand: Event<IBrowserViewKeyDownEvent> = this._onDidKeyCommand.event;
63
64
private readonly _onDidChangeTitle = this._register(new Emitter<IBrowserViewTitleChangeEvent>());
65
readonly onDidChangeTitle: Event<IBrowserViewTitleChangeEvent> = this._onDidChangeTitle.event;
66
67
private readonly _onDidChangeFavicon = this._register(new Emitter<IBrowserViewFaviconChangeEvent>());
68
readonly onDidChangeFavicon: Event<IBrowserViewFaviconChangeEvent> = this._onDidChangeFavicon.event;
69
70
private readonly _onDidRequestNewPage = this._register(new Emitter<IBrowserViewNewPageRequest>());
71
readonly onDidRequestNewPage: Event<IBrowserViewNewPageRequest> = this._onDidRequestNewPage.event;
72
73
private readonly _onDidClose = this._register(new Emitter<void>());
74
readonly onDidClose: Event<void> = this._onDidClose.event;
75
76
constructor(
77
viewSession: Electron.Session,
78
private readonly storageScope: BrowserViewStorageScope,
79
@IThemeMainService private readonly themeMainService: IThemeMainService,
80
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
81
@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService,
82
@ILogService private readonly logService: ILogService
83
) {
84
super();
85
86
this._view = new WebContentsView({
87
webPreferences: {
88
nodeIntegration: false,
89
contextIsolation: true,
90
sandbox: true,
91
webviewTag: false,
92
session: viewSession
93
}
94
});
95
96
this._view.webContents.setWindowOpenHandler((details) => {
97
// For new tab requests, fire event for workbench to handle
98
if (details.disposition === 'background-tab' || details.disposition === 'foreground-tab') {
99
this._onDidRequestNewPage.fire({
100
url: details.url,
101
name: details.frameName || undefined,
102
background: details.disposition === 'background-tab'
103
});
104
return { action: 'deny' }; // Deny the default browser behavior since we're handling it
105
}
106
107
// Deny other requests like new windows.
108
return { action: 'deny' };
109
});
110
111
this._view.webContents.on('destroyed', () => {
112
this._onDidClose.fire();
113
});
114
115
this.setupEventListeners();
116
117
// Create and register plugins for this web contents
118
this._register(new ThemePlugin(this._view, this.themeMainService, this.logService));
119
}
120
121
private setupEventListeners(): void {
122
const webContents = this._view.webContents;
123
124
// DevTools state events
125
webContents.on('devtools-opened', () => {
126
this._onDidChangeDevToolsState.fire({ isDevToolsOpen: true });
127
});
128
129
webContents.on('devtools-closed', () => {
130
this._onDidChangeDevToolsState.fire({ isDevToolsOpen: false });
131
});
132
133
// Favicon events
134
webContents.on('page-favicon-updated', async (_event, favicons) => {
135
if (!favicons || favicons.length === 0) {
136
return;
137
}
138
139
const found = favicons.find(f => this._faviconRequestCache.get(f));
140
if (found) {
141
// already have a cached request for this favicon, use it
142
this._lastFavicon = await this._faviconRequestCache.get(found)!;
143
this._onDidChangeFavicon.fire({ favicon: this._lastFavicon });
144
return;
145
}
146
147
// try each url in order until one works
148
for (const url of favicons) {
149
const request = (async () => {
150
const response = await webContents.session.fetch(url, {
151
cache: 'force-cache'
152
});
153
const type = await response.headers.get('content-type');
154
const buffer = await response.arrayBuffer();
155
156
return `data:${type};base64,${Buffer.from(buffer).toString('base64')}`;
157
})();
158
159
this._faviconRequestCache.set(url, request);
160
161
try {
162
this._lastFavicon = await request;
163
this._onDidChangeFavicon.fire({ favicon: this._lastFavicon });
164
// On success, leave the promise in the cache and stop looping
165
return;
166
} catch (e) {
167
this._faviconRequestCache.delete(url);
168
// On failure, try the next one
169
}
170
}
171
});
172
173
// Title events
174
webContents.on('page-title-updated', (_event, title) => {
175
this._onDidChangeTitle.fire({ title });
176
});
177
178
const fireNavigationEvent = () => {
179
this._onDidNavigate.fire({
180
url: webContents.getURL(),
181
canGoBack: webContents.navigationHistory.canGoBack(),
182
canGoForward: webContents.navigationHistory.canGoForward()
183
});
184
};
185
186
const fireLoadingEvent = (loading: boolean) => {
187
this._onDidChangeLoadingState.fire({ loading, error: this._lastError });
188
};
189
190
// Loading state events
191
webContents.on('did-start-loading', () => {
192
this._lastError = undefined;
193
fireLoadingEvent(true);
194
});
195
webContents.on('did-stop-loading', () => fireLoadingEvent(false));
196
webContents.on('did-fail-load', (e, errorCode, errorDescription, validatedURL, isMainFrame) => {
197
if (isMainFrame) {
198
this._lastError = {
199
url: validatedURL,
200
errorCode,
201
errorDescription
202
};
203
204
fireLoadingEvent(false);
205
this._onDidNavigate.fire({
206
url: validatedURL,
207
canGoBack: webContents.navigationHistory.canGoBack(),
208
canGoForward: webContents.navigationHistory.canGoForward()
209
});
210
}
211
});
212
webContents.on('did-finish-load', () => fireLoadingEvent(false));
213
214
webContents.on('render-process-gone', (_event, details) => {
215
this._lastError = {
216
url: webContents.getURL(),
217
errorCode: details.exitCode,
218
errorDescription: `Render process gone: ${details.reason}`
219
};
220
221
fireLoadingEvent(false);
222
});
223
224
// Navigation events (when URL actually changes)
225
webContents.on('did-navigate', fireNavigationEvent);
226
webContents.on('did-navigate-in-page', fireNavigationEvent);
227
228
// Focus events
229
webContents.on('focus', () => {
230
this._onDidChangeFocus.fire({ focused: true });
231
});
232
233
webContents.on('blur', () => {
234
this._onDidChangeFocus.fire({ focused: false });
235
});
236
237
// Key down events - listen for raw key input events
238
webContents.on('before-input-event', async (event, input) => {
239
if (input.type === 'keyDown' && !this._isSendingKeyEvent) {
240
if (shouldIgnoreNativeShortcut(input)) {
241
return;
242
}
243
const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0;
244
const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown;
245
const hasCommandModifier = input.control || input.alt || input.meta;
246
const isNonEditingKey =
247
keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 ||
248
keyCode >= KeyCode.AudioVolumeMute;
249
250
if (hasCommandModifier || isNonEditingKey) {
251
event.preventDefault();
252
this._onDidKeyCommand.fire({
253
key: input.key,
254
keyCode: eventKeyCode,
255
code: input.code,
256
ctrlKey: input.control || false,
257
shiftKey: input.shift || false,
258
altKey: input.alt || false,
259
metaKey: input.meta || false,
260
repeat: input.isAutoRepeat || false
261
});
262
}
263
}
264
});
265
266
// For now, always prevent sites from blocking unload.
267
// In the future we may want to show a dialog to ask the user,
268
// with heavy restrictions regarding interaction and repeated prompts.
269
webContents.on('will-prevent-unload', (e) => {
270
e.preventDefault();
271
});
272
}
273
274
/**
275
* Get the current state of this browser view
276
*/
277
getState(): IBrowserViewState {
278
const webContents = this._view.webContents;
279
return {
280
url: webContents.getURL(),
281
title: webContents.getTitle(),
282
canGoBack: webContents.navigationHistory.canGoBack(),
283
canGoForward: webContents.navigationHistory.canGoForward(),
284
loading: webContents.isLoading(),
285
isDevToolsOpen: webContents.isDevToolsOpened(),
286
lastScreenshot: this._lastScreenshot,
287
lastFavicon: this._lastFavicon,
288
lastError: this._lastError,
289
storageScope: this.storageScope
290
};
291
}
292
293
/**
294
* Toggle developer tools for this browser view.
295
*/
296
toggleDevTools(): void {
297
this._view.webContents.toggleDevTools();
298
}
299
300
/**
301
* Update the layout bounds of this view
302
*/
303
layout(bounds: IBrowserViewBounds): void {
304
if (this._window?.win?.id !== bounds.windowId) {
305
const newWindow = this.windowById(bounds.windowId);
306
if (newWindow) {
307
this._window?.win?.contentView.removeChildView(this._view);
308
this._window = newWindow;
309
newWindow.win?.contentView.addChildView(this._view);
310
}
311
}
312
313
this._view.webContents.setZoomFactor(bounds.zoomFactor);
314
this._view.setBounds({
315
x: Math.round(bounds.x * bounds.zoomFactor),
316
y: Math.round(bounds.y * bounds.zoomFactor),
317
width: Math.round(bounds.width * bounds.zoomFactor),
318
height: Math.round(bounds.height * bounds.zoomFactor)
319
});
320
}
321
322
/**
323
* Set the visibility of this view
324
*/
325
setVisible(visible: boolean): void {
326
// If the view is focused, pass focus back to the window when hiding
327
if (!visible && this._view.webContents.isFocused()) {
328
this._window?.win?.webContents.focus();
329
}
330
331
this._view.setVisible(visible);
332
}
333
334
/**
335
* Load a URL in this view
336
*/
337
async loadURL(url: string): Promise<void> {
338
await this._view.webContents.loadURL(url);
339
}
340
341
/**
342
* Get the current URL
343
*/
344
getURL(): string {
345
return this._view.webContents.getURL();
346
}
347
348
/**
349
* Navigate back in history
350
*/
351
goBack(): void {
352
if (this._view.webContents.navigationHistory.canGoBack()) {
353
this._view.webContents.navigationHistory.goBack();
354
}
355
}
356
357
/**
358
* Navigate forward in history
359
*/
360
goForward(): void {
361
if (this._view.webContents.navigationHistory.canGoForward()) {
362
this._view.webContents.navigationHistory.goForward();
363
}
364
}
365
366
/**
367
* Reload the current page
368
*/
369
reload(): void {
370
this._view.webContents.reload();
371
}
372
373
/**
374
* Check if the view can navigate back
375
*/
376
canGoBack(): boolean {
377
return this._view.webContents.navigationHistory.canGoBack();
378
}
379
380
/**
381
* Check if the view can navigate forward
382
*/
383
canGoForward(): boolean {
384
return this._view.webContents.navigationHistory.canGoForward();
385
}
386
387
/**
388
* Capture a screenshot of this view
389
*/
390
async captureScreenshot(options?: IBrowserViewCaptureScreenshotOptions): Promise<VSBuffer> {
391
const quality = options?.quality ?? 80;
392
const image = await this._view.webContents.capturePage(options?.rect, {
393
stayHidden: true,
394
stayAwake: true
395
});
396
const buffer = image.toJPEG(quality);
397
const screenshot = VSBuffer.wrap(buffer);
398
// Only update _lastScreenshot if capturing the full view
399
if (!options?.rect) {
400
this._lastScreenshot = screenshot;
401
}
402
return screenshot;
403
}
404
405
/**
406
* Dispatch a keyboard event to this view
407
*/
408
async dispatchKeyEvent(keyEvent: IBrowserViewKeyDownEvent): Promise<void> {
409
const event: Electron.KeyboardInputEvent = {
410
type: 'keyDown',
411
keyCode: keyEvent.key,
412
modifiers: []
413
};
414
if (keyEvent.ctrlKey) {
415
event.modifiers!.push('control');
416
}
417
if (keyEvent.shiftKey) {
418
event.modifiers!.push('shift');
419
}
420
if (keyEvent.altKey) {
421
event.modifiers!.push('alt');
422
}
423
if (keyEvent.metaKey) {
424
event.modifiers!.push('meta');
425
}
426
this._isSendingKeyEvent = true;
427
try {
428
await this._view.webContents.sendInputEvent(event);
429
} finally {
430
this._isSendingKeyEvent = false;
431
}
432
}
433
434
/**
435
* Set the zoom factor of this view
436
*/
437
async setZoomFactor(zoomFactor: number): Promise<void> {
438
await this._view.webContents.setZoomFactor(zoomFactor);
439
}
440
441
/**
442
* Focus this view
443
*/
444
async focus(): Promise<void> {
445
this._view.webContents.focus();
446
}
447
448
/**
449
* Get the underlying WebContentsView
450
*/
451
getWebContentsView(): WebContentsView {
452
return this._view;
453
}
454
455
override dispose(): void {
456
// Remove from parent window
457
this._window?.win?.contentView.removeChildView(this._view);
458
459
// Clean up the view and all its event listeners
460
// Note: webContents.close() automatically removes all event listeners
461
this._view.webContents.close({ waitForBeforeUnload: false });
462
463
super.dispose();
464
}
465
466
467
private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined {
468
return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId);
469
}
470
471
private codeWindowById(windowId: number | undefined): ICodeWindow | undefined {
472
if (typeof windowId !== 'number') {
473
return undefined;
474
}
475
476
return this.windowsMainService.getWindowById(windowId);
477
}
478
479
private auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined {
480
if (typeof windowId !== 'number') {
481
return undefined;
482
}
483
484
const contents = webContents.fromId(windowId);
485
if (!contents) {
486
return undefined;
487
}
488
489
return this.auxiliaryWindowsMainService.getWindowByWebContents(contents);
490
}
491
}
492
493
export class ThemePlugin extends Disposable {
494
private readonly _webContents: Electron.WebContents;
495
private _injectedCSSKey?: string;
496
497
constructor(
498
private readonly _view: Electron.WebContentsView,
499
private readonly themeMainService: IThemeMainService,
500
private readonly logService: ILogService
501
) {
502
super();
503
this._webContents = _view.webContents;
504
505
// Set view background to match editor background
506
this.applyBackgroundColor();
507
508
// Apply theme when page loads
509
this._webContents.on('did-finish-load', () => this.applyTheme());
510
511
// Update theme when VS Code theme changes
512
this._register(this.themeMainService.onDidChangeColorScheme(() => {
513
this.applyBackgroundColor();
514
this.applyTheme();
515
}));
516
}
517
518
private applyBackgroundColor(): void {
519
const backgroundColor = this.themeMainService.getBackgroundColor();
520
this._view.setBackgroundColor(backgroundColor);
521
}
522
523
private async applyTheme(): Promise<void> {
524
if (this._webContents.isDestroyed()) {
525
return;
526
}
527
528
const colorScheme = this.themeMainService.getColorScheme().dark ? 'dark' : 'light';
529
530
try {
531
// Remove previous theme CSS if it exists
532
if (this._injectedCSSKey) {
533
await this._webContents.removeInsertedCSS(this._injectedCSSKey);
534
}
535
536
// Insert new theme CSS
537
this._injectedCSSKey = await this._webContents.insertCSS(`
538
/* VS Code theme override */
539
:root {
540
color-scheme: ${colorScheme};
541
}
542
`);
543
} catch (error) {
544
this.logService.error('ThemePlugin: Failed to inject CSS', error);
545
}
546
}
547
}
548
549