Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts
5237 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 { IElementData, INativeBrowserElementsService, IBrowserTargetLocator } from '../common/browserElements.js';
7
import { CancellationToken } from '../../../base/common/cancellation.js';
8
import { IRectangle } from '../../window/common/window.js';
9
import { BrowserWindow, webContents } from 'electron';
10
import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js';
11
import { ICodeWindow } from '../../window/electron-main/window.js';
12
import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js';
13
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
14
import { createDecorator } from '../../instantiation/common/instantiation.js';
15
import { Disposable } from '../../../base/common/lifecycle.js';
16
import { AddFirstParameterToFunctions } from '../../../base/common/types.js';
17
import { IBrowserViewMainService } from '../../browserView/electron-main/browserViewMainService.js';
18
19
export const INativeBrowserElementsMainService = createDecorator<INativeBrowserElementsMainService>('browserElementsMainService');
20
export interface INativeBrowserElementsMainService extends AddFirstParameterToFunctions<INativeBrowserElementsService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
21
22
interface NodeDataResponse {
23
outerHTML: string;
24
computedStyle: string;
25
bounds: IRectangle;
26
}
27
28
export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {
29
_serviceBrand: undefined;
30
31
constructor(
32
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
33
@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService,
34
@IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService
35
) {
36
super();
37
}
38
39
get windowId(): never { throw new Error('Not implemented in electron-main'); }
40
41
/**
42
* Find the webview target that matches the given locator.
43
* Checks either webviewId or browserViewId depending on what's provided.
44
*/
45
async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
46
const { targetInfos } = await debuggers.sendCommand('Target.getTargets');
47
48
if (locator.webviewId) {
49
let extensionId = '';
50
for (const targetInfo of targetInfos) {
51
try {
52
const url = new URL(targetInfo.url);
53
if (url.searchParams.get('id') === locator.webviewId) {
54
extensionId = url.searchParams.get('extensionId') || '';
55
break;
56
}
57
} catch (err) {
58
// ignore
59
}
60
}
61
if (!extensionId) {
62
return undefined;
63
}
64
65
// search for webview via search parameters
66
const target = targetInfos.find((targetInfo: { url: string }) => {
67
try {
68
const url = new URL(targetInfo.url);
69
const isLiveServer = extensionId === 'ms-vscode.live-server' && url.searchParams.get('serverWindowId') === locator.webviewId;
70
const isSimpleBrowser = extensionId === 'vscode.simple-browser' && url.searchParams.get('id') === locator.webviewId && url.searchParams.has('vscodeBrowserReqId');
71
if (isLiveServer || isSimpleBrowser) {
72
return true;
73
}
74
return false;
75
} catch (e) {
76
return false;
77
}
78
});
79
return target?.targetId;
80
}
81
82
if (locator.browserViewId) {
83
const webContentsInstance = this.browserViewMainService.tryGetBrowserView(locator.browserViewId)?.webContents;
84
const target = targetInfos.find((targetInfo: { targetId: string; type: string }) => {
85
if (targetInfo.type !== 'page') {
86
return false;
87
}
88
89
return webContents.fromDevToolsTargetId(targetInfo.targetId) === webContentsInstance;
90
});
91
return target?.targetId;
92
}
93
94
return undefined;
95
}
96
97
async waitForWebviewTargets(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
98
const start = Date.now();
99
const timeout = 10000;
100
101
while (Date.now() - start < timeout) {
102
const targetId = await this.findWebviewTarget(debuggers, locator);
103
if (targetId) {
104
return targetId;
105
}
106
107
// Wait for a short period before checking again
108
await new Promise(resolve => setTimeout(resolve, 500));
109
}
110
111
debuggers.detach();
112
return undefined;
113
}
114
115
async startDebugSession(windowId: number | undefined, token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void> {
116
const window = this.windowById(windowId);
117
if (!window?.win) {
118
return undefined;
119
}
120
121
// Find the simple browser webview
122
const allWebContents = webContents.getAllWebContents();
123
const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id);
124
125
if (!simpleBrowserWebview) {
126
return undefined;
127
}
128
129
const debuggers = simpleBrowserWebview.debugger;
130
if (!debuggers.isAttached()) {
131
debuggers.attach();
132
}
133
134
try {
135
const matchingTargetId = await this.waitForWebviewTargets(debuggers, locator);
136
if (!matchingTargetId) {
137
if (debuggers.isAttached()) {
138
debuggers.detach();
139
}
140
throw new Error('No target found');
141
}
142
143
} catch (e) {
144
if (debuggers.isAttached()) {
145
debuggers.detach();
146
}
147
throw new Error('No target found');
148
}
149
150
window.win.webContents.on('ipc-message', async (event, channel, closedCancelAndDetachId) => {
151
if (channel === `vscode:cancelCurrentSession${cancelAndDetachId}`) {
152
if (cancelAndDetachId !== closedCancelAndDetachId) {
153
return;
154
}
155
if (debuggers.isAttached()) {
156
debuggers.detach();
157
}
158
if (window.win) {
159
window.win.webContents.removeAllListeners('ipc-message');
160
}
161
}
162
});
163
}
164
165
async finishOverlay(debuggers: Electron.Debugger, sessionId: string | undefined): Promise<void> {
166
if (debuggers.isAttached() && sessionId) {
167
await debuggers.sendCommand('Overlay.setInspectMode', {
168
mode: 'none',
169
highlightConfig: {
170
showInfo: false,
171
showStyles: false
172
}
173
}, sessionId);
174
await debuggers.sendCommand('Overlay.hideHighlight', {}, sessionId);
175
await debuggers.sendCommand('Overlay.disable', {}, sessionId);
176
debuggers.detach();
177
}
178
}
179
180
async getElementData(windowId: number | undefined, rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise<IElementData | undefined> {
181
const window = this.windowById(windowId);
182
if (!window?.win) {
183
return undefined;
184
}
185
186
// Find the simple browser webview
187
const allWebContents = webContents.getAllWebContents();
188
const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id);
189
190
if (!simpleBrowserWebview) {
191
return undefined;
192
}
193
194
const debuggers = simpleBrowserWebview.debugger;
195
if (!debuggers.isAttached()) {
196
debuggers.attach();
197
}
198
199
let targetSessionId: string | undefined = undefined;
200
try {
201
const targetId = await this.findWebviewTarget(debuggers, locator);
202
const { sessionId } = await debuggers.sendCommand('Target.attachToTarget', {
203
targetId: targetId,
204
flatten: true,
205
});
206
207
targetSessionId = sessionId;
208
209
await debuggers.sendCommand('DOM.enable', {}, sessionId);
210
await debuggers.sendCommand('CSS.enable', {}, sessionId);
211
await debuggers.sendCommand('Overlay.enable', {}, sessionId);
212
await debuggers.sendCommand('Debugger.enable', {}, sessionId);
213
await debuggers.sendCommand('Runtime.enable', {}, sessionId);
214
215
await debuggers.sendCommand('Runtime.evaluate', {
216
expression: `(function() {
217
const style = document.createElement('style');
218
style.id = '__pseudoBlocker__';
219
style.textContent = '*::before, *::after { pointer-events: none !important; }';
220
document.head.appendChild(style);
221
})();`,
222
}, sessionId);
223
224
// slightly changed default CDP debugger inspect colors
225
await debuggers.sendCommand('Overlay.setInspectMode', {
226
mode: 'searchForNode',
227
highlightConfig: {
228
showInfo: true,
229
showRulers: false,
230
showStyles: true,
231
showAccessibilityInfo: true,
232
showExtensionLines: false,
233
contrastAlgorithm: 'aa',
234
contentColor: { r: 173, g: 216, b: 255, a: 0.8 },
235
paddingColor: { r: 150, g: 200, b: 255, a: 0.5 },
236
borderColor: { r: 120, g: 180, b: 255, a: 0.7 },
237
marginColor: { r: 200, g: 220, b: 255, a: 0.4 },
238
eventTargetColor: { r: 130, g: 160, b: 255, a: 0.8 },
239
shapeColor: { r: 130, g: 160, b: 255, a: 0.8 },
240
shapeMarginColor: { r: 130, g: 160, b: 255, a: 0.5 },
241
gridHighlightConfig: {
242
rowGapColor: { r: 140, g: 190, b: 255, a: 0.3 },
243
rowHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
244
columnGapColor: { r: 140, g: 190, b: 255, a: 0.3 },
245
columnHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
246
rowLineColor: { r: 120, g: 180, b: 255 },
247
columnLineColor: { r: 120, g: 180, b: 255 },
248
rowLineDash: true,
249
columnLineDash: true
250
},
251
flexContainerHighlightConfig: {
252
containerBorder: {
253
color: { r: 120, g: 180, b: 255 },
254
pattern: 'solid'
255
},
256
itemSeparator: {
257
color: { r: 140, g: 190, b: 255 },
258
pattern: 'solid'
259
},
260
lineSeparator: {
261
color: { r: 140, g: 190, b: 255 },
262
pattern: 'solid'
263
},
264
mainDistributedSpace: {
265
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
266
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
267
},
268
crossDistributedSpace: {
269
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
270
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
271
},
272
rowGapSpace: {
273
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
274
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
275
},
276
columnGapSpace: {
277
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
278
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
279
}
280
},
281
flexItemHighlightConfig: {
282
baseSizeBox: {
283
hatchColor: { r: 130, g: 170, b: 255, a: 0.6 }
284
},
285
baseSizeBorder: {
286
color: { r: 120, g: 180, b: 255 },
287
pattern: 'solid'
288
},
289
flexibilityArrow: {
290
color: { r: 130, g: 190, b: 255 }
291
}
292
},
293
},
294
}, sessionId);
295
} catch (e) {
296
debuggers.detach();
297
throw new Error('No target found', e);
298
}
299
300
if (!targetSessionId) {
301
debuggers.detach();
302
throw new Error('No target session id found');
303
}
304
305
const nodeData = await this.getNodeData(targetSessionId, debuggers, window.win, cancellationId);
306
await this.finishOverlay(debuggers, targetSessionId);
307
308
const zoomFactor = simpleBrowserWebview.getZoomFactor();
309
const absoluteBounds = {
310
x: rect.x + nodeData.bounds.x,
311
y: rect.y + nodeData.bounds.y,
312
width: nodeData.bounds.width,
313
height: nodeData.bounds.height
314
};
315
316
const clippedBounds = {
317
x: Math.max(absoluteBounds.x, rect.x),
318
y: Math.max(absoluteBounds.y, rect.y),
319
width: Math.max(0, Math.min(absoluteBounds.x + absoluteBounds.width, rect.x + rect.width) - Math.max(absoluteBounds.x, rect.x)),
320
height: Math.max(0, Math.min(absoluteBounds.y + absoluteBounds.height, rect.y + rect.height) - Math.max(absoluteBounds.y, rect.y))
321
};
322
323
const scaledBounds = {
324
x: clippedBounds.x * zoomFactor,
325
y: clippedBounds.y * zoomFactor,
326
width: clippedBounds.width * zoomFactor,
327
height: clippedBounds.height * zoomFactor
328
};
329
330
return { outerHTML: nodeData.outerHTML, computedStyle: nodeData.computedStyle, bounds: scaledBounds };
331
}
332
333
async getNodeData(sessionId: string, debuggers: Electron.Debugger, window: BrowserWindow, cancellationId?: number): Promise<NodeDataResponse> {
334
return new Promise((resolve, reject) => {
335
const onMessage = async (event: Electron.Event, method: string, params: { backendNodeId: number }) => {
336
if (method === 'Overlay.inspectNodeRequested') {
337
debuggers.off('message', onMessage);
338
await debuggers.sendCommand('Runtime.evaluate', {
339
expression: `(() => {
340
const style = document.getElementById('__pseudoBlocker__');
341
if (style) style.remove();
342
})();`,
343
}, sessionId);
344
345
const backendNodeId = params?.backendNodeId;
346
if (!backendNodeId) {
347
throw new Error('Missing backendNodeId in inspectNodeRequested event');
348
}
349
350
try {
351
await debuggers.sendCommand('DOM.getDocument', {}, sessionId);
352
const { nodeIds } = await debuggers.sendCommand('DOM.pushNodesByBackendIdsToFrontend', { backendNodeIds: [backendNodeId] }, sessionId);
353
if (!nodeIds || nodeIds.length === 0) {
354
throw new Error('Failed to get node IDs.');
355
}
356
const nodeId = nodeIds[0];
357
358
const { model } = await debuggers.sendCommand('DOM.getBoxModel', { nodeId }, sessionId);
359
if (!model) {
360
throw new Error('Failed to get box model.');
361
}
362
363
const content = model.content;
364
const margin = model.margin;
365
const x = Math.min(margin[0], content[0]);
366
const y = Math.min(margin[1], content[1]);
367
const width = Math.max(margin[2] - margin[0], content[2] - content[0]);
368
const height = Math.max(margin[5] - margin[1], content[5] - content[1]);
369
370
const matched = await debuggers.sendCommand('CSS.getMatchedStylesForNode', { nodeId }, sessionId);
371
if (!matched) {
372
throw new Error('Failed to get matched css.');
373
}
374
375
const formatted = this.formatMatchedStyles(matched);
376
const { outerHTML } = await debuggers.sendCommand('DOM.getOuterHTML', { nodeId }, sessionId);
377
if (!outerHTML) {
378
throw new Error('Failed to get outerHTML.');
379
}
380
381
resolve({
382
outerHTML,
383
computedStyle: formatted,
384
bounds: { x, y, width, height }
385
});
386
} catch (err) {
387
debuggers.off('message', onMessage);
388
debuggers.detach();
389
reject(err);
390
}
391
}
392
};
393
394
window.webContents.on('ipc-message', async (event, channel, closedCancellationId) => {
395
if (channel === `vscode:cancelElementSelection${cancellationId}`) {
396
if (cancellationId !== closedCancellationId) {
397
return;
398
}
399
debuggers.off('message', onMessage);
400
await this.finishOverlay(debuggers, sessionId);
401
window.webContents.removeAllListeners('ipc-message');
402
}
403
});
404
405
debuggers.on('message', onMessage);
406
});
407
}
408
409
formatMatchedStyles(matched: { inlineStyle?: { cssProperties?: Array<{ name: string; value: string }> }; matchedCSSRules?: Array<{ rule: { selectorList: { selectors: Array<{ text: string }> }; origin: string; style: { cssProperties: Array<{ name: string; value: string }> } } }>; inherited?: Array<{ inlineStyle?: { cssText: string }; matchedCSSRules?: Array<{ rule: { selectorList: { selectors: Array<{ text: string }> }; origin: string; style: { cssProperties: Array<{ name: string; value: string }> } } }> }> }): string {
410
const lines: string[] = [];
411
412
// inline
413
if (matched.inlineStyle?.cssProperties?.length) {
414
lines.push('/* Inline style */');
415
lines.push('element {');
416
for (const prop of matched.inlineStyle.cssProperties) {
417
if (prop.name && prop.value) {
418
lines.push(` ${prop.name}: ${prop.value};`);
419
}
420
}
421
lines.push('}\n');
422
}
423
424
// matched
425
if (matched.matchedCSSRules?.length) {
426
for (const ruleEntry of matched.matchedCSSRules) {
427
const rule = ruleEntry.rule;
428
const selectors = rule.selectorList.selectors.map(s => s.text).join(', ');
429
lines.push(`/* Matched Rule from ${rule.origin} */`);
430
lines.push(`${selectors} {`);
431
for (const prop of rule.style.cssProperties) {
432
if (prop.name && prop.value) {
433
lines.push(` ${prop.name}: ${prop.value};`);
434
}
435
}
436
lines.push('}\n');
437
}
438
}
439
440
// inherited rules
441
if (matched.inherited?.length) {
442
let level = 1;
443
for (const inherited of matched.inherited) {
444
const inline = inherited.inlineStyle;
445
if (inline) {
446
lines.push(`/* Inherited from ancestor level ${level} (inline) */`);
447
lines.push('element {');
448
lines.push(inline.cssText);
449
lines.push('}\n');
450
}
451
452
const rules = inherited.matchedCSSRules || [];
453
for (const ruleEntry of rules) {
454
const rule = ruleEntry.rule;
455
const selectors = rule.selectorList.selectors.map(s => s.text).join(', ');
456
lines.push(`/* Inherited from ancestor level ${level} (${rule.origin}) */`);
457
lines.push(`${selectors} {`);
458
for (const prop of rule.style.cssProperties) {
459
if (prop.name && prop.value) {
460
lines.push(` ${prop.name}: ${prop.value};`);
461
}
462
}
463
lines.push('}\n');
464
}
465
level++;
466
}
467
}
468
469
return '\n' + lines.join('\n');
470
}
471
472
private windowById(windowId: number | undefined, fallbackCodeWindowId?: number): ICodeWindow | IAuxiliaryWindow | undefined {
473
return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId) ?? this.codeWindowById(fallbackCodeWindowId);
474
}
475
476
private codeWindowById(windowId: number | undefined): ICodeWindow | undefined {
477
if (typeof windowId !== 'number') {
478
return undefined;
479
}
480
481
return this.windowsMainService.getWindowById(windowId);
482
}
483
484
private auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined {
485
if (typeof windowId !== 'number') {
486
return undefined;
487
}
488
489
const contents = webContents.fromId(windowId);
490
if (!contents) {
491
return undefined;
492
}
493
494
return this.auxiliaryWindowsMainService.getWindowByWebContents(contents);
495
}
496
}
497
498