Skip to content

Commit 85c9047

Browse files
committed
feat: improve display of HTML in the debugger
Advertises ANSI styles as proposed in microsoft/debug-adapter-protocol#500, though it works without them too, just without colors! Previously the Nodes were displayed as naive objects, so we'd just list their properties, which was quite useless when trying to get a handle on the DOM. Now we display their children as the primary element display, and have "Node Attributes" in a separate section. ![](https://memes.peet.io/img/24-09-9b2b35e1-3874-4e06-825a-5c84abeeb6e4.png) Refs microsoft/vscode#227729
1 parent 9419f57 commit 85c9047

19 files changed

+286
-10
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he
44

55
## Nightly (only)
66

7+
- feat: improve display of HTML elements in the debugger
78
- feat: add node tool picker completion for launch.json ([#1997](https://github.com/microsoft/vscode-js-debug/issues/1997))
89
- fix: process attachment with `--inspect=:1234` style ([#2063](https://github.com/microsoft/vscode-js-debug/issues/2063))
910

src/adapter/clientCapabilities.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { injectable } from 'inversify';
6+
import Dap from '../dap/api';
7+
8+
export interface IClientCapabilies {
9+
value?: Dap.InitializeParams;
10+
}
11+
12+
export const IClientCapabilies = Symbol('IClientCapabilies');
13+
14+
@injectable()
15+
export class ClientCapabilities implements IClientCapabilies {
16+
value?: Dap.InitializeParams | undefined;
17+
}

src/adapter/debugAdapter.ts

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { IShutdownParticipants } from '../ui/shutdownParticipants';
2424
import { IAsyncStackPolicy } from './asyncStackPolicy';
2525
import { BreakpointManager } from './breakpoints';
2626
import { ICdpProxyProvider } from './cdpProxy';
27+
import { IClientCapabilies } from './clientCapabilities';
2728
import { ICompletions } from './completions';
2829
import { IConsole } from './console';
2930
import { Diagnostics } from './diagnosics';
@@ -250,6 +251,7 @@ export class DebugAdapter implements IDisposable {
250251
): Promise<Dap.InitializeResult | Dap.Error> {
251252
console.assert(params.linesStartAt1);
252253
console.assert(params.columnsStartAt1);
254+
this._services.get<IClientCapabilies>(IClientCapabilies).value = params;
253255
const capabilities = DebugAdapter.capabilities(true);
254256
setTimeout(() => this.dap.initialized({}), 0);
255257
setTimeout(() => this._thread?.dapInitialized(), 0);
@@ -310,6 +312,7 @@ export class DebugAdapter implements IDisposable {
310312
supportsEvaluationOptions: extended ? true : false,
311313
supportsDebuggerProperties: extended ? true : false,
312314
supportsSetSymbolOptions: extended ? true : false,
315+
supportsANSIStyling: true,
313316
// supportsDataBreakpoints: false,
314317
// supportsDisassembleRequest: false,
315318
};
@@ -515,6 +518,7 @@ export class DebugAdapter implements IDisposable {
515518
this._services.get(IExceptionPauseService),
516519
this._services.get(SmartStepper),
517520
this._services.get(IShutdownParticipants),
521+
this._services.get(IClientCapabilies),
518522
);
519523

520524
const profile = this._services.get<IProfileController>(IProfileController);

src/adapter/messageFormat.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,13 @@ export function formatCssAsAnsi(style: string): string {
148148
if (background) escapedSequence += `\x1b[48;5;${background}m`;
149149
break;
150150
case 'font-weight':
151-
if (match[2] === 'bold') escapedSequence += '\x1b[1m';
151+
if (match[2] === 'bold') escapedSequence += AnsiStyles.Bold;
152152
break;
153153
case 'font-style':
154-
if (match[2] === 'italic') escapedSequence += '\x1b[3m';
154+
if (match[2] === 'italic') escapedSequence += AnsiStyles.Italic;
155155
break;
156156
case 'text-decoration':
157-
if (match[2] === 'underline') escapedSequence += '\x1b[4m';
157+
if (match[2] === 'underline') escapedSequence += AnsiStyles.Underline;
158158
break;
159159
default:
160160
// css not mapped, skip
@@ -166,3 +166,31 @@ export function formatCssAsAnsi(style: string): string {
166166

167167
return escapedSequence;
168168
}
169+
170+
export const enum AnsiStyles {
171+
Reset = '\x1b[0m',
172+
Bold = '\x1b[1m',
173+
Dim = '\x1b[2m',
174+
Italic = '\x1b[3m',
175+
Underline = '\x1b[4m',
176+
Blink = '\x1b[5m',
177+
Reverse = '\x1b[7m',
178+
Hidden = '\x1b[8m',
179+
Strikethrough = '\x1b[9m',
180+
Black = '\x1b[30m',
181+
Red = '\x1b[31m',
182+
Green = '\x1b[32m',
183+
Yellow = '\x1b[33m',
184+
Blue = '\x1b[34m',
185+
Magenta = '\x1b[35m',
186+
Cyan = '\x1b[36m',
187+
White = '\x1b[37m',
188+
BrightBlack = '\x1b[30;1m',
189+
BrightRed = '\x1b[31;1m',
190+
BrightGreen = '\x1b[32;1m',
191+
BrightYellow = '\x1b[33;1m',
192+
BrightBlue = '\x1b[34;1m',
193+
BrightMagenta = '\x1b[35;1m',
194+
BrightCyan = '\x1b[36;1m',
195+
BrightWhite = '\x1b[37;1m',
196+
}

src/adapter/objectPreview/index.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,12 @@ function appendKeyValue(
314314
if (key.length + separator.length > characterBudget) {
315315
return stringUtils.trimEnd(key, characterBudget);
316316
}
317-
return `${key}${separator}${
317+
return escapeAnsiInString(`${key}${separator}${
318318
stringUtils.trimMiddle(
319319
value,
320320
characterBudget - key.length - separator.length,
321321
)
322-
}`; // Keep in sync with characterBudget calculation.
322+
}`); // Keep in sync with characterBudget calculation.
323323
}
324324

325325
function renderPropertyPreview(
@@ -341,6 +341,10 @@ function renderPropertyPreview(
341341
return appendKeyValue(name, ': ', prop.value ?? 'unknown', characterBudget);
342342
}
343343

344+
function escapeAnsiInString(value: string) {
345+
return value.replaceAll('\x1b', '\\x1b');
346+
}
347+
344348
function quoteStringValue(value: string) {
345349
// Try a quote style that doesn't appear in the string, preferring/falling back to single quotes
346350
const quoteStyle = value.includes("'")
@@ -368,7 +372,7 @@ function renderValue(
368372
quote = false;
369373
}
370374
const value = stringUtils.trimMiddle(stringValue, quote ? budget - 2 : budget);
371-
return quote ? quoteStringValue(value) : value;
375+
return escapeAnsiInString(quote ? quoteStringValue(value) : value);
372376
}
373377

374378
if (object.type === 'undefined') {
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { remoteFunction } from '.';
6+
7+
/**
8+
* Returns an object containing array property descriptors for the given
9+
* range of array indices.
10+
*/
11+
export const getNodeChildren = remoteFunction(function(
12+
this: Node,
13+
start: number,
14+
count: number,
15+
) {
16+
const result: Record<number, Node | string> = {};
17+
const from = start === -1 ? 0 : start;
18+
const to = count === -1 ? this.childNodes.length : start + count;
19+
for (let i = from; i < to && i < this.childNodes.length; ++i) {
20+
const cn = this.childNodes[i];
21+
result[i] = cn.nodeName === '#text' ? (cn.textContent || '') : this.childNodes[i];
22+
}
23+
24+
return result;
25+
});

src/adapter/threads.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ITarget } from '../targets/targets';
2727
import { IShutdownParticipants } from '../ui/shutdownParticipants';
2828
import { BreakpointManager, EntryBreakpointMode } from './breakpoints';
2929
import { UserDefinedBreakpoint } from './breakpoints/userDefinedBreakpoint';
30+
import { IClientCapabilies } from './clientCapabilities';
3031
import { ICompletions } from './completions';
3132
import { ExceptionMessage, IConsole, QueryObjectsMessage } from './console';
3233
import { customBreakpoints } from './customBreakpoints';
@@ -221,12 +222,20 @@ export class Thread implements IVariableStoreLocationProvider {
221222
private readonly exceptionPause: IExceptionPauseService,
222223
private readonly _smartStepper: SmartStepper,
223224
private readonly shutdown: IShutdownParticipants,
225+
clientCapabilities: IClientCapabilies,
224226
) {
225227
this._dap = new DeferredContainer(dap);
226228
this._sourceContainer = sourceContainer;
227229
this._cdp = cdp;
228230
this.id = Thread._lastThreadId++;
229-
this.replVariables = new VariableStore(renameProvider, this._cdp, dap, launchConfig, this);
231+
this.replVariables = new VariableStore(
232+
renameProvider,
233+
this._cdp,
234+
dap,
235+
launchConfig,
236+
clientCapabilities,
237+
this,
238+
);
230239
sourceContainer.onSourceMappedSteppingChange(() => this.refreshStackTrace());
231240
this._initialize();
232241
}

src/adapter/variableStore.ts

+124
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ import Dap from '../dap/api';
1515
import { IDapApi } from '../dap/connection';
1616
import * as errors from '../dap/errors';
1717
import { ProtocolError } from '../dap/protocolError';
18+
import { ClientCapabilities, IClientCapabilies } from './clientCapabilities';
1819
import { IWasmVariable, IWasmVariableEvaluation, WasmScope } from './dwarf/wasmSymbolProvider';
20+
import { AnsiStyles } from './messageFormat';
1921
import * as objectPreview from './objectPreview';
2022
import { MapPreview, SetPreview } from './objectPreview/betterTypes';
2123
import { PreviewContextType } from './objectPreview/contexts';
2224
import { StackFrame, StackTrace } from './stackTrace';
2325
import { getSourceSuffix, RemoteException, RemoteObjectId } from './templates';
2426
import { getArrayProperties } from './templates/getArrayProperties';
2527
import { getArraySlots } from './templates/getArraySlots';
28+
import { getNodeChildren } from './templates/getNodeChildren';
2629
import {
2730
getDescriptionSymbols,
2831
getStringyProps,
@@ -204,6 +207,7 @@ class VariableContext {
204207
public readonly locationProvider: IVariableStoreLocationProvider,
205208
private readonly currentRef: undefined | (() => IVariable | Scope),
206209
private readonly settings: IContextSettings,
210+
public readonly clientCapabilities: IClientCapabilies,
207211
) {
208212
this.name = ctx.name;
209213
this.presentationHint = ctx.presentationHint;
@@ -247,6 +251,7 @@ class VariableContext {
247251
this.locationProvider,
248252
() => v,
249253
this.settings,
254+
this.clientCapabilities,
250255
),
251256
...rest,
252257
) as InstanceType<T>;
@@ -272,6 +277,8 @@ class VariableContext {
272277
return this.createVariable(FunctionVariable, ctx, object, customStringRepr);
273278
} else if (object.subtype === 'map' || object.subtype === 'set') {
274279
return this.createVariable(SetOrMapVariable, ctx, object, customStringRepr);
280+
} else if (object.subtype === 'node') {
281+
return this.createVariable(NodeVariable, ctx, object, customStringRepr);
275282
} else if (!objectPreview.subtypesWithoutPreview.has(object.subtype)) {
276283
return this.createVariable(ObjectVariable, ctx, object, customStringRepr);
277284
}
@@ -851,6 +858,118 @@ class ObjectVariable extends Variable implements IMemoryReadable {
851858
}
852859
}
853860

861+
class NodeAttributes extends ObjectVariable {
862+
public readonly id = getVariableId();
863+
864+
override get accessor(): string {
865+
return (this.context.parent as NodeVariable).accessor;
866+
}
867+
868+
public override async toDap(
869+
context: PreviewContextType,
870+
valueFormat?: Dap.ValueFormat,
871+
): Promise<Dap.Variable> {
872+
return Promise.resolve({
873+
...await super.toDap(context, valueFormat),
874+
value: '...',
875+
});
876+
}
877+
}
878+
879+
class NodeVariable extends Variable {
880+
public override async toDap(
881+
previewContext: PreviewContextType,
882+
valueFormat?: Dap.ValueFormat,
883+
): Promise<Dap.Variable> {
884+
const description = await this.description();
885+
const length = description?.node?.childNodeCount || 0;
886+
return {
887+
...await super.toDap(previewContext, valueFormat),
888+
value: await this.getValuePreview(previewContext),
889+
variablesReference: this.id,
890+
indexedVariables: length > 100 ? length : undefined,
891+
namedVariables: length > 100 ? 1 : undefined,
892+
};
893+
}
894+
895+
public override async getChildren(params?: Dap.VariablesParamsExtended): Promise<Variable[]> {
896+
switch (params?.filter) {
897+
case 'indexed':
898+
return this.getNodeChildren(params.start, params.count);
899+
case 'named':
900+
return [this.getAttributesVar()];
901+
default:
902+
return [this.getAttributesVar(), ...(await this.getNodeChildren())];
903+
}
904+
}
905+
906+
private getAttributesVar() {
907+
return this.context.createVariable(
908+
NodeAttributes,
909+
{
910+
name: l10n.t('Node Attributes'),
911+
presentationHint: { visibility: 'internal' },
912+
sortOrder: Number.MAX_SAFE_INTEGER,
913+
},
914+
this.remoteObject,
915+
undefined,
916+
);
917+
}
918+
919+
private async getNodeChildren(start = -1, count = -1) {
920+
let slotsObject: Cdp.Runtime.RemoteObject;
921+
try {
922+
slotsObject = await getNodeChildren({
923+
cdp: this.context.cdp,
924+
generatePreview: false,
925+
args: [start, count],
926+
objectId: this.remoteObject.objectId,
927+
});
928+
} catch (e) {
929+
return [];
930+
}
931+
932+
const result = await this.context.createObjectPropertyVars(slotsObject);
933+
if (slotsObject.objectId) {
934+
await this.context.cdp.Runtime.releaseObject({ objectId: slotsObject.objectId });
935+
}
936+
937+
return result;
938+
}
939+
940+
private readonly description = once(() =>
941+
this.context.cdp.DOM.describeNode({
942+
objectId: this.remoteObject.objectId,
943+
})
944+
);
945+
946+
private async getValuePreview(_previewContext: PreviewContextType) {
947+
const description = await this.description();
948+
if (!description?.node) {
949+
return '';
950+
}
951+
952+
const { localName, attributes, childNodeCount } = description.node;
953+
const styleCheck = this.context.clientCapabilities.value?.supportsANSIStyling ? true : '';
954+
let str = (styleCheck && AnsiStyles.Blue) + `<${localName}`;
955+
if (attributes) {
956+
for (let i = 0; i < attributes.length; i += 2) {
957+
const key = attributes[i];
958+
const value = attributes[i + 1];
959+
str += ` ${(styleCheck && AnsiStyles.BrightBlue)}${key}${(styleCheck
960+
&& AnsiStyles.Dim)}=${(styleCheck && AnsiStyles.Yellow)}${JSON.stringify(value)}`;
961+
}
962+
}
963+
str += (styleCheck && AnsiStyles.Blue) + '>';
964+
if (childNodeCount) {
965+
str += `${(styleCheck && AnsiStyles.Dim)}...${(styleCheck && AnsiStyles.Blue)}`;
966+
}
967+
str += `</${localName}>${(styleCheck && AnsiStyles.Reset)}`;
968+
969+
return str;
970+
}
971+
}
972+
854973
class FunctionVariable extends ObjectVariable {
855974
private readonly baseChildren = once(() => super.getChildren({ variablesReference: this.id }));
856975

@@ -1272,6 +1391,7 @@ export class VariableStore {
12721391
@inject(ICdpApi) private readonly cdp: Cdp.Api,
12731392
@inject(IDapApi) private readonly dap: Dap.Api,
12741393
@inject(AnyLaunchConfiguration) private readonly launchConfig: AnyLaunchConfiguration,
1394+
@inject(ClientCapabilities) private readonly clientCapabilities: ClientCapabilities,
12751395
private readonly locationProvider: IVariableStoreLocationProvider,
12761396
) {
12771397
this.contextSettings = {
@@ -1293,6 +1413,7 @@ export class VariableStore {
12931413
this.cdp,
12941414
this.dap,
12951415
this.launchConfig,
1416+
this.clientCapabilities,
12961417
this.locationProvider,
12971418
);
12981419
}
@@ -1345,6 +1466,7 @@ export class VariableStore {
13451466
this.locationProvider,
13461467
() => scope,
13471468
this.contextSettings,
1469+
this.clientCapabilities,
13481470
),
13491471
scopeRef,
13501472
extraProperties,
@@ -1371,6 +1493,7 @@ export class VariableStore {
13711493
this.locationProvider,
13721494
() => scope,
13731495
this.contextSettings,
1496+
this.clientCapabilities,
13741497
),
13751498
kind,
13761499
variables,
@@ -1497,6 +1620,7 @@ export class VariableStore {
14971620
this.locationProvider,
14981621
undefined,
14991622
this.contextSettings,
1623+
this.clientCapabilities,
15001624
);
15011625
}
15021626
}

0 commit comments

Comments
 (0)