From 1958152d146389b00c50a149c33012a16864ef83 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Fri, 8 Mar 2024 08:25:42 +0100 Subject: [PATCH] Pretty-print values in the REPL Pretty-print values in the REPL by printing each item in a list or attrset on a separate line. When possible, single-item lists and attrsets are printed on one line, as long as they don't contain a nested list, attrset, or thunk. Before: ``` { attrs = { a = { b = { c = { }; }; }; }; list = [ 1 ]; list' = [ 1 2 3 ]; } ``` After: ``` { attrs = { a = { b = { c = { }; }; }; }; list = [ 1 ]; list' = [ 1 2 3 ]; } ``` (cherry picked from commit c0a15fb7d03dfb8f53bc6726c414bc88aa362592) Change-Id: Ia2b41849165a5ddb63f7a8c272a2476b3e4292df --- src/libcmd/repl.cc | 3 +- src/libexpr/print-options.hh | 22 ++++ src/libexpr/print.cc | 112 ++++++++++++++++-- ...al-fail-bad-string-interpolation-4.err.exp | 2 +- tests/functional/repl.sh | 69 ++++++++++- tests/unit/libexpr/value/print.cc | 8 +- 6 files changed, 194 insertions(+), 22 deletions(-) diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 8ff8d04c4..bc5ff9525 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -98,7 +98,8 @@ struct NixRepl .ansiColors = true, .force = true, .derivationPaths = true, - .maxDepth = maxDepth + .maxDepth = maxDepth, + .prettyIndent = 2 }); } }; diff --git a/src/libexpr/print-options.hh b/src/libexpr/print-options.hh index e03746ece..94767df9c 100644 --- a/src/libexpr/print-options.hh +++ b/src/libexpr/print-options.hh @@ -17,24 +17,29 @@ struct PrintOptions * If true, output ANSI color sequences. */ bool ansiColors = false; + /** * If true, force values. */ bool force = false; + /** * If true and `force` is set, print derivations as * `«derivation /nix/store/...»` instead of as attribute sets. */ bool derivationPaths = false; + /** * If true, track which values have been printed and skip them on * subsequent encounters. Useful for self-referential values. */ bool trackRepeated = true; + /** * Maximum depth to evaluate to. */ size_t maxDepth = std::numeric_limits::max(); + /** * Maximum number of attributes in attribute sets to print. * @@ -42,6 +47,7 @@ struct PrintOptions * attribute set encountered. */ size_t maxAttrs = std::numeric_limits::max(); + /** * Maximum number of list items to print. * @@ -49,10 +55,26 @@ struct PrintOptions * list encountered. */ size_t maxListItems = std::numeric_limits::max(); + /** * Maximum string length to print. */ size_t maxStringLength = std::numeric_limits::max(); + + /** + * Indentation width for pretty-printing. + * + * If set to 0 (the default), values are not pretty-printed. + */ + size_t prettyIndent = 0; + + /** + * True if pretty-printing is enabled. + */ + inline bool prettyPrint() + { + return prettyIndent > 0; + } }; /** diff --git a/src/libexpr/print.cc b/src/libexpr/print.cc index b792f657d..53d0861bf 100644 --- a/src/libexpr/print.cc +++ b/src/libexpr/print.cc @@ -151,6 +151,7 @@ struct ImportantFirstAttrNameCmp }; typedef std::set ValuesSeen; +typedef std::vector> AttrVec; class Printer { @@ -161,6 +162,21 @@ private: std::optional seen; size_t attrsPrinted = 0; size_t listItemsPrinted = 0; + std::string indent; + + void increaseIndent() + { + if (options.prettyPrint()) { + indent.append(options.prettyIndent, ' '); + } + } + + void decreaseIndent() + { + if (options.prettyPrint()) { + indent.resize(indent.size() - options.prettyIndent); + } + } void printRepeated() { @@ -258,6 +274,28 @@ private: } } + bool shouldPrettyPrintAttrs(AttrVec & v) + { + if (!options.prettyPrint() || v.empty()) { + return false; + } + + // Pretty-print attrsets with more than one item. + if (v.size() > 1) { + return true; + } + + auto item = v[0].second; + if (!item) { + return true; + } + + // Pretty-print single-item attrsets only if they contain nested + // structures. + auto itemType = item->type(); + return itemType == nList || itemType == nAttrs || itemType == nThunk; + } + void printAttrs(Value & v, size_t depth) { if (seen && !seen->insert(v.attrs).second) { @@ -268,9 +306,10 @@ private: if (options.force && options.derivationPaths && state.isDerivation(v)) { printDerivation(v); } else if (depth < options.maxDepth) { - output << "{ "; + increaseIndent(); + output << "{"; - std::vector> sorted; + AttrVec sorted; for (auto & i : *v.attrs) sorted.emplace_back(std::pair(state.symbols[i.name], i.value)); @@ -279,7 +318,15 @@ private: else std::sort(sorted.begin(), sorted.end(), ImportantFirstAttrNameCmp()); + auto prettyPrint = shouldPrettyPrintAttrs(sorted); + for (auto & i : sorted) { + if (prettyPrint) { + output << "\n" << indent; + } else { + output << " "; + } + if (attrsPrinted >= options.maxAttrs) { printElided(sorted.size() - attrsPrinted, "attribute", "attributes"); break; @@ -288,13 +335,42 @@ private: printAttributeName(output, i.first); output << " = "; print(*i.second, depth + 1); - output << "; "; + output << ";"; attrsPrinted++; } + decreaseIndent(); + if (prettyPrint) { + output << "\n" << indent; + } else { + output << " "; + } output << "}"; - } else + } else { output << "{ ... }"; + } + } + + bool shouldPrettyPrintList(std::span list) + { + if (!options.prettyPrint() || list.empty()) { + return false; + } + + // Pretty-print lists with more than one item. + if (list.size() > 1) { + return true; + } + + auto item = list[0]; + if (!item) { + return true; + } + + // Pretty-print single-item lists only if they contain nested + // structures. + auto itemType = item->type(); + return itemType == nList || itemType == nAttrs || itemType == nThunk; } void printList(Value & v, size_t depth) @@ -304,9 +380,18 @@ private: return; } - output << "[ "; if (depth < options.maxDepth) { - for (auto elem : v.listItems()) { + increaseIndent(); + output << "["; + auto listItems = v.listItems(); + auto prettyPrint = shouldPrettyPrintList(listItems); + for (auto elem : listItems) { + if (prettyPrint) { + output << "\n" << indent; + } else { + output << " "; + } + if (listItemsPrinted >= options.maxListItems) { printElided(v.listSize() - listItemsPrinted, "item", "items"); break; @@ -317,13 +402,19 @@ private: } else { printNullptr(); } - output << " "; listItemsPrinted++; } + + decreaseIndent(); + if (prettyPrint) { + output << "\n" << indent; + } else { + output << " "; + } + output << "]"; + } else { + output << "[ ... ]"; } - else - output << "... "; - output << "]"; } void printFunction(Value & v) @@ -486,6 +577,7 @@ public: { attrsPrinted = 0; listItemsPrinted = 0; + indent.clear(); if (options.trackRepeated) { seen.emplace(); diff --git a/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp b/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp index 5119238d7..6f907106b 100644 --- a/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp +++ b/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp @@ -6,4 +6,4 @@ error: | ^ 10| - error: cannot coerce a set to a string: { a = { a = { a = { a = "ha"; b = "ha"; c = "ha"; d = "ha"; e = "ha"; f = "ha"; g = "ha"; h = "ha"; j = "ha"; }; «4294967295 attributes elided»}; «4294967294 attributes elided»}; «4294967293 attributes elided»} + error: cannot coerce a set to a string: { a = { a = { a = { a = "ha"; b = "ha"; c = "ha"; d = "ha"; e = "ha"; f = "ha"; g = "ha"; h = "ha"; j = "ha"; }; «4294967295 attributes elided» }; «4294967294 attributes elided» }; «4294967293 attributes elided» } diff --git a/tests/functional/repl.sh b/tests/functional/repl.sh index 0921edc0f..3765fb492 100644 --- a/tests/functional/repl.sh +++ b/tests/functional/repl.sh @@ -151,29 +151,86 @@ echo "$replResult" | grepQuiet -s afterChange # Normal output should print attributes in lexicographical order non-recursively testReplResponseNoRegex ' { a = { b = 2; }; l = [ 1 2 3 ]; s = "string"; n = 1234; x = rec { y = { z = { inherit y; }; }; }; } -' '{ a = { ... }; l = [ ... ]; n = 1234; s = "string"; x = { ... }; }' +' \ +'{ + a = { ... }; + l = [ ... ]; + n = 1234; + s = "string"; + x = { ... }; +} +' # Same for lists, but order is preserved testReplResponseNoRegex ' [ 42 1 "thingy" ({ a = 1; }) ([ 1 2 3 ]) ] -' '[ 42 1 "thingy" { ... } [ ... ] ]' +' \ +'[ + 42 + 1 + "thingy" + { ... } + [ ... ] +] +' # Same for let expressions testReplResponseNoRegex ' let x = { y = { a = 1; }; inherit x; }; in x -' '{ x = «repeated»; y = { ... }; }' +' \ +'{ + x = { ... }; + y = { ... }; +} +' # The :p command should recursively print sets, but prevent infinite recursion testReplResponseNoRegex ' :p { a = { b = 2; }; s = "string"; n = 1234; x = rec { y = { z = { inherit y; }; }; }; } -' '{ a = { b = 2; }; n = 1234; s = "string"; x = { y = { z = { y = «repeated»; }; }; }; }' +' \ +'{ + a = { b = 2; }; + n = 1234; + s = "string"; + x = { + y = { + z = { + y = «repeated»; + }; + }; + }; +} +' # Same for lists testReplResponseNoRegex ' :p [ 42 1 "thingy" (rec { a = 1; b = { inherit a; inherit b; }; }) ([ 1 2 3 ]) ] -' '[ 42 1 "thingy" { a = 1; b = { a = 1; b = «repeated»; }; } [ 1 2 3 ] ]' +' \ +'[ + 42 + 1 + "thingy" + { + a = 1; + b = { + a = 1; + b = «repeated»; + }; + } + [ + 1 + 2 + 3 + ] +] +' # Same for let expressions testReplResponseNoRegex ' :p let x = { y = { a = 1; }; inherit x; }; in x -' '{ x = «repeated»; y = { a = 1; }; }' +' \ +'{ + x = «repeated»; + y = { a = 1 }; +} +' diff --git a/tests/unit/libexpr/value/print.cc b/tests/unit/libexpr/value/print.cc index c1de3a6a9..aabf156c2 100644 --- a/tests/unit/libexpr/value/print.cc +++ b/tests/unit/libexpr/value/print.cc @@ -720,7 +720,7 @@ TEST_F(ValuePrintingTests, ansiColorsAttrsElided) vAttrs.mkAttrs(builder.finish()); test(vAttrs, - "{ one = " ANSI_CYAN "1" ANSI_NORMAL "; " ANSI_FAINT "«1 attribute elided»" ANSI_NORMAL "}", + "{ one = " ANSI_CYAN "1" ANSI_NORMAL "; " ANSI_FAINT "«1 attribute elided»" ANSI_NORMAL " }", PrintOptions { .ansiColors = true, .maxAttrs = 1 @@ -733,7 +733,7 @@ TEST_F(ValuePrintingTests, ansiColorsAttrsElided) vAttrs.mkAttrs(builder.finish()); test(vAttrs, - "{ one = " ANSI_CYAN "1" ANSI_NORMAL "; " ANSI_FAINT "«2 attributes elided»" ANSI_NORMAL "}", + "{ one = " ANSI_CYAN "1" ANSI_NORMAL "; " ANSI_FAINT "«2 attributes elided»" ANSI_NORMAL " }", PrintOptions { .ansiColors = true, .maxAttrs = 1 @@ -757,7 +757,7 @@ TEST_F(ValuePrintingTests, ansiColorsListElided) vList.bigList.size = 2; test(vList, - "[ " ANSI_CYAN "1" ANSI_NORMAL " " ANSI_FAINT "«1 item elided»" ANSI_NORMAL "]", + "[ " ANSI_CYAN "1" ANSI_NORMAL " " ANSI_FAINT "«1 item elided»" ANSI_NORMAL " ]", PrintOptions { .ansiColors = true, .maxListItems = 1 @@ -770,7 +770,7 @@ TEST_F(ValuePrintingTests, ansiColorsListElided) vList.bigList.size = 3; test(vList, - "[ " ANSI_CYAN "1" ANSI_NORMAL " " ANSI_FAINT "«2 items elided»" ANSI_NORMAL "]", + "[ " ANSI_CYAN "1" ANSI_NORMAL " " ANSI_FAINT "«2 items elided»" ANSI_NORMAL " ]", PrintOptions { .ansiColors = true, .maxListItems = 1