diff --git a/package.nls.json b/package.nls.json index 97a31d0f1..1563f3529 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,5 +1,9 @@ { "add.eventListener.breakpoint": "Toggle Event Listener Breakpoints", + "add.xhr.breakpoint": "Add XHR/fetch Breakpoint", + "breakpoint.xhr.contains":"Break when URL contains:", + "breakpoint.xhr.any":"Any XHR/fetch", + "edit.xhr.breakpoint": "Edit XHR/fetch Breakpoint", "attach.node.process": "Attach to Node Process", "base.cascadeTerminateToConfigurations.label": "A list of debug sessions which, when this debug session is terminated, will also be stopped.", "base.enableDWARF.label": "Toggles whether the debugger will try to read DWARF debug symbols from WebAssembly, which can be resource intensive. Requires the `ms-vscode.wasm-dwarf-debugging` extension to function.", @@ -186,7 +190,8 @@ "profile.start": "Take Performance Profile", "profile.stop": "Stop Performance Profile", "remove.eventListener.breakpoint.all": "Remove All Event Listener Breakpoints", - "remove.eventListener.breakpoint": "Remove Event Listener Breakpoint", + "remove.xhr.breakpoint.all": "Remove All XHR/fetch Breakpoints", + "remove.xhr.breakpoint": "Remove XHR/fetch Breakpoint", "requestCDPProxy.label": "Request CDP Proxy for Debug Session", "skipFiles.description": "An array of glob patterns for files to skip when debugging. The pattern `/**` matches all internal Node.js modules.", "smartStep.description": "Automatically step through generated code that cannot be mapped back to the original source.", diff --git a/src/adapter/customBreakpoints.ts b/src/adapter/customBreakpoints.ts index 51496b0ec..cfe3fa656 100644 --- a/src/adapter/customBreakpoints.ts +++ b/src/adapter/customBreakpoints.ts @@ -5,7 +5,9 @@ import * as l10n from '@vscode/l10n'; import Cdp from '../cdp/api'; -export type CustomBreakpointId = string; +export interface IXHRBreakpoint { + match: string; +} export interface ICustomBreakpoint { id: string; diff --git a/src/adapter/debugAdapter.ts b/src/adapter/debugAdapter.ts index e61e848c4..6627fd82f 100644 --- a/src/adapter/debugAdapter.ts +++ b/src/adapter/debugAdapter.ts @@ -49,6 +49,7 @@ export class DebugAdapter implements IDisposable { readonly breakpointManager: BreakpointManager; private _disposables = new DisposableList(); private _customBreakpoints: string[] = []; + private _xhrBreakpoints: string[] = []; private _thread: Thread | undefined; private _threadDeferred = getDeferred(); private _configurationDoneDeferred: IDeferred; @@ -103,6 +104,7 @@ export class DebugAdapter implements IDisposable { this.dap.on('exceptionInfo', () => this._withThread(thread => thread.exceptionInfo())); this.dap.on('setCustomBreakpoints', params => this.setCustomBreakpoints(params)); this.dap.on('toggleSkipFileStatus', params => this._toggleSkipFileStatus(params)); + this.dap.on('toggleSkipFileStatus', params => this._toggleSkipFileStatus(params)); this.dap.on('prettyPrintSource', params => this._prettyPrintSource(params)); this.dap.on('revealPage', () => this._withThread(thread => thread.revealPage())); this.dap.on('getPerformance', () => @@ -455,7 +457,7 @@ export class DebugAdapter implements IDisposable { profile.start(this.dap, this._thread, { type: BasicCpuProfiler.type }); } - this._thread.updateCustomBreakpoints(this._customBreakpoints); + this._thread.updateCustomBreakpoints(this._xhrBreakpoints, this._customBreakpoints); this.asyncStackPolicy .connect(cdp) @@ -475,12 +477,11 @@ export class DebugAdapter implements IDisposable { async setCustomBreakpoints({ ids, + xhr, }: Dap.SetCustomBreakpointsParams): Promise { + await this._thread?.updateCustomBreakpoints(xhr, ids); this._customBreakpoints = ids; - if (this._thread) { - await this._thread.updateCustomBreakpoints(ids); - } - + this._xhrBreakpoints = xhr; return {}; } diff --git a/src/adapter/threads.ts b/src/adapter/threads.ts index 30b94dead..8fe877a80 100644 --- a/src/adapter/threads.ts +++ b/src/adapter/threads.ts @@ -29,7 +29,7 @@ import { BreakpointManager, EntryBreakpointMode } from './breakpoints'; import { UserDefinedBreakpoint } from './breakpoints/userDefinedBreakpoint'; import { ICompletions } from './completions'; import { ExceptionMessage, IConsole, QueryObjectsMessage } from './console'; -import { CustomBreakpointId, customBreakpoints } from './customBreakpoints'; +import { customBreakpoints } from './customBreakpoints'; import { IEvaluator } from './evaluator'; import { IExceptionPauseService } from './exceptionPauseService'; import * as objectPreview from './objectPreview'; @@ -156,6 +156,11 @@ class StateQueue { } } +const enum CustomBreakpointPrefix { + XHR = 'x', + Event = 'e', +} + export class Thread implements IVariableStoreLocationProvider { private static _lastThreadId = 0; public readonly id: number; @@ -173,7 +178,7 @@ export class Thread implements IVariableStoreLocationProvider { private _sourceMapDisabler?: SourceMapDisabler; private _expectedPauseReason?: ExpectedPauseReason; private _excludedCallers: readonly Dap.ExcludedCaller[] = []; - private _enabledCustomBreakpoints?: ReadonlySet; + private _enabledCustomBreakpoints?: ReadonlySet; private readonly stateQueue = new StateQueue(); private readonly _onPausedEmitter = new EventEmitter(); private readonly _dap: DeferredContainer; @@ -1250,23 +1255,39 @@ export class Thread implements IVariableStoreLocationProvider { return `@ VM${raw.scriptId || 'XX'}:${raw.lineNumber}`; } - async updateCustomBreakpoints(ids: CustomBreakpointId[]): Promise { + async updateCustomBreakpoints(xhr: string[], events: string[]): Promise { if (!this.target.supportsCustomBreakpoints()) return; this._enabledCustomBreakpoints ??= new Set(); // Do not fail for custom breakpoints, to account for // future changes in cdp vs stale breakpoints saved in the workspace. - const newIds = new Set(ids); + const newIds = new Set(); + for (const x of xhr) { + newIds.add(CustomBreakpointPrefix.XHR + x); + } + for (const e of events) { + newIds.add(CustomBreakpointPrefix.Event + e); + } const todo: (Promise | undefined)[] = []; for (const newId of newIds) { if (!this._enabledCustomBreakpoints.has(newId)) { - todo.push(customBreakpoints().get(newId)?.apply(this._cdp, true)); + const id = newId.slice(CustomBreakpointPrefix.XHR.length); + todo.push( + newId.startsWith(CustomBreakpointPrefix.XHR) + ? this._cdp.DOMDebugger.setXHRBreakpoint({ url: id }) + : customBreakpoints().get(id)?.apply(this._cdp, true), + ); } } for (const oldId of this._enabledCustomBreakpoints) { if (!newIds.has(oldId)) { - todo.push(customBreakpoints().get(oldId)?.apply(this._cdp, false)); + const id = oldId.slice(CustomBreakpointPrefix.XHR.length); + todo.push( + oldId.startsWith(CustomBreakpointPrefix.XHR) + ? this._cdp.DOMDebugger.removeXHRBreakpoint({ url: id }) + : customBreakpoints().get(id)?.apply(this._cdp, false), + ); } } diff --git a/src/build/dapCustom.ts b/src/build/dapCustom.ts index 394fe9304..18941ed71 100644 --- a/src/build/dapCustom.ts +++ b/src/build/dapCustom.ts @@ -90,8 +90,15 @@ const dapCustom: JSONSchema4 = { }, description: 'Id of breakpoints that should be enabled.', }, + xhr: { + type: 'array', + items: { + type: 'string', + }, + description: 'strings of XHR breakpoints that should be enabled.', + }, }, - required: ['ids'], + required: ['ids', 'xhr'], }), ...makeRequest('prettyPrintSource', 'Pretty prints source for debugging.', { diff --git a/src/build/generate-contributions.ts b/src/build/generate-contributions.ts index 98dc8f30f..d7bb1ccb6 100644 --- a/src/build/generate-contributions.ts +++ b/src/build/generate-contributions.ts @@ -4,8 +4,6 @@ import { JSONSchema6, JSONSchema6Definition } from 'json-schema'; import type strings from '../../package.nls.json'; import { - allCommands, - allDebugTypes, AutoAttachMode, Commands, Configuration, @@ -13,19 +11,14 @@ import { CustomViews, DebugType, IConfigurationTypes, + allCommands, + allDebugTypes, preferredDebugTypes, } from '../common/contributionUtils'; import { knownToolToken } from '../common/knownTools'; import { mapValues, sortKeys, walkObject } from '../common/objUtils'; import { AnyLaunchConfiguration, - baseDefaults, - breakpointLanguages, - chromeAttachConfigDefaults, - chromeLaunchConfigDefaults, - edgeAttachConfigDefaults, - edgeLaunchConfigDefaults, - extensionHostConfigDefaults, IBaseConfiguration, IChromeAttachConfiguration, IChromeLaunchConfiguration, @@ -39,10 +32,17 @@ import { INodeLaunchConfiguration, ITerminalLaunchConfiguration, KillBehavior, - nodeAttachConfigDefaults, - nodeLaunchConfigDefaults, OutputSource, ResolvingConfiguration, + baseDefaults, + breakpointLanguages, + chromeAttachConfigDefaults, + chromeLaunchConfigDefaults, + edgeAttachConfigDefaults, + edgeLaunchConfigDefaults, + extensionHostConfigDefaults, + nodeAttachConfigDefaults, + nodeLaunchConfigDefaults, terminalBaseDefaults, } from '../configuration'; @@ -1232,6 +1232,21 @@ const commands: ReadonlyArray<{ title: refString('remove.eventListener.breakpoint.all'), icon: '$(close-all)', }, + { + command: Commands.AddXHRBreakpoints, + title: refString('add.xhr.breakpoint'), + icon: '$(add)', + }, + { + command: Commands.RemoveXHRBreakpoints, + title: refString('remove.xhr.breakpoint'), + icon: '$(remove)', + }, + { + command: Commands.EditXHRBreakpoint, + title: refString('edit.xhr.breakpoint'), + icon: '$(edit)', + }, { command: Commands.AttachProcess, title: refString('attach.node.process'), @@ -1474,6 +1489,33 @@ const menus: Menus = { }, ], 'view/item/context': [ + { + command: Commands.AddXHRBreakpoints, + when: `view == ${CustomViews.EventListenerBreakpoints} && viewItem == xhrBreakpoint`, + }, + { + command: Commands.EditXHRBreakpoint, + when: `view == ${CustomViews.EventListenerBreakpoints} && viewItem == xhrBreakpoint`, + group: 'inline', + }, + { + command: Commands.EditXHRBreakpoint, + when: `view == ${CustomViews.EventListenerBreakpoints} && viewItem == xhrBreakpoint`, + }, + { + command: Commands.RemoveXHRBreakpoints, + when: `view == ${CustomViews.EventListenerBreakpoints} && viewItem == xhrBreakpoint`, + group: 'inline', + }, + { + command: Commands.RemoveXHRBreakpoints, + when: `view == ${CustomViews.EventListenerBreakpoints} && viewItem == xhrBreakpoint`, + }, + { + command: Commands.AddXHRBreakpoints, + when: `view == ${CustomViews.EventListenerBreakpoints} && viewItem == xhrCategory`, + group: 'inline', + }, { command: Commands.CallersGoToCaller, group: 'inline', diff --git a/src/cdp/api.d.ts b/src/cdp/api.d.ts index db94d6586..da5000847 100644 --- a/src/cdp/api.d.ts +++ b/src/cdp/api.d.ts @@ -1703,6 +1703,8 @@ export namespace Cdp { | 'IdTokenHttpNotFound' | 'IdTokenNoResponse' | 'IdTokenInvalidResponse' + | 'IdTokenIdpErrorResponse' + | 'IdTokenCrossSiteIdpErrorResponse' | 'IdTokenInvalidRequest' | 'IdTokenInvalidContentType' | 'ErrorIdToken' @@ -2938,7 +2940,7 @@ export namespace Cdp { /** * Definition of PermissionDescriptor defined in the Permissions API: - * https://w3c.github.io/permissions/#dictdef-permissiondescriptor. + * https://w3c.github.io/permissions/#dom-permissiondescriptor. */ export interface PermissionDescriptor { /** @@ -23093,6 +23095,7 @@ export namespace Cdp { | 'unload' | 'usb' | 'vertical-scroll' + | 'web-printing' | 'web-share' | 'window-management' | 'window-placement' @@ -24323,6 +24326,8 @@ export namespace Cdp { * that is incompatible with prerender and has caused the cancellation of the attempt. */ disallowedMojoInterface?: string; + + mismatchedHeaders?: PrerenderMismatchedHeaders[]; } /** @@ -24557,6 +24562,17 @@ export namespace Cdp { | 'PrefetchResponseUsed' | 'PrefetchSuccessfulButNotUsed' | 'PrefetchNotUsedProbeFailed'; + + /** + * Information of headers to be displayed when the header mismatch occurred. + */ + export interface PrerenderMismatchedHeaders { + headerName: string; + + initialValue?: string; + + activationValue?: string; + } } /** @@ -27424,6 +27440,8 @@ export namespace Cdp { controlledClients?: Target.TargetID[]; targetId?: Target.TargetID; + + routerRules?: string; } /** @@ -28700,6 +28718,16 @@ export namespace Cdp { ends: integer[]; } + export interface AttributionReportingTriggerSpec { + /** + * number instead of integer because not all uint32 can be represented by + * int + */ + triggerData: number[]; + + eventReportWindows: AttributionReportingEventReportWindows; + } + export type AttributionReportingTriggerDataMatching = 'exact' | 'modulus'; export interface AttributionReportingSourceRegistration { @@ -28710,7 +28738,7 @@ export namespace Cdp { */ expiry: integer; - eventReportWindows: AttributionReportingEventReportWindows; + triggerSpecs: AttributionReportingTriggerSpec[]; /** * duration in seconds diff --git a/src/common/contributionUtils.ts b/src/common/contributionUtils.ts index 93f4583e2..84f524ca4 100644 --- a/src/common/contributionUtils.ts +++ b/src/common/contributionUtils.ts @@ -16,16 +16,20 @@ import type { IStartProfileArguments } from '../ui/profiling/uiProfileManager'; export const enum Contributions { BrowserBreakpointsView = 'jsBrowserBreakpoints', + XHRFetchBreakpointsView = 'jsXHRBreakpoints', DiagnosticsView = 'jsDebugDiagnostics', } export const enum CustomViews { EventListenerBreakpoints = 'jsBrowserBreakpoints', + XHRFetchBreakpoints = 'jsXHRBreakpoints', ExcludedCallers = 'jsExcludedCallers', } export const enum Commands { ToggleCustomBreakpoints = 'extension.js-debug.addCustomBreakpoints', + AddXHRBreakpoints = 'extension.js-debug.addXHRBreakpoints', + EditXHRBreakpoint = 'extension.js-debug.editXHRBreakpoints', AttachProcess = 'extension.pwa-node-debug.attachNodeProcess', AutoAttachClearVariables = 'extension.js-debug.clearAutoAttachVariables', AutoAttachSetVariables = 'extension.js-debug.setAutoAttachVariables', @@ -38,6 +42,7 @@ export const enum Commands { PickProcess = 'extension.js-debug.pickNodeProcess', PrettyPrint = 'extension.js-debug.prettyPrint', RemoveAllCustomBreakpoints = 'extension.js-debug.removeAllCustomBreakpoints', + RemoveXHRBreakpoints = 'extension.js-debug.removeXHRBreakpoint', RevealPage = 'extension.js-debug.revealPage', RequestCDPProxy = 'extension.js-debug.requestCDPProxy', /** Use node-debug's command so existing keybindings work */ @@ -86,6 +91,8 @@ const debugTypes: { [K in DebugType]: null } = { const commandsObj: { [K in Commands]: null } = { [Commands.ToggleCustomBreakpoints]: null, + [Commands.AddXHRBreakpoints]: null, + [Commands.EditXHRBreakpoint]: null, [Commands.AttachProcess]: null, [Commands.AutoAttachClearVariables]: null, [Commands.AutoAttachSetVariables]: null, @@ -97,6 +104,7 @@ const commandsObj: { [K in Commands]: null } = { [Commands.DebugNpmScript]: null, [Commands.PickProcess]: null, [Commands.PrettyPrint]: null, + [Commands.RemoveXHRBreakpoints]: null, [Commands.RemoveAllCustomBreakpoints]: null, [Commands.RevealPage]: null, [Commands.StartProfile]: null, diff --git a/src/common/sourceMaps/sourceMapFactory.test.ts b/src/common/sourceMaps/sourceMapFactory.test.ts index e523d8f85..6c71c4763 100644 --- a/src/common/sourceMaps/sourceMapFactory.test.ts +++ b/src/common/sourceMaps/sourceMapFactory.test.ts @@ -7,7 +7,7 @@ import { dataUriToBuffer } from 'data-uri-to-buffer'; import { stub } from 'sinon'; import { RawIndexMap, RawSourceMap } from 'source-map'; import { IResourceProvider } from '../../adapter/resourceProvider'; -import { stubbedDapApi, StubDapApi } from '../../dap/stubbedApi'; +import { StubDapApi, stubbedDapApi } from '../../dap/stubbedApi'; import { Logger } from '../logging/logger'; import { RawIndexMapUnresolved, RootSourceMapFactory } from './sourceMapFactory'; diff --git a/src/dap/api.d.ts b/src/dap/api.d.ts index e257746ab..235c672a2 100644 --- a/src/dap/api.d.ts +++ b/src/dap/api.d.ts @@ -3228,6 +3228,11 @@ export namespace Dap { * Id of breakpoints that should be enabled. */ ids: string[]; + + /** + * strings of XHR breakpoints that should be enabled. + */ + xhr: string[]; } export interface SetCustomBreakpointsResult {} diff --git a/src/dapDebugServer.ts b/src/dapDebugServer.ts index 1c57079b7..3f404d1b5 100644 --- a/src/dapDebugServer.ts +++ b/src/dapDebugServer.ts @@ -32,7 +32,8 @@ const storagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-js-debug-')); interface IInitializationCollection { setExceptionBreakpointsParams?: Dap.SetExceptionBreakpointsParams; setBreakpointsParams: { params: Dap.SetBreakpointsParams; ids: number[] }[]; - customBreakpoints: Set; + customBreakpoints: string[]; + xhrBreakpoints: string[]; initializeParams: Dap.InitializeParams; launchParams: AnyResolvingConfiguration; @@ -50,7 +51,8 @@ interface IInitializationCollection { function collectInitialize(dap: Dap.Api) { let setExceptionBreakpointsParams: Dap.SetExceptionBreakpointsParams | undefined; const setBreakpointsParams: { params: Dap.SetBreakpointsParams; ids: number[] }[] = []; - const customBreakpoints = new Set(); + let customBreakpoints: string[] = []; + let xhrBreakpoints: string[] = []; const configurationDone = getDeferred(); let lastBreakpointId = 0; let initializeParams: Dap.InitializeParams; @@ -72,8 +74,8 @@ function collectInitialize(dap: Dap.Api) { }); dap.on('setCustomBreakpoints', async params => { - customBreakpoints.clear(); - for (const id of params.ids) customBreakpoints.add(id); + customBreakpoints = params.ids; + xhrBreakpoints = params.xhr; return {}; }); @@ -115,6 +117,7 @@ function collectInitialize(dap: Dap.Api) { setExceptionBreakpointsParams, setBreakpointsParams, customBreakpoints, + xhrBreakpoints, launchParams: launchParams as AnyResolvingConfiguration, deferred, }); @@ -185,7 +188,10 @@ class DapSessionManager implements IBinderDelegate { await adapter.setExceptionBreakpoints(init.setExceptionBreakpointsParams); for (const { params, ids } of init.setBreakpointsParams) await adapter.breakpointManager.setBreakpoints(params, ids); - await adapter.setCustomBreakpoints({ ids: Array.from(init.customBreakpoints) }); + await adapter.setCustomBreakpoints({ + xhr: init.xhrBreakpoints, + ids: init.customBreakpoints, + }); await adapter.onInitialize(init.initializeParams); await adapter.configurationDone(); diff --git a/src/debugServer.ts b/src/debugServer.ts index 9db32de22..dd46143bb 100644 --- a/src/debugServer.ts +++ b/src/debugServer.ts @@ -23,6 +23,7 @@ class Configurator { private _setExceptionBreakpointsParams?: Dap.SetExceptionBreakpointsParams; private _setBreakpointsParams: { params: Dap.SetBreakpointsParams; ids: number[] }[]; private _customBreakpoints: string[] = []; + private _xhrBreakpoints: string[] = []; private lastBreakpointId = 0; constructor(dap: Dap.Api) { @@ -49,6 +50,7 @@ class Configurator { dap.on('setCustomBreakpoints', async params => { this._customBreakpoints = params.ids; + this._xhrBreakpoints = params.xhr; return {}; }); @@ -70,7 +72,10 @@ class Configurator { await adapter.setExceptionBreakpoints(this._setExceptionBreakpointsParams); for (const { params, ids } of this._setBreakpointsParams) await adapter.breakpointManager.setBreakpoints(params, ids); - await adapter.setCustomBreakpoints({ ids: Array.from(this._customBreakpoints) }); + await adapter.setCustomBreakpoints({ + xhr: this._xhrBreakpoints, + ids: this._customBreakpoints, + }); await adapter.configurationDone(); } } diff --git a/src/targets/browser/browserTargets.ts b/src/targets/browser/browserTargets.ts index 18fbba612..2c73751c4 100644 --- a/src/targets/browser/browserTargets.ts +++ b/src/targets/browser/browserTargets.ts @@ -236,6 +236,10 @@ export class BrowserTarget implements ITarget { return domDebuggerTypes.has(this.type()); } + supportsXHRBreakpoints(): boolean { + return domDebuggerTypes.has(this.type()); + } + scriptUrlToUrl(url: string): string { return urlUtils.completeUrl(this._targetInfo.url, url) || url; } diff --git a/src/targets/node/nodeTarget.ts b/src/targets/node/nodeTarget.ts index fb9769b43..1711ea6b4 100644 --- a/src/targets/node/nodeTarget.ts +++ b/src/targets/node/nodeTarget.ts @@ -109,6 +109,10 @@ export class NodeTarget implements ITarget { return false; } + supportsXHRBreakpoints(): boolean { + return false; + } + executionContextName(): string { return this._targetName; } diff --git a/src/targets/node/nodeWorkerTarget.ts b/src/targets/node/nodeWorkerTarget.ts index a8b466442..070fc5214 100644 --- a/src/targets/node/nodeWorkerTarget.ts +++ b/src/targets/node/nodeWorkerTarget.ts @@ -116,6 +116,10 @@ export class NodeWorkerTarget implements ITarget { return false; } + supportsXHRBreakpoints(): boolean { + return false; + } + scriptUrlToUrl(url: string): string { // copied from NodeTarget. Todo: should be merged into the path resolver logic const isPath = diff --git a/src/targets/targets.ts b/src/targets/targets.ts index db116401f..be8763ccd 100644 --- a/src/targets/targets.ts +++ b/src/targets/targets.ts @@ -79,6 +79,7 @@ export interface ITarget { initialize(): Promise; waitingForDebugger(): boolean; supportsCustomBreakpoints(): boolean; + supportsXHRBreakpoints(): boolean; scriptUrlToUrl(url: string): string; executionContextName(context: Cdp.Runtime.ExecutionContextDescription): string; entryBreakpoint?: IBreakpointPathAndId | undefined; diff --git a/src/test/breakpoints/breakpointsTest.ts b/src/test/breakpoints/breakpointsTest.ts index 63afd8352..05dc14162 100644 --- a/src/test/breakpoints/breakpointsTest.ts +++ b/src/test/breakpoints/breakpointsTest.ts @@ -642,7 +642,7 @@ describe('breakpoints', () => { await p.evaluate(`document.querySelector('div').innerHTML = 'foo';`); p.log('Pausing on innerHTML'); - await p.dap.setCustomBreakpoints({ ids: ['instrumentation:Element.setInnerHTML'] }); + await p.dap.setCustomBreakpoints({ ids: ['instrumentation:Element.setInnerHTML'], xhr: [] }); p.evaluate(`document.querySelector('div').innerHTML = 'bar';`); const event = p.log(await p.dap.once('stopped')); p.log(await p.dap.continue({ threadId: event.threadId })); diff --git a/src/ui/customBreakpointsUI.ts b/src/ui/customBreakpointsUI.ts index e50f342fb..537edaae0 100644 --- a/src/ui/customBreakpointsUI.ts +++ b/src/ui/customBreakpointsUI.ts @@ -3,17 +3,40 @@ *--------------------------------------------------------*/ import * as vscode from 'vscode'; -import { - CustomBreakpointId, - customBreakpoints, - ICustomBreakpoint, -} from '../adapter/customBreakpoints'; +import { l10n } from 'vscode'; +import { customBreakpoints, ICustomBreakpoint, IXHRBreakpoint } from '../adapter/customBreakpoints'; import { Commands, CustomViews } from '../common/contributionUtils'; import { EventEmitter } from '../common/events'; import { DebugSessionTracker } from './debugSessionTracker'; +const xhrBreakpointsCategory = () => l10n.t('XHR/Fetch URLs'); + +class XHRBreakpoint extends vscode.TreeItem { + public get checked() { + return this.checkboxState === vscode.TreeItemCheckboxState.Checked; + } + + match: string; + constructor(xhr: IXHRBreakpoint, enabled: boolean) { + super( + xhr.match ? l10n.t('URL contains "{0}"', xhr.match) : l10n.t('Any XHR or fetch'), + vscode.TreeItemCollapsibleState.None, + ); + this.contextValue = 'xhrBreakpoint'; + this.id = xhr.match; + this.match = xhr.match; + this.checkboxState = enabled + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked; + } + + static compare(a: XHRBreakpoint, b: XHRBreakpoint) { + return a.match.localeCompare(b.match); + } +} + class Breakpoint extends vscode.TreeItem { - id: CustomBreakpointId; + id: string; group: string; public get checked() { @@ -45,13 +68,16 @@ class Category extends vscode.TreeItem { } } -class BreakpointsDataProvider implements vscode.TreeDataProvider { +type TreeItem = Breakpoint | Category | XHRBreakpoint; + +class BreakpointsDataProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; private _debugSessionTracker: DebugSessionTracker; private readonly categories = new Map(); + xhrBreakpoints: XHRBreakpoint[] = []; /** Gets all breakpoint categories */ public get allCategories() { @@ -73,6 +99,13 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider { if (!DebugSessionTracker.isConcreteSession(session)) { @@ -84,22 +117,31 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider b.checkboxState).map(b => b.id), + ids: toEnable, + }); }); } /** @inheritdoc */ - getTreeItem(item: Breakpoint | Category): vscode.TreeItem { + getTreeItem(item: TreeItem): vscode.TreeItem { return item; } /** @inheritdoc */ - getChildren(item?: Breakpoint | Category): vscode.ProviderResult<(Breakpoint | Category)[]> { + getChildren(item?: TreeItem): vscode.ProviderResult { if (!item) { - return [...this.categories.values()]; + return [...this.categories.values()].sort((a, b) => a.label.localeCompare(b.label)); } if (item instanceof Category) { + if (item.contextValue === 'xhrCategory') { + const title = l10n.t('Add new URL...'); + const addNew = new vscode.TreeItem(title) as XHRBreakpoint; + addNew.command = { title, command: Commands.AddXHRBreakpoints }; + return [...this.xhrBreakpoints, addNew]; + } return this.categories.get(item.label)?.children; } @@ -107,16 +149,18 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider { + getParent(item: TreeItem): vscode.ProviderResult { if (item instanceof Breakpoint) { return this.categories.get(item.group); + } else if (item instanceof XHRBreakpoint) { + return this.categories.get(xhrBreakpointsCategory()); } return undefined; } /** Updates the enablement state of the breakpoints/categories */ - public setEnabled(breakpoints: [Breakpoint | Category, boolean][]) { + public setEnabled(breakpoints: [TreeItem, boolean][]) { for (const [breakpoint, enabled] of breakpoints) { const state = enabled ? vscode.TreeItemCheckboxState.Checked @@ -125,27 +169,73 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider c.checked)) { - breakpoint.parent.checkboxState = state; + } else if (breakpoint instanceof Breakpoint || breakpoint instanceof XHRBreakpoint) { + const parent = this.getParent(breakpoint) as Category; + if (!enabled && parent.checked) { + parent.checkboxState = state; + } else if ( + enabled && + (this.getChildren(parent) as XHRBreakpoint[]).every( + c => c.checked || c.checkboxState == undefined, + ) + ) { + parent.checkboxState = state; } } } + this.updateDebuggersState(); + this._onDidChangeTreeData.fire(undefined); + } + + private updateDebuggersState() { const ids = this.allBreakpoints.filter(b => b.checked).map(b => b.id); + const xhr = this.xhrBreakpoints.filter(b => b.checked).map(b => b.id); for (const session of this._debugSessionTracker.getConcreteSessions()) { - session.customRequest('setCustomBreakpoints', { ids }); + session.customRequest('setCustomBreakpoints', { xhr, ids }); } + } + + addXHRBreakpoints(breakpoint: XHRBreakpoint) { + if (this.xhrBreakpoints.some(b => b.id === breakpoint.id)) { + return; + } + + this.xhrBreakpoints.push(breakpoint); + this.updateDebuggersState(); + this.syncXHRCategoryState(); + this._onDidChangeTreeData.fire(undefined); + } + removeXHRBreakpoint(breakpoint: XHRBreakpoint) { + this.xhrBreakpoints = this.xhrBreakpoints.filter(b => b !== breakpoint); + this.updateDebuggersState(); + this.syncXHRCategoryState(); this._onDidChangeTreeData.fire(undefined); } + + syncXHRCategoryState() { + const category = this.categories.get(xhrBreakpointsCategory()); + if (!category) { + return; + } + + if (!this.xhrBreakpoints.length) { + category.checkboxState = undefined; + return; + } + + category.checkboxState = this.xhrBreakpoints.every( + b => b.checkboxState === vscode.TreeItemCheckboxState.Checked, + ) + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked; + } } export function registerCustomBreakpointsUI( @@ -203,7 +293,50 @@ export function registerCustomBreakpointsUI( context.subscriptions.push( vscode.commands.registerCommand(Commands.RemoveAllCustomBreakpoints, () => { - provider.setEnabled(provider.allBreakpoints.map(bp => [bp, false])); + provider.setEnabled( + [...provider.allBreakpoints, ...provider.xhrBreakpoints].map(bp => [bp, false]), + ); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand(Commands.AddXHRBreakpoints, () => { + const inputBox = vscode.window.createInputBox(); + inputBox.title = l10n.t('Add XHR Breakpoint'); + inputBox.placeholder = l10n.t('Break when URL Contains'); + inputBox.onDidAccept(() => { + const match = inputBox.value; + provider.addXHRBreakpoints(new XHRBreakpoint({ match }, true)); + inputBox.dispose(); + }); + inputBox.show(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand(Commands.EditXHRBreakpoint, (treeItem: vscode.TreeItem) => { + const inputBox = vscode.window.createInputBox(); + inputBox.title = l10n.t('Edit XHR Breakpoint'); + inputBox.placeholder = l10n.t('Enter a URL or a pattern to match'); + inputBox.value = (treeItem as XHRBreakpoint).match; + inputBox.onDidAccept(() => { + const match = inputBox.value; + provider.removeXHRBreakpoint(treeItem as XHRBreakpoint); + provider.addXHRBreakpoints( + new XHRBreakpoint( + { match }, + treeItem.checkboxState == vscode.TreeItemCheckboxState.Checked, + ), + ); + inputBox.dispose(); + }); + inputBox.show(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand(Commands.RemoveXHRBreakpoints, (treeItem: vscode.TreeItem) => { + provider.removeXHRBreakpoint(treeItem as XHRBreakpoint); }), ); }