blob: 8803bc6ee657186f4e2ef5b9ac8b1fdc4f64c21c [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/zxdb/console/format_node_console.h"
#include "src/developer/debug/zxdb/common/ref_ptr_to.h"
#include "src/developer/debug/zxdb/common/string_util.h"
#include "src/developer/debug/zxdb/console/command_utils.h"
#include "src/developer/debug/zxdb/console/format_name.h"
#include "src/developer/debug/zxdb/console/string_util.h"
#include "src/developer/debug/zxdb/expr/format.h"
#include "src/developer/debug/zxdb/expr/format_node.h"
#include "src/lib/fxl/memory/ref_counted.h"
namespace zxdb {
namespace {
// Provides a way to execute a single callback when a set of deferred actions is complete.
class AggregateDeferredCallback : public fxl::RefCountedThreadSafe<AggregateDeferredCallback> {
public:
// Returns a subordinate callback, the completion of which will contribute to the outer callback.
fit::deferred_callback MakeSubordinate() {
// This callback exists only to hold the bound reference to the outer callback.
return fit::defer_callback([ref = RefPtrTo(this)]() {});
}
private:
FRIEND_REF_COUNTED_THREAD_SAFE(AggregateDeferredCallback);
FRIEND_MAKE_REF_COUNTED(AggregateDeferredCallback);
explicit AggregateDeferredCallback(fit::deferred_callback cb) : outer_(std::move(cb)) {}
fit::deferred_callback outer_;
};
// Some state needs to be tracked recursively as we iterate through the tree.
struct RecursiveState {
explicit RecursiveState(const ConsoleFormatOptions& opts) : options(opts) {}
// Returns a modified version of the state for one additional level of recursion, advancing
// both indent and tree depth.
RecursiveState Advance() const;
// Advances the tree depth but not the visual nesting depth.
RecursiveState AdvanceNoIndent() const;
// Returns true if the combination of options means the type should be shown.
bool TypeForcedOn() const {
return !inhibit_one_type && options.verbosity == ConsoleFormatOptions::Verbosity::kAllTypes;
}
// Returns true if the current item should be formatted in expanded tree mode or in one line.
bool ShouldExpand() const {
return options.wrapping == ConsoleFormatOptions::Wrapping::kExpanded ||
(options.wrapping == ConsoleFormatOptions::Wrapping::kSmart && smart_indent_is_expanded);
}
int GetIndentAmount() const {
if (!ShouldExpand())
return 0;
return options.indent_amount * indent_depth;
}
// Returns the string to indent a node at the given level.
std::string GetIndentString() const { return std::string(GetIndentAmount(), ' '); }
// Returns true if items at this level should be elided because the printing depth is too deep.
bool DepthTooDeep() const { return tree_depth >= options.max_depth; }
const ConsoleFormatOptions options;
// Forces various information off for exactly one level of printing. Use for array printing, for
// example, to avoid duplicating the type information for every entry.
bool inhibit_one_name = false;
bool inhibit_one_type = false;
// How deep in the node tree we are, the top level is 0.
int tree_depth = 0;
// How many levels of visual indent we are, the top level is 0. This is often the same as the
// tree depth but some things like pointer resolution are different levels of the tree but
// are presented as the same visual level.
int indent_depth = 0;
// Number of pointers we've expanded so far in this level of recursion.
int pointer_depth = 0;
// Controls what mode of smart indenting (single-line or expanded) the current node should be
// formatted in.
bool smart_indent_is_expanded = true;
};
RecursiveState RecursiveState::Advance() const {
RecursiveState result = *this;
result.inhibit_one_name = false;
result.inhibit_one_type = false;
result.tree_depth++;
result.indent_depth++;
return result;
}
RecursiveState RecursiveState::AdvanceNoIndent() const {
RecursiveState result = Advance();
result.indent_depth--; // Undo visual indent from Advance().
return result;
}
void RecursiveDescribeFormatNode(FormatNode* node, fxl::RefPtr<EvalContext> context,
const RecursiveState& state, fit::deferred_callback cb) {
if (state.tree_depth == state.options.max_depth)
return; // Reached max depth, give up.
auto on_complete = fit::defer_callback([weak_node = node->GetWeakPtr(), context,
cb = std::move(cb), state = state.Advance()]() mutable {
// Description is complete.
if (!weak_node || weak_node->children().empty())
return;
// Check for pointer expansion to avoid recursing into too many.
if (weak_node->description_kind() == FormatNode::kPointer) {
if (state.pointer_depth == state.options.pointer_expand_depth)
return; // Don't recurse into another level of pointer.
state.pointer_depth++;
}
// Recurse into children.
auto aggregator = fxl::MakeRefCounted<AggregateDeferredCallback>(std::move(cb));
for (const auto& child : weak_node->children())
RecursiveDescribeFormatNode(child.get(), context, state, aggregator->MakeSubordinate());
});
if (node->state() != FormatNode::kDescribed)
FillFormatNodeDescription(node, state.options, context, std::move(on_complete));
}
bool IsRust(const FormatNode* node) {
if (!node->value().type())
return false;
return node->value().type()->GetLanguage() == DwarfLang::kRust;
}
// Forward decls, see the implementation for comments.
OutputBuffer DoFormatNode(const FormatNode* node, const RecursiveState& state, int line_start_char);
OutputBuffer DoFormatNodeOneWay(const FormatNode* node, const RecursiveState& state,
int indent_amount);
// Get a possibly-elided version of the type name for a medium verbosity level.
std::string GetElidedTypeName(const RecursiveState& state, const std::string& name) {
if (state.options.verbosity != ConsoleFormatOptions::Verbosity::kMinimal)
return name; // No eliding except in minimal mode.
// Pick different thresholds depending on if we're doing multiline or not. The threshold and
// the eliding length are different so we don't elide one or two characters.
size_t always_allow_below = 64;
size_t elide_to = 50;
if (!state.ShouldExpand()) {
always_allow_below /= 2;
elide_to /= 2;
}
// Names shorter than this are always displayed in full.
if (name.size() <= always_allow_below)
return name;
return name.substr(0, elide_to) + "…";
}
// Rust collections (structs) normally have the type name before the bracket like "MyStruct{...}".
// This function appends this name.
//
// For some struct types including common standard containers the names can get very long. As a
// result in "minimal" verbosity mode we never show the namespaces and also elide template
// parameters when the template is long.
void AppendRustCollectionName(const FormatNode* node, const RecursiveState& state,
OutputBuffer* out) {
if (!IsRust(node) || !node->value().type())
return;
if (state.options.verbosity != ConsoleFormatOptions::Verbosity::kMinimal) {
// Use the full identifier name in the more verbose modes.
FormatIdentifierOptions opts;
opts.show_global_qual = false;
opts.elide_templates = false;
opts.bold_last = false;
out->Append(FormatIdentifier(node->value().type()->GetIdentifier(), opts));
return;
}
// Special-case "Vec" with the macro that's normally used and omit the type.
if (StringStartsWith(node->type(), "alloc::vec::Vec<")) {
out->Append("vec!");
return;
}
// In minimal mode, extract just the last component of the identifier.
const Identifier& ident = node->value().type()->GetIdentifier();
if (ident.empty())
return;
ParsedIdentifier parsed =
ToParsedIdentifier(Identifier(IdentifierQualification::kRelative, ident.components().back()));
if (parsed.components()[0].has_template()) {
// Some containers have very long template names. Since Rust structs always have the name, the
// lengths can get a little bit nuts. Elide the template contents when too long by converting
// all template parameters to one string and eliding.
const auto& template_contents = parsed.components()[0].template_contents();
std::string template_string;
for (size_t i = 0; i < template_contents.size(); i++) {
if (i > 0)
template_string += ", ";
template_string += template_contents[i];
}
// Replace the template parameters with our single string, elided if necessary.
parsed.components()[0] = ParsedIdentifierComponent(parsed.components()[0].name(),
{GetElidedTypeName(state, template_string)});
}
FormatIdentifierOptions ident_opts;
ident_opts.show_global_qual = false;
ident_opts.elide_templates = false;
ident_opts.bold_last = false;
out->Append(FormatIdentifier(parsed, ident_opts));
}
// Writes the suffix for when there are multiple items in a collection or an array.
void AppendItemSuffix(const RecursiveState& state, bool last_item, OutputBuffer* out) {
if (state.ShouldExpand())
out->Append("\n"); // Expanded children always end with a newline.
else if (!last_item)
out->Append(Syntax::kOperatorNormal, ", "); // Get commas for everything but the last.
}
// Returns true if type information should be displayed before the variable name, as in
// <TYPE> name = value
bool ShouldPrependTypeNameBeforeName(const FormatNode* node, const RecursiveState& state) {
if (!state.TypeForcedOn())
return false; // Never show types unless requested.
if (node->type().empty())
return false; // Don't show empty types.
if (!IsRust(node))
return true; // Non-Rust code always gets the type.
if (node->description_kind() == FormatNode::kCollection ||
node->description_kind() == FormatNode::kArray ||
node->description_kind() == FormatNode::kRustTuple ||
node->description_kind() == FormatNode::kRustTupleStruct) {
// Rust structs and tuple structs are special. They encode the type after the variable, so:
// "foo = StructName{...}" and we don't want to prepend it before "foo". But this doesn't happen
// for tuples.
//
// Rust tuples don't have the name shown ever. The individual members will have their types
// displayed which will encode the type of the tuple.
return false;
}
return true;
}
// Appends the formatted name and "=" as needed by this node.
void AppendNodeNameAndType(const FormatNode* node, const RecursiveState& state, OutputBuffer* out) {
if (ShouldPrependTypeNameBeforeName(node, state)) {
out->Append(Syntax::kOperatorDim, "(");
out->Append(Syntax::kComment, node->type());
out->Append(Syntax::kOperatorDim, ") ");
}
if (!state.inhibit_one_name && !node->name().empty()) {
// Node name. Base class names are less important so get dimmed. Especially STL base classes
// can get very long so we also elide them unless verbose mode is turned on.
if (node->child_kind() == FormatNode::kBaseClass) {
out->Append(Syntax::kComment, GetElidedTypeName(state, node->name()));
} else {
// Normal variable-style node name.
out->Append(Syntax::kVariable, node->name());
}
// Rust uses colons for most things, while C uses equals. The exception for Rust is local
// variables which "=" to better match how the variables are declared.
if (IsRust(node) && node->child_kind() != FormatNode::kVariable)
out->Append(Syntax::kOperatorNormal, ": ");
else
out->Append(Syntax::kOperatorNormal, " = ");
}
}
// Writes the node's children. The caller sets up how it wants the children to be formatted by
// passing both the recursive state for the node, and the state it wants to use for the children.
//
// The opening and closing strings are used to wrap the contents, e.g. "{" and "}". If these are
// empty it's assumed that the first line doesn't need a newline after it in multiline mode
// (normally the opening character would be followed by a newline and an indent in multiline mode).
void AppendNodeChildren(const FormatNode* node, const RecursiveState& node_state,
const std::string& opening, const std::string& closing,
const RecursiveState& child_state, OutputBuffer* out) {
out->Append(Syntax::kOperatorNormal, opening);
if (child_state.DepthTooDeep()) {
// Don't print the child names if those children will be elided. Otherwise this prints a
// whole struct out with no data: "{foo=…, bar=…, baz=…}" which is wasteful of space and not
// helpful.
out->Append(Syntax::kComment, "…");
out->Append(Syntax::kOperatorNormal, closing);
return;
}
// Special-case empty ones because we never want wrapping.
if (node->children().empty()) {
out->Append(Syntax::kOperatorNormal, closing);
return;
}
if (!opening.empty() && node_state.ShouldExpand())
out->Append("\n");
int child_indent = child_state.GetIndentAmount();
for (size_t i = 0; i < node->children().size(); i++) {
const FormatNode* child = node->children()[i].get();
if (child->state() == FormatNode::kEmpty) {
// Arrays can have "empty" nodes which use the name to indicate clipping ("...").
if (!child->name().empty())
out->Append(Syntax::kComment, child->name());
} else {
out->Append(DoFormatNode(child, child_state, child_indent));
}
// Separator (comma or newline).
AppendItemSuffix(node_state, i + 1 == node->children().size(), out);
}
out->Append(Syntax::kOperatorNormal, node_state.GetIndentString() + closing);
}
OutputBuffer DoFormatArrayOrTupleNode(const FormatNode* node, const RecursiveState& state) {
OutputBuffer out;
RecursiveState child_state = state.Advance();
// In multiline mode, show the names of the array indices ("[0] = ...").
child_state.inhibit_one_name = !state.ShouldExpand();
if (node->description_kind() == FormatNode::kArray) {
// Arrays all have the same type so don't show the type for every child.
child_state.inhibit_one_type = true;
if (IsRust(node)) {
// Rust sequences use "[...]".
AppendRustCollectionName(node, state, &out);
AppendNodeChildren(node, state, "[", "]", child_state, &out);
} else {
AppendNodeChildren(node, state, "{", "}", child_state, &out);
}
} else {
// Rust tuple or tuple struct. These should not be empty.
if (node->description_kind() == FormatNode::kRustTupleStruct) {
// Tuple structs get the name, regular tuples have none.
AppendRustCollectionName(node, state, &out);
}
// Tuples of length 1 never show the child names, even in expanded mode because it's not
// helpful and looks weird.o
//
// We show the indices of tuple members in expanded mode for larger tuples even though
// Rust wouldn't do that because highly nested data structures can get very difficult to
// follow. Something like a closure or a generator can go on for >100 lines and have many
// tuple members (>10). Without these indices the structure is not interpretable. This doesn't
// come up when using println in the language very often because normally these types of
// tuples are not printed.
if (node->children().size() == 1)
child_state.inhibit_one_name = true;
AppendNodeChildren(node, state, "(", ")", child_state, &out);
}
return out;
}
OutputBuffer DoFormatCollectionNode(const FormatNode* node, const RecursiveState& state) {
OutputBuffer out;
// Rust structs are always prefixed with the struct name. This does nothing for non-Rust.
AppendRustCollectionName(node, state, &out);
// Special-case empty collections because we never want wrapping.
if (node->children().empty()) {
// Rust empty structs have no brackets.
if (!IsRust(node))
out.Append(Syntax::kOperatorNormal, "{}");
return out;
}
AppendNodeChildren(node, state, "{", "}", state.Advance(), &out);
return out;
}
OutputBuffer DoFormatPointerNode(const FormatNode* node, const RecursiveState& state) {
OutputBuffer out;
// When type information is forced on, the type will have already been printed. Otherwise we print
// a "(*)" to indicate the value is a pointer.
if (!state.TypeForcedOn())
out.Append(Syntax::kOperatorDim, "(*)");
// Pointers will have a child node that expands to the pointed-to value. If this is in a
// "described" state with no error, then show the data.
//
// Don't show errors dereferencing pointers here since for long structure listings with
// potentially multiple bad pointers, these get very verbose. The user can print the specific
// value and see the error if desired.
if (node->children().empty() || node->children()[0]->state() != FormatNode::kDescribed ||
node->children()[0]->err().has_error()) {
// Just have the pointer address.
out.Append(node->description());
} else {
// Have the pointed-to data, dim the address and show the data. Omit types because we already
// printed the pointer type. We do not want to indent another level.
RecursiveState child_state = state.AdvanceNoIndent();
child_state.inhibit_one_name = true;
child_state.inhibit_one_type = true;
out.Append(Syntax::kComment, node->description() + " " + GetRightArrow() + " ");
// Use the "one way" version to propagate our formatting mode. This node cant't get different
// formatting than its child.
out.Append(DoFormatNodeOneWay(node->children()[0].get(), child_state, 0));
}
return out;
}
OutputBuffer DoFormatReferenceNode(const FormatNode* node, const RecursiveState& state) {
// References will have a child node that expands to the referenced value. If this is in a
// "described" state, then we have the data and should show it. Otherwise omit this.
if (node->children().empty() || node->children()[0]->state() != FormatNode::kDescribed) {
// The caller should have expanded the references if it wants them shown. If not, display
// a placeholder with the address. Show the whole thing dimmed to help indicate this isn't the
// actual value.
return OutputBuffer(Syntax::kComment, "(&)" + node->description());
}
// Have the pointed-to data, just show the value. We do not want to indent another level.
RecursiveState child_state = state.AdvanceNoIndent();
child_state.inhibit_one_name = true;
child_state.inhibit_one_type = true;
// Use the "one way" version to propagate our formatting mode. This node cant't get different
// formatting than its child.
return DoFormatNodeOneWay(node->children()[0].get(), child_state, 0);
}
// A Rust enum is currently like a reference in that is has a child that's the resolved type.
// A resolved enum type will be a TupleStruct (unnamed members) or a Struct (named members). Enums
// with no members are encoded as empty structs which the struct printing code will handle.
OutputBuffer DoFormatRustEnum(const FormatNode* node, const RecursiveState& state) {
if (node->children().empty()) {
// The caller should have expanded the enum if it wants it to be shown. If not, display the
// description which will generally be the short name of the enum.
return OutputBuffer(Syntax::kComment, node->description());
}
// Have the pointed-to data, just show the value. We do not want to indent another level. The
// type is also hidden because it will be a generated type. If requested, the type of the overall
// enum will have already been printed.
RecursiveState child_state = state.AdvanceNoIndent();
child_state.inhibit_one_name = true;
child_state.inhibit_one_type = true;
// Use the "one way" version to propagate our formatting mode. This node can't get different
// formatting than its child.
return DoFormatNodeOneWay(node->children()[0].get(), child_state, 0);
}
// A generic node with one child for things like std::optional or std::atomic.
OutputBuffer DoFormatWrapper(const FormatNode* node, const RecursiveState& state) {
if (node->children().empty())
return OutputBuffer(node->description() + node->wrapper_prefix() + node->wrapper_suffix());
RecursiveState child_state = state.AdvanceNoIndent();
OutputBuffer out;
out.Append(node->description());
out.Append(Syntax::kOperatorNormal, node->wrapper_prefix());
out.Append(DoFormatNodeOneWay(node->children()[0].get(), child_state, 0));
out.Append(Syntax::kOperatorNormal, node->wrapper_suffix());
return out;
}
// Appends the description for a normal node (number or whatever).
OutputBuffer DoFormatStandardNode(const FormatNode* node, const RecursiveState& state,
Syntax syntax = Syntax::kNormal) {
return OutputBuffer(syntax, node->description());
}
// Groups have no heading, they're always just a list of children.
OutputBuffer DoFormatGroupNode(const FormatNode* node, const RecursiveState& state) {
OutputBuffer out;
AppendNodeChildren(node, state, std::string(), std::string(), state, &out);
return out;
}
// Inner implementation for DoFormatNode() (see that for more). This does the actual formatting one
// time according to the current formatting options and state.
//
// Some callers may want to call this directly rather than DoFormatNode() if they want to bypass the
// two-pass formatting. This is useful for pointers which are two nodes (when auto dereferenced) but
// appear as one and the expansion mode of the outer should be passed transparently to the nested
// node.
OutputBuffer DoFormatNodeOneWay(const FormatNode* node, const RecursiveState& state,
int indent_amount) {
OutputBuffer out;
if (indent_amount)
out.Append(std::string(indent_amount, ' '));
AppendNodeNameAndType(node, state, &out);
if (state.DepthTooDeep() || node->state() != FormatNode::kDescribed) {
// Hit max depth, give up.
out.Append(Syntax::kComment, "…");
} else if (node->err().has_error()) {
// Write the error.
out.Append(Syntax::kComment, "<" + node->err().msg() + ">");
} else {
// Normal formatting.
switch (node->description_kind()) {
case FormatNode::kNone:
case FormatNode::kBaseType:
case FormatNode::kFunctionPointer:
case FormatNode::kOther:
// All these things just print the description only.
out.Append(DoFormatStandardNode(node, state));
break;
case FormatNode::kGroup:
out.Append(DoFormatGroupNode(node, state));
break;
case FormatNode::kArray:
case FormatNode::kRustTuple:
case FormatNode::kRustTupleStruct:
out.Append(DoFormatArrayOrTupleNode(node, state));
break;
case FormatNode::kCollection:
out.Append(DoFormatCollectionNode(node, state));
break;
case FormatNode::kPointer:
out.Append(DoFormatPointerNode(node, state));
break;
case FormatNode::kReference:
out.Append(DoFormatReferenceNode(node, state));
break;
case FormatNode::kRustEnum:
out.Append(DoFormatRustEnum(node, state));
break;
case FormatNode::kWrapper:
out.Append(DoFormatWrapper(node, state));
break;
case FormatNode::kString:
// For strings, use the number format to determine if the output should be displayed
// as the string data or a numeric array.
if (state.options.num_format == FormatOptions::NumFormat::kDefault) {
out.Append(DoFormatStandardNode(node, state, Syntax::kStringNormal));
} else {
out.Append(DoFormatArrayOrTupleNode(node, state));
}
break;
}
}
return out;
}
// Formats one node. This layer provides the smart wrapping logic where we may try to format an
// item twice, first to see if it fits on one line, and possibly again ig it doesn't.
//
// The caller of this function is in charge of computing (but not inserting) the appropriate level
// of indent and also appending trailing newlines if necessary.
//
// The caller must compute the indenting amount because whether or not there's indenting for this
// node depends the expansion level of the caller, not this node. (This item could be in single-line
// mode and still be indented if the caller is expanded.)
OutputBuffer DoFormatNode(const FormatNode* node, const RecursiveState& state, int indent_amount) {
if (state.options.wrapping != ConsoleFormatOptions::Wrapping::kSmart)
return DoFormatNodeOneWay(node, state, indent_amount); // Nothing fancy to do.
// Nodes with few children we try to fit on one line if possible in smart mode. Generally
// structures with more than 4 children will always spill over to multiline mode, so this check is
// more of an optimization to avoid trying and failing to format the whole thing as a single line
// first. Even if it fits, many members on a line are not readable.
if (state.smart_indent_is_expanded && node->children().size() <= 4) {
// The previous node was in an expanded state. First try to format this one as non-expanded.
// to see if it fits.
RecursiveState one_line_state = state;
one_line_state.smart_indent_is_expanded = false;
OutputBuffer one_line = DoFormatNodeOneWay(node, one_line_state, indent_amount);
if (static_cast<int>(one_line.UnicodeCharWidth()) <= state.options.smart_indent_cols)
return one_line; // Fits.
// Too big, format as expanded.
return DoFormatNodeOneWay(node, state, indent_amount);
}
// The previous node was already in one-line mode, stay in that mode.
return DoFormatNodeOneWay(node, state, indent_amount);
}
} // namespace
void DescribeFormatNodeForConsole(FormatNode* node, const ConsoleFormatOptions& options,
fxl::RefPtr<EvalContext> context, fit::deferred_callback cb) {
RecursiveDescribeFormatNode(node, context, RecursiveState(options), std::move(cb));
}
OutputBuffer FormatNodeForConsole(const FormatNode& node, const ConsoleFormatOptions& options) {
return DoFormatNode(&node, RecursiveState(options), 0);
}
fxl::RefPtr<AsyncOutputBuffer> FormatValueForConsole(ExprValue value,
const ConsoleFormatOptions& options,
fxl::RefPtr<EvalContext> context,
const std::string& value_name) {
auto node = std::make_unique<FormatNode>(value_name, std::move(value));
node->set_child_kind(FormatNode::kVariable);
FormatNode* node_ptr = node.get();
auto out = fxl::MakeRefCounted<AsyncOutputBuffer>();
DescribeFormatNodeForConsole(node_ptr, options, context,
fit::defer_callback([node = std::move(node), options, out]() {
// Asynchronous expansion is complete, now format the completed
// output.
out->Complete(FormatNodeForConsole(*node, options));
}));
return out;
}
fxl::RefPtr<AsyncOutputBuffer> FormatVariableForConsole(const Variable* var,
const ConsoleFormatOptions& options,
fxl::RefPtr<EvalContext> context) {
auto out = fxl::MakeRefCounted<AsyncOutputBuffer>();
context->GetVariableValue(
RefPtrTo(var), [name = var->GetAssignedName(), context, options, out](ErrOrValue value) {
if (value.has_error()) {
// In the error case, construct a node with the error set so the formatting with other
// types of errors is consistent.
FormatNode err_node(name);
err_node.SetDescribedError(value.err());
out->Complete(FormatNodeForConsole(err_node, options));
} else {
out->Complete(FormatValueForConsole(value.take_value(), options, context, name));
}
});
return out;
}
fxl::RefPtr<AsyncOutputBuffer> FormatExpressionsForConsole(
const std::vector<std::string>& expressions, const ConsoleFormatOptions& options,
fxl::RefPtr<EvalContext> context) {
// Put all of the expressions in a "group" node and then evaluate and format this node. This will
// allow single- and multi-line formatting for the values and also handles all of the asynchronous
// state across all of the expressions automatically.
auto out = fxl::MakeRefCounted<AsyncOutputBuffer>();
auto list_node = std::make_unique<FormatNode>(FormatNode::GroupTag());
FormatNode* list_node_ptr = list_node.get();
for (const std::string& expr : expressions) {
// Use the expression as the description.
auto child_node = std::make_unique<FormatNode>(expr, expr);
list_node->children().push_back(std::move(child_node));
}
DescribeFormatNodeForConsole(
list_node_ptr, options, context,
fit::defer_callback([list_node = std::move(list_node), options, out]() {
RecursiveState state(options);
out->Complete(FormatNodeForConsole(*list_node, options));
}));
return out;
}
} // namespace zxdb