Path: blob/main/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BrowserType, IElementData, INativeBrowserElementsService } from '../common/browserElements.js';6import { CancellationToken } from '../../../base/common/cancellation.js';7import { IRectangle } from '../../window/common/window.js';8import { BrowserWindow, webContents } from 'electron';9import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js';10import { ICodeWindow } from '../../window/electron-main/window.js';11import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js';12import { IWindowsMainService } from '../../windows/electron-main/windows.js';13import { createDecorator } from '../../instantiation/common/instantiation.js';14import { Disposable } from '../../../base/common/lifecycle.js';15import { AddFirstParameterToFunctions } from '../../../base/common/types.js';1617export const INativeBrowserElementsMainService = createDecorator<INativeBrowserElementsMainService>('browserElementsMainService');18export interface INativeBrowserElementsMainService extends AddFirstParameterToFunctions<INativeBrowserElementsService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }1920interface NodeDataResponse {21outerHTML: string;22computedStyle: string;23bounds: IRectangle;24}2526export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {27_serviceBrand: undefined;2829currentLocalAddress: string | undefined;3031constructor(32@IWindowsMainService private readonly windowsMainService: IWindowsMainService,33@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService,3435) {36super();37}3839get windowId(): never { throw new Error('Not implemented in electron-main'); }4041async findWebviewTarget(debuggers: any, windowId: number, browserType: BrowserType): Promise<string | undefined> {42const { targetInfos } = await debuggers.sendCommand('Target.getTargets');43let target: typeof targetInfos[number] | undefined = undefined;44const matchingTarget = targetInfos.find((targetInfo: { url: string }) => {45try {46const url = new URL(targetInfo.url);47if (browserType === BrowserType.LiveServer) {48return url.searchParams.get('id') && url.searchParams.get('extensionId') === 'ms-vscode.live-server';49} else if (browserType === BrowserType.SimpleBrowser) {50return url.searchParams.get('parentId') === windowId.toString() && url.searchParams.get('extensionId') === 'vscode.simple-browser';51}52return false;53} catch (err) {54return false;55}56});5758// search for webview via search parameters59if (matchingTarget) {60let resultId: string | undefined;61let url: URL | undefined;62try {63url = new URL(matchingTarget.url);64resultId = url.searchParams.get('id')!;65} catch (e) {66return undefined;67}6869target = targetInfos.find((targetInfo: { url: string }) => {70try {71const url = new URL(targetInfo.url);72const isLiveServer = browserType === BrowserType.LiveServer && url.searchParams.get('serverWindowId') === resultId;73const isSimpleBrowser = browserType === BrowserType.SimpleBrowser && url.searchParams.get('id') === resultId && url.searchParams.has('vscodeBrowserReqId');74if (isLiveServer || isSimpleBrowser) {75this.currentLocalAddress = url.origin;76return true;77}78return false;79} catch (e) {80return false;81}82});8384if (target) {85return target.targetId;86}87}8889// fallback: search for webview without parameters based on current origin90target = targetInfos.find((targetInfo: { url: string }) => {91try {92const url = new URL(targetInfo.url);93return (this.currentLocalAddress === url.origin);94} catch (e) {95return false;96}97});9899if (!target) {100return undefined;101}102103return target.targetId;104}105106async waitForWebviewTargets(debuggers: any, windowId: number, browserType: BrowserType): Promise<any> {107const start = Date.now();108const timeout = 10000;109110while (Date.now() - start < timeout) {111const targetId = await this.findWebviewTarget(debuggers, windowId, browserType);112if (targetId) {113return targetId;114}115116// Wait for a short period before checking again117await new Promise(resolve => setTimeout(resolve, 500));118}119120debuggers.detach();121return undefined;122}123124async startDebugSession(windowId: number | undefined, token: CancellationToken, browserType: BrowserType, cancelAndDetachId?: number): Promise<void> {125const window = this.windowById(windowId);126if (!window?.win) {127return undefined;128}129130// Find the simple browser webview131const allWebContents = webContents.getAllWebContents();132const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id);133134if (!simpleBrowserWebview) {135return undefined;136}137138const debuggers = simpleBrowserWebview.debugger;139if (!debuggers.isAttached()) {140debuggers.attach();141}142143try {144const matchingTargetId = await this.waitForWebviewTargets(debuggers, windowId!, browserType);145if (!matchingTargetId) {146if (debuggers.isAttached()) {147debuggers.detach();148}149throw new Error('No target found');150}151152} catch (e) {153if (debuggers.isAttached()) {154debuggers.detach();155}156throw new Error('No target found');157}158159window.win.webContents.on('ipc-message', async (event, channel, closedCancelAndDetachId) => {160if (channel === `vscode:cancelCurrentSession${cancelAndDetachId}`) {161if (cancelAndDetachId !== closedCancelAndDetachId) {162return;163}164if (debuggers.isAttached()) {165debuggers.detach();166}167if (window.win) {168window.win.webContents.removeAllListeners('ipc-message');169}170}171});172}173174async finishOverlay(debuggers: any, sessionId: string | undefined): Promise<void> {175if (debuggers.isAttached() && sessionId) {176await debuggers.sendCommand('Overlay.setInspectMode', {177mode: 'none',178highlightConfig: {179showInfo: false,180showStyles: false181}182}, sessionId);183await debuggers.sendCommand('Overlay.hideHighlight', {}, sessionId);184await debuggers.sendCommand('Overlay.disable', {}, sessionId);185debuggers.detach();186}187}188189async getElementData(windowId: number | undefined, rect: IRectangle, token: CancellationToken, browserType: BrowserType, cancellationId?: number): Promise<IElementData | undefined> {190const window = this.windowById(windowId);191if (!window?.win) {192return undefined;193}194195// Find the simple browser webview196const allWebContents = webContents.getAllWebContents();197const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id);198199if (!simpleBrowserWebview) {200return undefined;201}202203const debuggers = simpleBrowserWebview.debugger;204if (!debuggers.isAttached()) {205debuggers.attach();206}207208let targetSessionId: string | undefined = undefined;209try {210const targetId = await this.findWebviewTarget(debuggers, windowId!, browserType);211const { sessionId } = await debuggers.sendCommand('Target.attachToTarget', {212targetId: targetId,213flatten: true,214});215216targetSessionId = sessionId;217218await debuggers.sendCommand('DOM.enable', {}, sessionId);219await debuggers.sendCommand('CSS.enable', {}, sessionId);220await debuggers.sendCommand('Overlay.enable', {}, sessionId);221await debuggers.sendCommand('Debugger.enable', {}, sessionId);222await debuggers.sendCommand('Runtime.enable', {}, sessionId);223224await debuggers.sendCommand('Runtime.evaluate', {225expression: `(function() {226const style = document.createElement('style');227style.id = '__pseudoBlocker__';228style.textContent = '*::before, *::after { pointer-events: none !important; }';229document.head.appendChild(style);230})();`,231}, sessionId);232233// slightly changed default CDP debugger inspect colors234await debuggers.sendCommand('Overlay.setInspectMode', {235mode: 'searchForNode',236highlightConfig: {237showInfo: true,238showRulers: false,239showStyles: true,240showAccessibilityInfo: true,241showExtensionLines: false,242contrastAlgorithm: 'aa',243contentColor: { r: 173, g: 216, b: 255, a: 0.8 },244paddingColor: { r: 150, g: 200, b: 255, a: 0.5 },245borderColor: { r: 120, g: 180, b: 255, a: 0.7 },246marginColor: { r: 200, g: 220, b: 255, a: 0.4 },247eventTargetColor: { r: 130, g: 160, b: 255, a: 0.8 },248shapeColor: { r: 130, g: 160, b: 255, a: 0.8 },249shapeMarginColor: { r: 130, g: 160, b: 255, a: 0.5 },250gridHighlightConfig: {251rowGapColor: { r: 140, g: 190, b: 255, a: 0.3 },252rowHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },253columnGapColor: { r: 140, g: 190, b: 255, a: 0.3 },254columnHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },255rowLineColor: { r: 120, g: 180, b: 255 },256columnLineColor: { r: 120, g: 180, b: 255 },257rowLineDash: true,258columnLineDash: true259},260flexContainerHighlightConfig: {261containerBorder: {262color: { r: 120, g: 180, b: 255 },263pattern: 'solid'264},265itemSeparator: {266color: { r: 140, g: 190, b: 255 },267pattern: 'solid'268},269lineSeparator: {270color: { r: 140, g: 190, b: 255 },271pattern: 'solid'272},273mainDistributedSpace: {274hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },275fillColor: { r: 140, g: 190, b: 255, a: 0.4 }276},277crossDistributedSpace: {278hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },279fillColor: { r: 140, g: 190, b: 255, a: 0.4 }280},281rowGapSpace: {282hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },283fillColor: { r: 140, g: 190, b: 255, a: 0.4 }284},285columnGapSpace: {286hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },287fillColor: { r: 140, g: 190, b: 255, a: 0.4 }288}289},290flexItemHighlightConfig: {291baseSizeBox: {292hatchColor: { r: 130, g: 170, b: 255, a: 0.6 }293},294baseSizeBorder: {295color: { r: 120, g: 180, b: 255 },296pattern: 'solid'297},298flexibilityArrow: {299color: { r: 130, g: 190, b: 255 }300}301},302},303}, sessionId);304} catch (e) {305debuggers.detach();306throw new Error('No target found', e);307}308309if (!targetSessionId) {310debuggers.detach();311throw new Error('No target session id found');312}313314const nodeData = await this.getNodeData(targetSessionId, debuggers, window.win, cancellationId);315await this.finishOverlay(debuggers, targetSessionId);316317const zoomFactor = simpleBrowserWebview.getZoomFactor();318const absoluteBounds = {319x: rect.x + nodeData.bounds.x,320y: rect.y + nodeData.bounds.y,321width: nodeData.bounds.width,322height: nodeData.bounds.height323};324325const clippedBounds = {326x: Math.max(absoluteBounds.x, rect.x),327y: Math.max(absoluteBounds.y, rect.y),328width: Math.max(0, Math.min(absoluteBounds.x + absoluteBounds.width, rect.x + rect.width) - Math.max(absoluteBounds.x, rect.x)),329height: Math.max(0, Math.min(absoluteBounds.y + absoluteBounds.height, rect.y + rect.height) - Math.max(absoluteBounds.y, rect.y))330};331332const scaledBounds = {333x: clippedBounds.x * zoomFactor,334y: clippedBounds.y * zoomFactor,335width: clippedBounds.width * zoomFactor,336height: clippedBounds.height * zoomFactor337};338339return { outerHTML: nodeData.outerHTML, computedStyle: nodeData.computedStyle, bounds: scaledBounds };340}341342async getNodeData(sessionId: string, debuggers: any, window: BrowserWindow, cancellationId?: number): Promise<NodeDataResponse> {343return new Promise((resolve, reject) => {344const onMessage = async (event: any, method: string, params: { backendNodeId: number }) => {345if (method === 'Overlay.inspectNodeRequested') {346debuggers.off('message', onMessage);347await debuggers.sendCommand('Runtime.evaluate', {348expression: `(() => {349const style = document.getElementById('__pseudoBlocker__');350if (style) style.remove();351})();`,352}, sessionId);353354const backendNodeId = params?.backendNodeId;355if (!backendNodeId) {356throw new Error('Missing backendNodeId in inspectNodeRequested event');357}358359try {360await debuggers.sendCommand('DOM.getDocument', {}, sessionId);361const { nodeIds } = await debuggers.sendCommand('DOM.pushNodesByBackendIdsToFrontend', { backendNodeIds: [backendNodeId] }, sessionId);362if (!nodeIds || nodeIds.length === 0) {363throw new Error('Failed to get node IDs.');364}365const nodeId = nodeIds[0];366367const { model } = await debuggers.sendCommand('DOM.getBoxModel', { nodeId }, sessionId);368if (!model) {369throw new Error('Failed to get box model.');370}371372const content = model.content;373const margin = model.margin;374const x = Math.min(margin[0], content[0]);375const y = Math.min(margin[1], content[1]) + 32.4; // 32.4 is height of the title bar376const width = Math.max(margin[2] - margin[0], content[2] - content[0]);377const height = Math.max(margin[5] - margin[1], content[5] - content[1]);378379const matched = await debuggers.sendCommand('CSS.getMatchedStylesForNode', { nodeId }, sessionId);380if (!matched) {381throw new Error('Failed to get matched css.');382}383384const formatted = this.formatMatchedStyles(matched);385const { outerHTML } = await debuggers.sendCommand('DOM.getOuterHTML', { nodeId }, sessionId);386if (!outerHTML) {387throw new Error('Failed to get outerHTML.');388}389390resolve({391outerHTML,392computedStyle: formatted,393bounds: { x, y, width, height }394});395} catch (err) {396debuggers.off('message', onMessage);397debuggers.detach();398reject(err);399}400}401};402403window.webContents.on('ipc-message', async (event, channel, closedCancellationId) => {404if (channel === `vscode:cancelElementSelection${cancellationId}`) {405if (cancellationId !== closedCancellationId) {406return;407}408debuggers.off('message', onMessage);409await this.finishOverlay(debuggers, sessionId);410window.webContents.removeAllListeners('ipc-message');411}412});413414debuggers.on('message', onMessage);415});416}417418formatMatchedStyles(matched: any): string {419const lines: string[] = [];420421// inline422if (matched.inlineStyle?.cssProperties?.length) {423lines.push('/* Inline style */');424lines.push('element {');425for (const prop of matched.inlineStyle.cssProperties) {426if (prop.name && prop.value) {427lines.push(` ${prop.name}: ${prop.value};`);428}429}430lines.push('}\n');431}432433// matched434if (matched.matchedCSSRules?.length) {435for (const ruleEntry of matched.matchedCSSRules) {436const rule = ruleEntry.rule;437const selectors = rule.selectorList.selectors.map((s: any) => s.text).join(', ');438lines.push(`/* Matched Rule from ${rule.origin} */`);439lines.push(`${selectors} {`);440for (const prop of rule.style.cssProperties) {441if (prop.name && prop.value) {442lines.push(` ${prop.name}: ${prop.value};`);443}444}445lines.push('}\n');446}447}448449// inherited rules450if (matched.inherited?.length) {451let level = 1;452for (const inherited of matched.inherited) {453const rules = inherited.matchedCSSRules || [];454for (const ruleEntry of rules) {455const rule = ruleEntry.rule;456const selectors = rule.selectorList.selectors.map((s: any) => s.text).join(', ');457lines.push(`/* Inherited from ancestor level ${level} (${rule.origin}) */`);458lines.push(`${selectors} {`);459for (const prop of rule.style.cssProperties) {460if (prop.name && prop.value) {461lines.push(` ${prop.name}: ${prop.value};`);462}463}464lines.push('}\n');465}466level++;467}468}469470return '\n' + lines.join('\n');471}472473private windowById(windowId: number | undefined, fallbackCodeWindowId?: number): ICodeWindow | IAuxiliaryWindow | undefined {474return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId) ?? this.codeWindowById(fallbackCodeWindowId);475}476477private codeWindowById(windowId: number | undefined): ICodeWindow | undefined {478if (typeof windowId !== 'number') {479return undefined;480}481482return this.windowsMainService.getWindowById(windowId);483}484485private auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined {486if (typeof windowId !== 'number') {487return undefined;488}489490const contents = webContents.fromId(windowId);491if (!contents) {492return undefined;493}494495return this.auxiliaryWindowsMainService.getWindowByWebContents(contents);496}497}498499500