diff --git a/cli/js/tests/console_test.ts b/cli/js/tests/console_test.ts index b4848332f7..c49c941f40 100644 --- a/cli/js/tests/console_test.ts +++ b/cli/js/tests/console_test.ts @@ -181,8 +181,11 @@ unitTest(function consoleTestStringifyCircular(): void { stringify(async function* agf() {}), "[AsyncGeneratorFunction: agf]" ); - assertEquals(stringify(new Uint8Array([1, 2, 3])), "Uint8Array [ 1, 2, 3 ]"); - assertEquals(stringify(Uint8Array.prototype), "TypedArray []"); + assertEquals( + stringify(new Uint8Array([1, 2, 3])), + "Uint8Array(3) [ 1, 2, 3 ]" + ); + assertEquals(stringify(Uint8Array.prototype), "TypedArray {}"); assertEquals( stringify({ a: { b: { c: { d: new Set([1]) } } } }), "{ a: { b: { c: { d: [Set] } } } }" @@ -283,6 +286,300 @@ unitTest(function consoleTestStringifyLargeObject(): void { ); }); +unitTest(function consoleTestStringifyIterable() { + const shortArray = [1, 2, 3, 4, 5]; + assertEquals(stringify(shortArray), "[ 1, 2, 3, 4, 5 ]"); + + const longArray = new Array(200).fill(0); + assertEquals( + stringify(longArray), + `[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ... 100 more items +]` + ); + + const obj = { a: "a", longArray }; + assertEquals( + stringify(obj), + `{ + a: "a", + longArray: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ... 100 more items + ] +}` + ); + + const shortMap = new Map([ + ["a", 0], + ["b", 1], + ]); + assertEquals(stringify(shortMap), `Map { "a" => 0, "b" => 1 }`); + + const longMap = new Map(); + for (const key of Array(200).keys()) { + longMap.set(`${key}`, key); + } + assertEquals( + stringify(longMap), + `Map { + "0" => 0, + "1" => 1, + "2" => 2, + "3" => 3, + "4" => 4, + "5" => 5, + "6" => 6, + "7" => 7, + "8" => 8, + "9" => 9, + "10" => 10, + "11" => 11, + "12" => 12, + "13" => 13, + "14" => 14, + "15" => 15, + "16" => 16, + "17" => 17, + "18" => 18, + "19" => 19, + "20" => 20, + "21" => 21, + "22" => 22, + "23" => 23, + "24" => 24, + "25" => 25, + "26" => 26, + "27" => 27, + "28" => 28, + "29" => 29, + "30" => 30, + "31" => 31, + "32" => 32, + "33" => 33, + "34" => 34, + "35" => 35, + "36" => 36, + "37" => 37, + "38" => 38, + "39" => 39, + "40" => 40, + "41" => 41, + "42" => 42, + "43" => 43, + "44" => 44, + "45" => 45, + "46" => 46, + "47" => 47, + "48" => 48, + "49" => 49, + "50" => 50, + "51" => 51, + "52" => 52, + "53" => 53, + "54" => 54, + "55" => 55, + "56" => 56, + "57" => 57, + "58" => 58, + "59" => 59, + "60" => 60, + "61" => 61, + "62" => 62, + "63" => 63, + "64" => 64, + "65" => 65, + "66" => 66, + "67" => 67, + "68" => 68, + "69" => 69, + "70" => 70, + "71" => 71, + "72" => 72, + "73" => 73, + "74" => 74, + "75" => 75, + "76" => 76, + "77" => 77, + "78" => 78, + "79" => 79, + "80" => 80, + "81" => 81, + "82" => 82, + "83" => 83, + "84" => 84, + "85" => 85, + "86" => 86, + "87" => 87, + "88" => 88, + "89" => 89, + "90" => 90, + "91" => 91, + "92" => 92, + "93" => 93, + "94" => 94, + "95" => 95, + "96" => 96, + "97" => 97, + "98" => 98, + "99" => 99, + ... 100 more items +}` + ); + + const shortSet = new Set([1, 2, 3]); + assertEquals(stringify(shortSet), `Set { 1, 2, 3 }`); + const longSet = new Set(); + for (const key of Array(200).keys()) { + longSet.add(key); + } + assertEquals( + stringify(longSet), + `Set { + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + ... 100 more items +}` + ); + + const withEmptyEl = Array(10); + withEmptyEl.fill(0, 4, 6); + assertEquals( + stringify(withEmptyEl), + `[ <4 empty items>, 0, 0, <4 empty items> ]` + ); + + const lWithEmptyEl = Array(200); + lWithEmptyEl.fill(0, 50, 80); + assertEquals( + stringify(lWithEmptyEl), + `[ + <50 empty items>, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, <120 empty items> +]` + ); +}); + unitTest(function consoleTestWithCustomInspector(): void { class A { [customInspect](): string { diff --git a/cli/js/web/console.ts b/cli/js/web/console.ts index 554c5a1b34..a9b4d53be7 100644 --- a/cli/js/web/console.ts +++ b/cli/js/web/console.ts @@ -13,13 +13,11 @@ type InspectOptions = Partial<{ indentLevel: number; }>; -// Default depth of logging nested objects -const DEFAULT_MAX_DEPTH = 4; - +const DEFAULT_MAX_DEPTH = 4; // Default depth of logging nested objects const LINE_BREAKING_LENGTH = 80; - +const MAX_ITERABLE_LENGTH = 100; +const MIN_GROUP_LENGTH = 6; const STR_ABBREVIATE_SIZE = 100; - // Char codes const CHAR_PERCENT = 37; /* % */ const CHAR_LOWERCASE_S = 115; /* s */ @@ -29,6 +27,7 @@ const CHAR_LOWERCASE_F = 102; /* f */ const CHAR_LOWERCASE_O = 111; /* o */ const CHAR_UPPERCASE_O = 79; /* O */ const CHAR_LOWERCASE_C = 99; /* c */ + export class CSI { static kClear = "\x1b[1;1H"; static kClearScreenDown = "\x1b[0J"; @@ -77,15 +76,19 @@ interface IterablePrintConfig { displayName: string; delims: [string, string]; entryHandler: ( - entry: T, + entry: [unknown, T], ctx: ConsoleContext, level: number, - maxLevel: number + maxLevel: number, + next: () => IteratorResult<[unknown, T], unknown> ) => string; + group: boolean; } - +type IterableEntries = Iterable & { + entries(): IterableIterator<[unknown, T]>; +}; function createIterableString( - value: Iterable, + value: IterableEntries, ctx: ConsoleContext, level: number, maxLevel: number, @@ -97,18 +100,165 @@ function createIterableString( ctx.add(value); const entries: string[] = []; - // In cases e.g. Uint8Array.prototype - try { - for (const el of value) { - entries.push(config.entryHandler(el, ctx, level + 1, maxLevel)); + + const iter = value.entries(); + let entriesLength = 0; + const next = (): IteratorResult<[unknown, T], unknown> => { + return iter.next(); + }; + for (const el of iter) { + if (entriesLength < MAX_ITERABLE_LENGTH) { + entries.push( + config.entryHandler(el, ctx, level + 1, maxLevel, next.bind(iter)) + ); } - } catch (e) {} + entriesLength++; + } ctx.delete(value); + + if (entriesLength > MAX_ITERABLE_LENGTH) { + const nmore = entriesLength - MAX_ITERABLE_LENGTH; + entries.push(`... ${nmore} more items`); + } + const iPrefix = `${config.displayName ? config.displayName + " " : ""}`; - const iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; + + let iContent: string; + if (config.group && entries.length > MIN_GROUP_LENGTH) { + const groups = groupEntries(entries, level, value); + const initIndentation = `\n${" ".repeat(level + 1)}`; + const entryIndetation = `,\n${" ".repeat(level + 1)}`; + const closingIndentation = `\n${" ".repeat(level)}`; + + iContent = `${initIndentation}${groups.join( + entryIndetation + )}${closingIndentation}`; + } else { + iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; + if (iContent.length > LINE_BREAKING_LENGTH) { + const initIndentation = `\n${" ".repeat(level + 1)}`; + const entryIndetation = `,\n${" ".repeat(level + 1)}`; + const closingIndentation = `\n`; + + iContent = `${initIndentation}${entries.join( + entryIndetation + )}${closingIndentation}`; + } + } + return `${iPrefix}${config.delims[0]}${iContent}${config.delims[1]}`; } +// Ported from Node.js +// Copyright Node.js contributors. All rights reserved. +function groupEntries( + entries: string[], + level: number, + value: Iterable +): string[] { + let totalLength = 0; + let maxLength = 0; + let entriesLength = entries.length; + if (MAX_ITERABLE_LENGTH < entriesLength) { + // This makes sure the "... n more items" part is not taken into account. + entriesLength--; + } + const separatorSpace = 2; // Add 1 for the space and 1 for the separator. + const dataLen = new Array(entriesLength); + // Calculate the total length of all output entries and the individual max + // entries length of all output entries. In future colors should be taken + // here into the account + for (let i = 0; i < entriesLength; i++) { + const len = entries[i].length; + dataLen[i] = len; + totalLength += len + separatorSpace; + if (maxLength < len) maxLength = len; + } + // Add two to `maxLength` as we add a single whitespace character plus a comma + // in-between two entries. + const actualMax = maxLength + separatorSpace; + // Check if at least three entries fit next to each other and prevent grouping + // of arrays that contains entries of very different length (i.e., if a single + // entry is longer than 1/5 of all other entries combined). Otherwise the + // space in-between small entries would be enormous. + if ( + actualMax * 3 + (level + 1) < LINE_BREAKING_LENGTH && + (totalLength / actualMax > 5 || maxLength <= 6) + ) { + const approxCharHeights = 2.5; + const averageBias = Math.sqrt(actualMax - totalLength / entries.length); + const biasedMax = Math.max(actualMax - 3 - averageBias, 1); + // Dynamically check how many columns seem possible. + const columns = Math.min( + // Ideally a square should be drawn. We expect a character to be about 2.5 + // times as high as wide. This is the area formula to calculate a square + // which contains n rectangles of size `actualMax * approxCharHeights`. + // Divide that by `actualMax` to receive the correct number of columns. + // The added bias increases the columns for short entries. + Math.round( + Math.sqrt(approxCharHeights * biasedMax * entriesLength) / biasedMax + ), + // Do not exceed the breakLength. + Math.floor((LINE_BREAKING_LENGTH - (level + 1)) / actualMax), + // Limit the columns to a maximum of fifteen. + 15 + ); + // Return with the original output if no grouping should happen. + if (columns <= 1) { + return entries; + } + const tmp = []; + const maxLineLength = []; + for (let i = 0; i < columns; i++) { + let lineMaxLength = 0; + for (let j = i; j < entries.length; j += columns) { + if (dataLen[j] > lineMaxLength) lineMaxLength = dataLen[j]; + } + lineMaxLength += separatorSpace; + maxLineLength[i] = lineMaxLength; + } + let order = "padStart"; + if (value !== undefined) { + for (let i = 0; i < entries.length; i++) { + //@ts-ignore + if (typeof value[i] !== "number" && typeof value[i] !== "bigint") { + order = "padEnd"; + break; + } + } + } + // Each iteration creates a single line of grouped entries. + for (let i = 0; i < entriesLength; i += columns) { + // The last lines may contain less entries than columns. + const max = Math.min(i + columns, entriesLength); + let str = ""; + let j = i; + for (; j < max - 1; j++) { + // In future, colors should be taken here into the account + const padding = maxLineLength[j - i]; + //@ts-ignore + str += `${entries[j]}, `[order](padding, " "); + } + if (order === "padStart") { + const padding = + maxLineLength[j - i] + + entries[j].length - + dataLen[j] - + separatorSpace; + str += entries[j].padStart(padding, " "); + } else { + str += entries[j]; + } + tmp.push(str); + } + if (MAX_ITERABLE_LENGTH < entries.length) { + tmp.push(entries[entriesLength]); + } + entries = tmp; + } + return entries; +} + function stringify( value: unknown, ctx: ConsoleContext, @@ -173,8 +323,23 @@ function createArrayString( typeName: "Array", displayName: "", delims: ["[", "]"], - entryHandler: (el, ctx, level, maxLevel): string => - stringifyWithQuotes(el, ctx, level + 1, maxLevel), + entryHandler: (entry, ctx, level, maxLevel, next): string => { + const [index, val] = entry as [number, unknown]; + let i = index; + if (!value.hasOwnProperty(i)) { + i++; + while (!value.hasOwnProperty(i) && i < value.length) { + next(); + i++; + } + const emptyItems = i - index; + const ending = emptyItems > 1 ? "s" : ""; + return `<${emptyItems} empty item${ending}>`; + } else { + return stringifyWithQuotes(val, ctx, level + 1, maxLevel); + } + }, + group: true, }; return createIterableString(value, ctx, level, maxLevel, printConfig); } @@ -186,12 +351,16 @@ function createTypedArrayString( level: number, maxLevel: number ): string { + const valueLength = value.length; const printConfig: IterablePrintConfig = { typeName: typedArrayName, - displayName: typedArrayName, + displayName: `${typedArrayName}(${valueLength})`, delims: ["[", "]"], - entryHandler: (el, ctx, level, maxLevel): string => - stringifyWithQuotes(el, ctx, level + 1, maxLevel), + entryHandler: (entry, ctx, level, maxLevel): string => { + const [_, val] = entry; + return stringifyWithQuotes(val, ctx, level + 1, maxLevel); + }, + group: true, }; return createIterableString(value, ctx, level, maxLevel, printConfig); } @@ -206,8 +375,11 @@ function createSetString( typeName: "Set", displayName: "Set", delims: ["{", "}"], - entryHandler: (el, ctx, level, maxLevel): string => - stringifyWithQuotes(el, ctx, level + 1, maxLevel), + entryHandler: (entry, ctx, level, maxLevel): string => { + const [_, val] = entry; + return stringifyWithQuotes(val, ctx, level + 1, maxLevel); + }, + group: false, }; return createIterableString(value, ctx, level, maxLevel, printConfig); } @@ -218,12 +390,12 @@ function createMapString( level: number, maxLevel: number ): string { - const printConfig: IterablePrintConfig<[unknown, unknown]> = { + const printConfig: IterablePrintConfig<[unknown]> = { typeName: "Map", displayName: "Map", delims: ["{", "}"], - entryHandler: (el, ctx, level, maxLevel): string => { - const [key, val] = el; + entryHandler: (entry, ctx, level, maxLevel): string => { + const [key, val] = entry; return `${stringifyWithQuotes( key, ctx, @@ -231,7 +403,9 @@ function createMapString( maxLevel )} => ${stringifyWithQuotes(val, ctx, level + 1, maxLevel)}`; }, + group: false, }; + //@ts-ignore return createIterableString(value, ctx, level, maxLevel, printConfig); } diff --git a/cli/js/web/util.ts b/cli/js/web/util.ts index 19a30a6753..2d63b4d606 100644 --- a/cli/js/web/util.ts +++ b/cli/js/web/util.ts @@ -1,9 +1,28 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -export type TypedArray = Uint8Array | Float32Array | Int32Array; -const TypedArrayConstructor = Object.getPrototypeOf(Uint8Array); +export type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array; + export function isTypedArray(x: unknown): x is TypedArray { - return x instanceof TypedArrayConstructor; + return ( + x instanceof Int8Array || + x instanceof Uint8Array || + x instanceof Uint8ClampedArray || + x instanceof Int16Array || + x instanceof Uint16Array || + x instanceof Int32Array || + x instanceof Uint32Array || + x instanceof Float32Array || + x instanceof Float64Array + ); } // @internal diff --git a/cli/tests/seed_random.js.out b/cli/tests/seed_random.js.out index c65e40f971..2b5551c83f 100644 --- a/cli/tests/seed_random.js.out +++ b/cli/tests/seed_random.js.out @@ -8,5 +8,16 @@ 0.3824611207183364 0.5950178237266042 0.22440633214343908 -Uint8Array [ 116, 125, 169, 69, 106, 231, 99, 39, 148, 188, 211, 41, 46, 211, 236, 141, 55, 10, 214, 63, 118, 230, 218, 249, 125, 161, 137, 110, 214, 36, 159, 154 ] -Uint8Array [ 248, 21, 21, 9, 41, 0, 71, 124, 244, 209, 252, 151, 7, 10, 168, 250, 84, 170, 243, 140, 53, 47, 99, 212, 18, 146, 68, 48, 66, 222, 67, 112 ] +Uint8Array(32) [ + 116, 125, 169, 69, 106, 231, 99, + 39, 148, 188, 211, 41, 46, 211, + 236, 141, 55, 10, 214, 63, 118, + 230, 218, 249, 125, 161, 137, 110, + 214, 36, 159, 154 +] +Uint8Array(32) [ + 248, 21, 21, 9, 41, 0, 71, 124, + 244, 209, 252, 151, 7, 10, 168, 250, + 84, 170, 243, 140, 53, 47, 99, 212, + 18, 146, 68, 48, 66, 222, 67, 112 +]