Path: blob/main/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts
5237 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 { IElementData, INativeBrowserElementsService, IBrowserTargetLocator } 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';16import { IBrowserViewMainService } from '../../browserView/electron-main/browserViewMainService.js';1718export const INativeBrowserElementsMainService = createDecorator<INativeBrowserElementsMainService>('browserElementsMainService');19export interface INativeBrowserElementsMainService extends AddFirstParameterToFunctions<INativeBrowserElementsService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }2021interface NodeDataResponse {22outerHTML: string;23computedStyle: string;24bounds: IRectangle;25}2627export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {28_serviceBrand: undefined;2930constructor(31@IWindowsMainService private readonly windowsMainService: IWindowsMainService,32@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService,33@IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService34) {35super();36}3738get windowId(): never { throw new Error('Not implemented in electron-main'); }3940/**41* Find the webview target that matches the given locator.42* Checks either webviewId or browserViewId depending on what's provided.43*/44async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {45const { targetInfos } = await debuggers.sendCommand('Target.getTargets');4647if (locator.webviewId) {48let extensionId = '';49for (const targetInfo of targetInfos) {50try {51const url = new URL(targetInfo.url);52if (url.searchParams.get('id') === locator.webviewId) {53extensionId = url.searchParams.get('extensionId') || '';54break;55}56} catch (err) {57// ignore58}59}60if (!extensionId) {61return undefined;62}6364// search for webview via search parameters65const target = targetInfos.find((targetInfo: { url: string }) => {66try {67const url = new URL(targetInfo.url);68const isLiveServer = extensionId === 'ms-vscode.live-server' && url.searchParams.get('serverWindowId') === locator.webviewId;69const isSimpleBrowser = extensionId === 'vscode.simple-browser' && url.searchParams.get('id') === locator.webviewId && url.searchParams.has('vscodeBrowserReqId');70if (isLiveServer || isSimpleBrowser) {71return true;72}73return false;74} catch (e) {75return false;76}77});78return target?.targetId;79}8081if (locator.browserViewId) {82const webContentsInstance = this.browserViewMainService.tryGetBrowserView(locator.browserViewId)?.webContents;83const target = targetInfos.find((targetInfo: { targetId: string; type: string }) => {84if (targetInfo.type !== 'page') {85return false;86}8788return webContents.fromDevToolsTargetId(targetInfo.targetId) === webContentsInstance;89});90return target?.targetId;91}9293return undefined;94}9596async waitForWebviewTargets(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {97const start = Date.now();98const timeout = 10000;99100while (Date.now() - start < timeout) {101const targetId = await this.findWebviewTarget(debuggers, locator);102if (targetId) {103return targetId;104}105106// Wait for a short period before checking again107await new Promise(resolve => setTimeout(resolve, 500));108}109110debuggers.detach();111return undefined;112}113114async startDebugSession(windowId: number | undefined, token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void> {115const window = this.windowById(windowId);116if (!window?.win) {117return undefined;118}119120// Find the simple browser webview121const allWebContents = webContents.getAllWebContents();122const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id);123124if (!simpleBrowserWebview) {125return undefined;126}127128const debuggers = simpleBrowserWebview.debugger;129if (!debuggers.isAttached()) {130debuggers.attach();131}132133try {134const matchingTargetId = await this.waitForWebviewTargets(debuggers, locator);135if (!matchingTargetId) {136if (debuggers.isAttached()) {137debuggers.detach();138}139throw new Error('No target found');140}141142} catch (e) {143if (debuggers.isAttached()) {144debuggers.detach();145}146throw new Error('No target found');147}148149window.win.webContents.on('ipc-message', async (event, channel, closedCancelAndDetachId) => {150if (channel === `vscode:cancelCurrentSession${cancelAndDetachId}`) {151if (cancelAndDetachId !== closedCancelAndDetachId) {152return;153}154if (debuggers.isAttached()) {155debuggers.detach();156}157if (window.win) {158window.win.webContents.removeAllListeners('ipc-message');159}160}161});162}163164async finishOverlay(debuggers: Electron.Debugger, sessionId: string | undefined): Promise<void> {165if (debuggers.isAttached() && sessionId) {166await debuggers.sendCommand('Overlay.setInspectMode', {167mode: 'none',168highlightConfig: {169showInfo: false,170showStyles: false171}172}, sessionId);173await debuggers.sendCommand('Overlay.hideHighlight', {}, sessionId);174await debuggers.sendCommand('Overlay.disable', {}, sessionId);175debuggers.detach();176}177}178179async getElementData(windowId: number | undefined, rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise<IElementData | undefined> {180const window = this.windowById(windowId);181if (!window?.win) {182return undefined;183}184185// Find the simple browser webview186const allWebContents = webContents.getAllWebContents();187const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id);188189if (!simpleBrowserWebview) {190return undefined;191}192193const debuggers = simpleBrowserWebview.debugger;194if (!debuggers.isAttached()) {195debuggers.attach();196}197198let targetSessionId: string | undefined = undefined;199try {200const targetId = await this.findWebviewTarget(debuggers, locator);201const { sessionId } = await debuggers.sendCommand('Target.attachToTarget', {202targetId: targetId,203flatten: true,204});205206targetSessionId = sessionId;207208await debuggers.sendCommand('DOM.enable', {}, sessionId);209await debuggers.sendCommand('CSS.enable', {}, sessionId);210await debuggers.sendCommand('Overlay.enable', {}, sessionId);211await debuggers.sendCommand('Debugger.enable', {}, sessionId);212await debuggers.sendCommand('Runtime.enable', {}, sessionId);213214await debuggers.sendCommand('Runtime.evaluate', {215expression: `(function() {216const style = document.createElement('style');217style.id = '__pseudoBlocker__';218style.textContent = '*::before, *::after { pointer-events: none !important; }';219document.head.appendChild(style);220})();`,221}, sessionId);222223// slightly changed default CDP debugger inspect colors224await debuggers.sendCommand('Overlay.setInspectMode', {225mode: 'searchForNode',226highlightConfig: {227showInfo: true,228showRulers: false,229showStyles: true,230showAccessibilityInfo: true,231showExtensionLines: false,232contrastAlgorithm: 'aa',233contentColor: { r: 173, g: 216, b: 255, a: 0.8 },234paddingColor: { r: 150, g: 200, b: 255, a: 0.5 },235borderColor: { r: 120, g: 180, b: 255, a: 0.7 },236marginColor: { r: 200, g: 220, b: 255, a: 0.4 },237eventTargetColor: { r: 130, g: 160, b: 255, a: 0.8 },238shapeColor: { r: 130, g: 160, b: 255, a: 0.8 },239shapeMarginColor: { r: 130, g: 160, b: 255, a: 0.5 },240gridHighlightConfig: {241rowGapColor: { r: 140, g: 190, b: 255, a: 0.3 },242rowHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },243columnGapColor: { r: 140, g: 190, b: 255, a: 0.3 },244columnHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },245rowLineColor: { r: 120, g: 180, b: 255 },246columnLineColor: { r: 120, g: 180, b: 255 },247rowLineDash: true,248columnLineDash: true249},250flexContainerHighlightConfig: {251containerBorder: {252color: { r: 120, g: 180, b: 255 },253pattern: 'solid'254},255itemSeparator: {256color: { r: 140, g: 190, b: 255 },257pattern: 'solid'258},259lineSeparator: {260color: { r: 140, g: 190, b: 255 },261pattern: 'solid'262},263mainDistributedSpace: {264hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },265fillColor: { r: 140, g: 190, b: 255, a: 0.4 }266},267crossDistributedSpace: {268hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },269fillColor: { r: 140, g: 190, b: 255, a: 0.4 }270},271rowGapSpace: {272hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },273fillColor: { r: 140, g: 190, b: 255, a: 0.4 }274},275columnGapSpace: {276hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },277fillColor: { r: 140, g: 190, b: 255, a: 0.4 }278}279},280flexItemHighlightConfig: {281baseSizeBox: {282hatchColor: { r: 130, g: 170, b: 255, a: 0.6 }283},284baseSizeBorder: {285color: { r: 120, g: 180, b: 255 },286pattern: 'solid'287},288flexibilityArrow: {289color: { r: 130, g: 190, b: 255 }290}291},292},293}, sessionId);294} catch (e) {295debuggers.detach();296throw new Error('No target found', e);297}298299if (!targetSessionId) {300debuggers.detach();301throw new Error('No target session id found');302}303304const nodeData = await this.getNodeData(targetSessionId, debuggers, window.win, cancellationId);305await this.finishOverlay(debuggers, targetSessionId);306307const zoomFactor = simpleBrowserWebview.getZoomFactor();308const absoluteBounds = {309x: rect.x + nodeData.bounds.x,310y: rect.y + nodeData.bounds.y,311width: nodeData.bounds.width,312height: nodeData.bounds.height313};314315const clippedBounds = {316x: Math.max(absoluteBounds.x, rect.x),317y: Math.max(absoluteBounds.y, rect.y),318width: Math.max(0, Math.min(absoluteBounds.x + absoluteBounds.width, rect.x + rect.width) - Math.max(absoluteBounds.x, rect.x)),319height: Math.max(0, Math.min(absoluteBounds.y + absoluteBounds.height, rect.y + rect.height) - Math.max(absoluteBounds.y, rect.y))320};321322const scaledBounds = {323x: clippedBounds.x * zoomFactor,324y: clippedBounds.y * zoomFactor,325width: clippedBounds.width * zoomFactor,326height: clippedBounds.height * zoomFactor327};328329return { outerHTML: nodeData.outerHTML, computedStyle: nodeData.computedStyle, bounds: scaledBounds };330}331332async getNodeData(sessionId: string, debuggers: Electron.Debugger, window: BrowserWindow, cancellationId?: number): Promise<NodeDataResponse> {333return new Promise((resolve, reject) => {334const onMessage = async (event: Electron.Event, method: string, params: { backendNodeId: number }) => {335if (method === 'Overlay.inspectNodeRequested') {336debuggers.off('message', onMessage);337await debuggers.sendCommand('Runtime.evaluate', {338expression: `(() => {339const style = document.getElementById('__pseudoBlocker__');340if (style) style.remove();341})();`,342}, sessionId);343344const backendNodeId = params?.backendNodeId;345if (!backendNodeId) {346throw new Error('Missing backendNodeId in inspectNodeRequested event');347}348349try {350await debuggers.sendCommand('DOM.getDocument', {}, sessionId);351const { nodeIds } = await debuggers.sendCommand('DOM.pushNodesByBackendIdsToFrontend', { backendNodeIds: [backendNodeId] }, sessionId);352if (!nodeIds || nodeIds.length === 0) {353throw new Error('Failed to get node IDs.');354}355const nodeId = nodeIds[0];356357const { model } = await debuggers.sendCommand('DOM.getBoxModel', { nodeId }, sessionId);358if (!model) {359throw new Error('Failed to get box model.');360}361362const content = model.content;363const margin = model.margin;364const x = Math.min(margin[0], content[0]);365const y = Math.min(margin[1], content[1]);366const width = Math.max(margin[2] - margin[0], content[2] - content[0]);367const height = Math.max(margin[5] - margin[1], content[5] - content[1]);368369const matched = await debuggers.sendCommand('CSS.getMatchedStylesForNode', { nodeId }, sessionId);370if (!matched) {371throw new Error('Failed to get matched css.');372}373374const formatted = this.formatMatchedStyles(matched);375const { outerHTML } = await debuggers.sendCommand('DOM.getOuterHTML', { nodeId }, sessionId);376if (!outerHTML) {377throw new Error('Failed to get outerHTML.');378}379380resolve({381outerHTML,382computedStyle: formatted,383bounds: { x, y, width, height }384});385} catch (err) {386debuggers.off('message', onMessage);387debuggers.detach();388reject(err);389}390}391};392393window.webContents.on('ipc-message', async (event, channel, closedCancellationId) => {394if (channel === `vscode:cancelElementSelection${cancellationId}`) {395if (cancellationId !== closedCancellationId) {396return;397}398debuggers.off('message', onMessage);399await this.finishOverlay(debuggers, sessionId);400window.webContents.removeAllListeners('ipc-message');401}402});403404debuggers.on('message', onMessage);405});406}407408formatMatchedStyles(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 {409const lines: string[] = [];410411// inline412if (matched.inlineStyle?.cssProperties?.length) {413lines.push('/* Inline style */');414lines.push('element {');415for (const prop of matched.inlineStyle.cssProperties) {416if (prop.name && prop.value) {417lines.push(` ${prop.name}: ${prop.value};`);418}419}420lines.push('}\n');421}422423// matched424if (matched.matchedCSSRules?.length) {425for (const ruleEntry of matched.matchedCSSRules) {426const rule = ruleEntry.rule;427const selectors = rule.selectorList.selectors.map(s => s.text).join(', ');428lines.push(`/* Matched Rule from ${rule.origin} */`);429lines.push(`${selectors} {`);430for (const prop of rule.style.cssProperties) {431if (prop.name && prop.value) {432lines.push(` ${prop.name}: ${prop.value};`);433}434}435lines.push('}\n');436}437}438439// inherited rules440if (matched.inherited?.length) {441let level = 1;442for (const inherited of matched.inherited) {443const inline = inherited.inlineStyle;444if (inline) {445lines.push(`/* Inherited from ancestor level ${level} (inline) */`);446lines.push('element {');447lines.push(inline.cssText);448lines.push('}\n');449}450451const rules = inherited.matchedCSSRules || [];452for (const ruleEntry of rules) {453const rule = ruleEntry.rule;454const selectors = rule.selectorList.selectors.map(s => s.text).join(', ');455lines.push(`/* Inherited from ancestor level ${level} (${rule.origin}) */`);456lines.push(`${selectors} {`);457for (const prop of rule.style.cssProperties) {458if (prop.name && prop.value) {459lines.push(` ${prop.name}: ${prop.value};`);460}461}462lines.push('}\n');463}464level++;465}466}467468return '\n' + lines.join('\n');469}470471private windowById(windowId: number | undefined, fallbackCodeWindowId?: number): ICodeWindow | IAuxiliaryWindow | undefined {472return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId) ?? this.codeWindowById(fallbackCodeWindowId);473}474475private codeWindowById(windowId: number | undefined): ICodeWindow | undefined {476if (typeof windowId !== 'number') {477return undefined;478}479480return this.windowsMainService.getWindowById(windowId);481}482483private auxiliaryWindowById(windowId: number | undefined): IAuxiliaryWindow | undefined {484if (typeof windowId !== 'number') {485return undefined;486}487488const contents = webContents.fromId(windowId);489if (!contents) {490return undefined;491}492493return this.auxiliaryWindowsMainService.getWindowByWebContents(contents);494}495}496497498