From 339c332749c13a5639848f6451342aeff7fb22b3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sat, 10 Jun 2023 10:30:10 -0700 Subject: [PATCH] fix: improve previews of objects and sets (#1726) Objects and sets now show their values under the first expansion instead of requiring the user to expand an additional level. Also, their accessors are fixed which allows "copy value" and "copy as expression"/ ![](https://memes.peet.io/img/23-06-2bb71702-f805-46d8-aff2-6cf3b8dd40c1.png) --- CHANGELOG.md | 1 + src/adapter/objectPreview/betterTypes.ts | 46 ++++++++++++- src/adapter/objectPreview/index.ts | 2 +- src/adapter/variableStore.ts | 64 +++++++++++++++++-- src/test/evaluate/evaluate-repl.txt | 2 +- ...evaluate-supports-bigint-map-keys-1277.txt | 3 +- 6 files changed, 109 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81db4d5ef..ca495cf32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he - fix: terminal launches sometimes sending commands too soon ([#1642](https://github.com/microsoft/vscode-js-debug/issues/1642)) - fix: step into `eval` when `pauseForSourceMap` is true does not pause on next available line ([#1692](https://github.com/microsoft/vscode-js-debug/issues/1692)) - fix: useWebview debug sessions getting stuck if program exits without attaching ([#1666](https://github.com/microsoft/vscode-js-debug/issues/1666)) +- fix: improve the display of map and set entries - fix: do not to translate "promise rejection" ([#1658](https://github.com/microsoft/vscode-js-debug/issues/1658)) - fix: breakpoints not hitting early on in nested sourcemapped programs ([#1704](https://github.com/microsoft/vscode-js-debug/issues/1704)) - fix: sourcemap predictor not filtering nested session on windows ([#1719](https://github.com/microsoft/vscode-js-debug/issues/1719)) diff --git a/src/adapter/objectPreview/betterTypes.ts b/src/adapter/objectPreview/betterTypes.ts index 13b390658..06b1b1a48 100644 --- a/src/adapter/objectPreview/betterTypes.ts +++ b/src/adapter/objectPreview/betterTypes.ts @@ -27,7 +27,14 @@ export type TFunction = { // defined in V8, undefined in Hermes description?: string; }; -export type FunctionPreview = { type: 'function'; subtype: undefined; description: string }; +export type FunctionPreview = { + type: 'function'; + subtype: undefined; + description: string; + entries?: undefined; + properties?: undefined; + overflow?: undefined; +}; export type FunctionObj = TFunction; export type TNode = { @@ -38,6 +45,32 @@ export type TNode = { export type NodePreview = TNode & ObjectPreview; export type NodeObj = TNode & { preview: NodePreview }; +export type TSet = { + type: 'object'; + subtype: 'set'; + className: string; + description: string; +}; +export type SetPreview = TSet & { + entries: { key?: undefined; value: AnyPreview }[]; + properties: PropertyPreview[]; + overflow: boolean; +}; +export type SetObj = TSet & { preview: SetPreview }; + +export type TMap = { + type: 'object'; + subtype: 'map'; + className: string; + description: string; +}; +export type MapPreview = TMap & { + entries: { key: AnyPreview; value: AnyPreview }[]; + properties: PropertyPreview[]; + overflow: boolean; +}; +export type MapObj = TMap & { preview: MapPreview }; + export type TString = { type: 'string'; value: string; @@ -110,6 +143,8 @@ export type AnyObject = | ObjectObj | NodeObj | ArrayObj + | SetObj + | MapObj | ErrorObj | RegExpObj | FunctionObj @@ -120,6 +155,8 @@ export type AnyObject = | NullObj; export type AnyPreview = | ObjectPreview + | SetPreview + | MapPreview | NodePreview | ArrayPreview | ErrorPreview @@ -130,7 +167,12 @@ export type AnyPreview = | UndefinedPreview | NullPreview; -export type PreviewAsObjectType = NodePreview | FunctionPreview | ObjectPreview; +export type PreviewAsObjectType = + | NodePreview + | FunctionPreview + | ObjectPreview + | MapPreview + | SetPreview; export type Numeric = NumberPreview | BigintPreview | TSpecialNumber; export type Primitive = | NullPreview diff --git a/src/adapter/objectPreview/index.ts b/src/adapter/objectPreview/index.ts index f6f78943d..4d532779a 100644 --- a/src/adapter/objectPreview/index.ts +++ b/src/adapter/objectPreview/index.ts @@ -169,7 +169,7 @@ function renderArrayPreview(preview: ObjectPreview.ArrayPreview, characterBudget } function renderObjectPreview( - preview: ObjectPreview.ObjectPreview | ObjectPreview.NodePreview, + preview: ObjectPreview.PreviewAsObjectType, characterBudget: number, format: Dap.ValueFormat | undefined, ): string { diff --git a/src/adapter/variableStore.ts b/src/adapter/variableStore.ts index 06b38fce7..c643021d6 100644 --- a/src/adapter/variableStore.ts +++ b/src/adapter/variableStore.ts @@ -7,7 +7,7 @@ import { generate } from 'astring'; import { inject, injectable } from 'inversify'; import Cdp from '../cdp/api'; import { ICdpApi } from '../cdp/connection'; -import { flatten, isInstanceOf } from '../common/objUtils'; +import { flatten, isInstanceOf, once } from '../common/objUtils'; import { parseSource, statementsToFunction } from '../common/sourceCodeManipulations'; import { IRenameProvider } from '../common/sourceMaps/renameProvider'; import { AnyLaunchConfiguration } from '../configuration'; @@ -16,6 +16,7 @@ import { IDapApi } from '../dap/connection'; import * as errors from '../dap/errors'; import { ProtocolError } from '../dap/protocolError'; 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'; @@ -254,8 +255,12 @@ class VariableContext { return this.createVariable(ArrayVariable, ctx, object); } - if (object.objectId && !objectPreview.subtypesWithoutPreview.has(object.subtype)) { - return this.createVariable(ObjectVariable, ctx, object, customStringRepr); + if (object.objectId) { + if (object.subtype === 'map' || object.subtype === 'set') { + return this.createVariable(SetOrMapVariable, ctx, object, customStringRepr); + } else if (!objectPreview.subtypesWithoutPreview.has(object.subtype)) { + return this.createVariable(ObjectVariable, ctx, object, customStringRepr); + } } return this.createVariable(Variable, ctx, object); @@ -493,6 +498,8 @@ class VariableContext { } } +const isNumberOrNumeric = (s: string | number) => typeof s === 'number' || /^[0-9]+$/.test(s); + class Variable implements IVariable { public id = getVariableId(); @@ -524,7 +531,19 @@ class Variable implements IVariable { return parent.accessor; } - if (typeof name === 'number' || /^[0-9]+$/.test(name)) { + // Maps and sets: + const grandparent = parent.context.parent; + if (grandparent instanceof Variable) { + if ((this.remoteObject.subtype as string) === 'internal#entry') { + return `[...${grandparent.accessor}.entries()][${+name}]`; + } + + if ((parent.remoteObject.subtype as string) === 'internal#entry') { + return `${parent.accessor}[${this.name === 'key' ? 0 : 1}]`; + } + } + + if (isNumberOrNumeric(name)) { return `${parent.accessor}[${name}]`; } @@ -792,6 +811,43 @@ class ObjectVariable extends Variable implements IMemoryReadable { } } +const entriesVariableName = '[[Entries]]'; + +class SetOrMapVariable extends ObjectVariable { + private readonly size?: number; + public readonly isMap: boolean; + private readonly baseChildren = once(() => super.getChildren({ variablesReference: this.id })); + + constructor(context: VariableContext, remoteObject: Cdp.Runtime.RemoteObject) { + super(context, remoteObject, NoCustomStringRepr); + const cast = remoteObject.preview as MapPreview | SetPreview; + this.isMap = cast.subtype === 'map'; + this.size = Number(cast.properties.find(p => p.name === 'size')?.value) ?? undefined; + } + + public override async toDap(previewContext: PreviewContextType): Promise { + const dap = await super.toDap(previewContext); + if (this.size && this.size > 100) { + dap.indexedVariables = this.size; + } + + return dap; + } + + public override async getChildren(params: Dap.VariablesParams): Promise { + const baseChildren = await this.baseChildren(); + const entryChildren = await baseChildren + .find(c => c.name === entriesVariableName) + ?.getChildren(params); + + return [ + // filter to only show the actualy entries, not the array prototype/length + ...(entryChildren || []).filter(v => v.sortOrder === SortOrder.Default), + ...baseChildren.filter(c => c.name !== entriesVariableName), + ]; + } +} + class ArrayVariable extends ObjectVariable { private length = 0; diff --git a/src/test/evaluate/evaluate-repl.txt b/src/test/evaluate/evaluate-repl.txt index 124ceb02a..75934d6e9 100644 --- a/src/test/evaluate/evaluate-repl.txt +++ b/src/test/evaluate/evaluate-repl.txt @@ -25,8 +25,8 @@ result: 1234567890n > result: Map(1) {size: 1, hello => ƒ ()} + > 0: {"hello" => function() { return 'world' }} size: 1 - > [[Entries]]: Array(1) > [[Prototype]]: Map result: 42 diff --git a/src/test/evaluate/evaluate-supports-bigint-map-keys-1277.txt b/src/test/evaluate/evaluate-supports-bigint-map-keys-1277.txt index 3a762320a..acf5b82ba 100644 --- a/src/test/evaluate/evaluate-supports-bigint-map-keys-1277.txt +++ b/src/test/evaluate/evaluate-supports-bigint-map-keys-1277.txt @@ -1,4 +1,5 @@ > result: Map(2) {size: 2, 1n => one, 2n => two} + > 0: {1n => "one"} + > 1: {2n => "two"} size: 2 - > [[Entries]]: Array(2) > [[Prototype]]: Map