Skip to content

Commit

Permalink
feat: improve display of HTML in the debugger
Browse files Browse the repository at this point in the history
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
  • Loading branch information
connor4312 committed Sep 6, 2024
1 parent 9419f57 commit 85c9047
Show file tree
Hide file tree
Showing 19 changed files with 286 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he

## Nightly (only)

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

Expand Down
17 changes: 17 additions & 0 deletions src/adapter/clientCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { injectable } from 'inversify';
import Dap from '../dap/api';

export interface IClientCapabilies {
value?: Dap.InitializeParams;
}

export const IClientCapabilies = Symbol('IClientCapabilies');

@injectable()
export class ClientCapabilities implements IClientCapabilies {
value?: Dap.InitializeParams | undefined;
}
4 changes: 4 additions & 0 deletions src/adapter/debugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IShutdownParticipants } from '../ui/shutdownParticipants';
import { IAsyncStackPolicy } from './asyncStackPolicy';
import { BreakpointManager } from './breakpoints';
import { ICdpProxyProvider } from './cdpProxy';
import { IClientCapabilies } from './clientCapabilities';
import { ICompletions } from './completions';
import { IConsole } from './console';
import { Diagnostics } from './diagnosics';
Expand Down Expand Up @@ -250,6 +251,7 @@ export class DebugAdapter implements IDisposable {
): Promise<Dap.InitializeResult | Dap.Error> {
console.assert(params.linesStartAt1);
console.assert(params.columnsStartAt1);
this._services.get<IClientCapabilies>(IClientCapabilies).value = params;
const capabilities = DebugAdapter.capabilities(true);
setTimeout(() => this.dap.initialized({}), 0);
setTimeout(() => this._thread?.dapInitialized(), 0);
Expand Down Expand Up @@ -310,6 +312,7 @@ export class DebugAdapter implements IDisposable {
supportsEvaluationOptions: extended ? true : false,
supportsDebuggerProperties: extended ? true : false,
supportsSetSymbolOptions: extended ? true : false,
supportsANSIStyling: true,
// supportsDataBreakpoints: false,
// supportsDisassembleRequest: false,
};
Expand Down Expand Up @@ -515,6 +518,7 @@ export class DebugAdapter implements IDisposable {
this._services.get(IExceptionPauseService),
this._services.get(SmartStepper),
this._services.get(IShutdownParticipants),
this._services.get(IClientCapabilies),
);

const profile = this._services.get<IProfileController>(IProfileController);
Expand Down
34 changes: 31 additions & 3 deletions src/adapter/messageFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,13 @@ export function formatCssAsAnsi(style: string): string {
if (background) escapedSequence += `\x1b[48;5;${background}m`;
break;
case 'font-weight':
if (match[2] === 'bold') escapedSequence += '\x1b[1m';
if (match[2] === 'bold') escapedSequence += AnsiStyles.Bold;
break;
case 'font-style':
if (match[2] === 'italic') escapedSequence += '\x1b[3m';
if (match[2] === 'italic') escapedSequence += AnsiStyles.Italic;
break;
case 'text-decoration':
if (match[2] === 'underline') escapedSequence += '\x1b[4m';
if (match[2] === 'underline') escapedSequence += AnsiStyles.Underline;
break;
default:
// css not mapped, skip
Expand All @@ -166,3 +166,31 @@ export function formatCssAsAnsi(style: string): string {

return escapedSequence;
}

export const enum AnsiStyles {
Reset = '\x1b[0m',
Bold = '\x1b[1m',
Dim = '\x1b[2m',
Italic = '\x1b[3m',
Underline = '\x1b[4m',
Blink = '\x1b[5m',
Reverse = '\x1b[7m',
Hidden = '\x1b[8m',
Strikethrough = '\x1b[9m',
Black = '\x1b[30m',
Red = '\x1b[31m',
Green = '\x1b[32m',
Yellow = '\x1b[33m',
Blue = '\x1b[34m',
Magenta = '\x1b[35m',
Cyan = '\x1b[36m',
White = '\x1b[37m',
BrightBlack = '\x1b[30;1m',
BrightRed = '\x1b[31;1m',
BrightGreen = '\x1b[32;1m',
BrightYellow = '\x1b[33;1m',
BrightBlue = '\x1b[34;1m',
BrightMagenta = '\x1b[35;1m',
BrightCyan = '\x1b[36;1m',
BrightWhite = '\x1b[37;1m',
}
10 changes: 7 additions & 3 deletions src/adapter/objectPreview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,12 @@ function appendKeyValue(
if (key.length + separator.length > characterBudget) {
return stringUtils.trimEnd(key, characterBudget);
}
return `${key}${separator}${
return escapeAnsiInString(`${key}${separator}${
stringUtils.trimMiddle(
value,
characterBudget - key.length - separator.length,
)
}`; // Keep in sync with characterBudget calculation.
}`); // Keep in sync with characterBudget calculation.
}

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

function escapeAnsiInString(value: string) {
return value.replaceAll('\x1b', '\\x1b');
}

function quoteStringValue(value: string) {
// Try a quote style that doesn't appear in the string, preferring/falling back to single quotes
const quoteStyle = value.includes("'")
Expand Down Expand Up @@ -368,7 +372,7 @@ function renderValue(
quote = false;
}
const value = stringUtils.trimMiddle(stringValue, quote ? budget - 2 : budget);
return quote ? quoteStringValue(value) : value;
return escapeAnsiInString(quote ? quoteStringValue(value) : value);
}

if (object.type === 'undefined') {
Expand Down
25 changes: 25 additions & 0 deletions src/adapter/templates/getNodeChildren.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { remoteFunction } from '.';

/**
* Returns an object containing array property descriptors for the given
* range of array indices.
*/
export const getNodeChildren = remoteFunction(function(
this: Node,
start: number,
count: number,
) {
const result: Record<number, Node | string> = {};
const from = start === -1 ? 0 : start;
const to = count === -1 ? this.childNodes.length : start + count;
for (let i = from; i < to && i < this.childNodes.length; ++i) {
const cn = this.childNodes[i];
result[i] = cn.nodeName === '#text' ? (cn.textContent || '') : this.childNodes[i];
}

return result;
});
11 changes: 10 additions & 1 deletion src/adapter/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ITarget } from '../targets/targets';
import { IShutdownParticipants } from '../ui/shutdownParticipants';
import { BreakpointManager, EntryBreakpointMode } from './breakpoints';
import { UserDefinedBreakpoint } from './breakpoints/userDefinedBreakpoint';
import { IClientCapabilies } from './clientCapabilities';
import { ICompletions } from './completions';
import { ExceptionMessage, IConsole, QueryObjectsMessage } from './console';
import { customBreakpoints } from './customBreakpoints';
Expand Down Expand Up @@ -221,12 +222,20 @@ export class Thread implements IVariableStoreLocationProvider {
private readonly exceptionPause: IExceptionPauseService,
private readonly _smartStepper: SmartStepper,
private readonly shutdown: IShutdownParticipants,
clientCapabilities: IClientCapabilies,
) {
this._dap = new DeferredContainer(dap);
this._sourceContainer = sourceContainer;
this._cdp = cdp;
this.id = Thread._lastThreadId++;
this.replVariables = new VariableStore(renameProvider, this._cdp, dap, launchConfig, this);
this.replVariables = new VariableStore(
renameProvider,
this._cdp,
dap,
launchConfig,
clientCapabilities,
this,
);
sourceContainer.onSourceMappedSteppingChange(() => this.refreshStackTrace());
this._initialize();
}
Expand Down
124 changes: 124 additions & 0 deletions src/adapter/variableStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import Dap from '../dap/api';
import { IDapApi } from '../dap/connection';
import * as errors from '../dap/errors';
import { ProtocolError } from '../dap/protocolError';
import { ClientCapabilities, IClientCapabilies } from './clientCapabilities';
import { IWasmVariable, IWasmVariableEvaluation, WasmScope } from './dwarf/wasmSymbolProvider';
import { AnsiStyles } from './messageFormat';
import * as objectPreview from './objectPreview';
import { MapPreview, SetPreview } from './objectPreview/betterTypes';
import { PreviewContextType } from './objectPreview/contexts';
import { StackFrame, StackTrace } from './stackTrace';
import { getSourceSuffix, RemoteException, RemoteObjectId } from './templates';
import { getArrayProperties } from './templates/getArrayProperties';
import { getArraySlots } from './templates/getArraySlots';
import { getNodeChildren } from './templates/getNodeChildren';
import {
getDescriptionSymbols,
getStringyProps,
Expand Down Expand Up @@ -204,6 +207,7 @@ class VariableContext {
public readonly locationProvider: IVariableStoreLocationProvider,
private readonly currentRef: undefined | (() => IVariable | Scope),
private readonly settings: IContextSettings,
public readonly clientCapabilities: IClientCapabilies,
) {
this.name = ctx.name;
this.presentationHint = ctx.presentationHint;
Expand Down Expand Up @@ -247,6 +251,7 @@ class VariableContext {
this.locationProvider,
() => v,
this.settings,
this.clientCapabilities,
),
...rest,
) as InstanceType<T>;
Expand All @@ -272,6 +277,8 @@ class VariableContext {
return this.createVariable(FunctionVariable, ctx, object, customStringRepr);
} else if (object.subtype === 'map' || object.subtype === 'set') {
return this.createVariable(SetOrMapVariable, ctx, object, customStringRepr);
} else if (object.subtype === 'node') {
return this.createVariable(NodeVariable, ctx, object, customStringRepr);
} else if (!objectPreview.subtypesWithoutPreview.has(object.subtype)) {
return this.createVariable(ObjectVariable, ctx, object, customStringRepr);
}
Expand Down Expand Up @@ -851,6 +858,118 @@ class ObjectVariable extends Variable implements IMemoryReadable {
}
}

class NodeAttributes extends ObjectVariable {
public readonly id = getVariableId();

override get accessor(): string {
return (this.context.parent as NodeVariable).accessor;
}

public override async toDap(
context: PreviewContextType,
valueFormat?: Dap.ValueFormat,
): Promise<Dap.Variable> {
return Promise.resolve({
...await super.toDap(context, valueFormat),
value: '...',
});
}
}

class NodeVariable extends Variable {
public override async toDap(
previewContext: PreviewContextType,
valueFormat?: Dap.ValueFormat,
): Promise<Dap.Variable> {
const description = await this.description();
const length = description?.node?.childNodeCount || 0;
return {
...await super.toDap(previewContext, valueFormat),
value: await this.getValuePreview(previewContext),
variablesReference: this.id,
indexedVariables: length > 100 ? length : undefined,
namedVariables: length > 100 ? 1 : undefined,
};
}

public override async getChildren(params?: Dap.VariablesParamsExtended): Promise<Variable[]> {
switch (params?.filter) {
case 'indexed':
return this.getNodeChildren(params.start, params.count);
case 'named':
return [this.getAttributesVar()];
default:
return [this.getAttributesVar(), ...(await this.getNodeChildren())];
}
}

private getAttributesVar() {
return this.context.createVariable(
NodeAttributes,
{
name: l10n.t('Node Attributes'),
presentationHint: { visibility: 'internal' },
sortOrder: Number.MAX_SAFE_INTEGER,
},
this.remoteObject,
undefined,
);
}

private async getNodeChildren(start = -1, count = -1) {
let slotsObject: Cdp.Runtime.RemoteObject;
try {
slotsObject = await getNodeChildren({
cdp: this.context.cdp,
generatePreview: false,
args: [start, count],
objectId: this.remoteObject.objectId,
});
} catch (e) {
return [];
}

const result = await this.context.createObjectPropertyVars(slotsObject);
if (slotsObject.objectId) {
await this.context.cdp.Runtime.releaseObject({ objectId: slotsObject.objectId });
}

return result;
}

private readonly description = once(() =>
this.context.cdp.DOM.describeNode({
objectId: this.remoteObject.objectId,
})
);

private async getValuePreview(_previewContext: PreviewContextType) {
const description = await this.description();
if (!description?.node) {
return '';
}

const { localName, attributes, childNodeCount } = description.node;
const styleCheck = this.context.clientCapabilities.value?.supportsANSIStyling ? true : '';
let str = (styleCheck && AnsiStyles.Blue) + `<${localName}`;
if (attributes) {
for (let i = 0; i < attributes.length; i += 2) {
const key = attributes[i];
const value = attributes[i + 1];
str += ` ${(styleCheck && AnsiStyles.BrightBlue)}${key}${(styleCheck
&& AnsiStyles.Dim)}=${(styleCheck && AnsiStyles.Yellow)}${JSON.stringify(value)}`;
}
}
str += (styleCheck && AnsiStyles.Blue) + '>';
if (childNodeCount) {
str += `${(styleCheck && AnsiStyles.Dim)}...${(styleCheck && AnsiStyles.Blue)}`;
}
str += `</${localName}>${(styleCheck && AnsiStyles.Reset)}`;

return str;
}
}

class FunctionVariable extends ObjectVariable {
private readonly baseChildren = once(() => super.getChildren({ variablesReference: this.id }));

Expand Down Expand Up @@ -1272,6 +1391,7 @@ export class VariableStore {
@inject(ICdpApi) private readonly cdp: Cdp.Api,
@inject(IDapApi) private readonly dap: Dap.Api,
@inject(AnyLaunchConfiguration) private readonly launchConfig: AnyLaunchConfiguration,
@inject(ClientCapabilities) private readonly clientCapabilities: ClientCapabilities,
private readonly locationProvider: IVariableStoreLocationProvider,
) {
this.contextSettings = {
Expand All @@ -1293,6 +1413,7 @@ export class VariableStore {
this.cdp,
this.dap,
this.launchConfig,
this.clientCapabilities,
this.locationProvider,
);
}
Expand Down Expand Up @@ -1345,6 +1466,7 @@ export class VariableStore {
this.locationProvider,
() => scope,
this.contextSettings,
this.clientCapabilities,
),
scopeRef,
extraProperties,
Expand All @@ -1371,6 +1493,7 @@ export class VariableStore {
this.locationProvider,
() => scope,
this.contextSettings,
this.clientCapabilities,
),
kind,
variables,
Expand Down Expand Up @@ -1497,6 +1620,7 @@ export class VariableStore {
this.locationProvider,
undefined,
this.contextSettings,
this.clientCapabilities,
);
}
}
Expand Down
Loading

0 comments on commit 85c9047

Please sign in to comment.