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