https://github.com/DavidSpickett updated https://github.com/llvm/llvm-project/pull/183558
>From d32e7b182843b3e9a4130c95270f74c7784bfe1c Mon Sep 17 00:00:00 2001 From: David Spickett <[email protected]> Date: Thu, 26 Feb 2026 15:48:42 +0000 Subject: [PATCH 1/2] [lldb] Indent option help with ANSI cursor codes when possible. This avoids formatting empty space when a range of text formatted by ANSI codes is split across lines. This is not currently done in any option, but the `${...}` syntax we have does support marking any range of text, so it could be done in future, and fixing it is simple. As an example, if I change a breakpoint option: ``` "${S}et the breakpoint only in this shared library. Can repeat " - "this option multiple times to specify multiple shared libraries.">; + "this option multiple ${times to specify multiple} shared libraries.">; ``` This applies the underline to words that will be split across lines. In the outputs below, `^` represents an underlined character. With spaces: ``` -s <shlib-name> ( --shlib <shlib-name> ) Set the breakpoint only in this shared library. Can repeat this option multiple times to ^^^^^^^^ specify multiple shared libraries. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` The indent and the text are underlined, this is not what we want. With cursor movement: ``` -s <shlib-name> ( --shlib <shlib-name> ) Set the breakpoint only in this shared library. Can repeat this option multiple times to ^^^^^^^^ specify multiple shared libraries. ^^^^^^^^^^^^^^^^ ``` Only the text is underlined, which is correct. If we are not allowed to use ANSI (use-color is off), then the descriptions will be stripped of ANSI anyway, so this is not a problem. --- lldb/include/lldb/Utility/AnsiTerminal.h | 20 +++++++++++---- lldb/source/Interpreter/Options.cpp | 3 ++- lldb/test/API/commands/help/TestHelp.py | 8 +++--- lldb/unittests/Utility/AnsiTerminalTest.cpp | 27 ++++++++++++++++----- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lldb/include/lldb/Utility/AnsiTerminal.h b/lldb/include/lldb/Utility/AnsiTerminal.h index 4ab6ef1eb1be7..8dddb2a487b13 100644 --- a/lldb/include/lldb/Utility/AnsiTerminal.h +++ b/lldb/include/lldb/Utility/AnsiTerminal.h @@ -74,6 +74,8 @@ // Cursor Position, set cursor to position [l, c] (default = [1, 1]). #define ANSI_CSI_CUP(...) ANSI_ESC_START #__VA_ARGS__ "H" +// Cursor Position, move cursor forward N columns. +#define ANSI_CSI_CUF(N) (ANSI_ESC_START + N + "C") // Reset cursor to position. #define ANSI_CSI_RESET_CURSOR ANSI_CSI_CUP() // Erase In Display. @@ -405,11 +407,9 @@ inline std::string TrimAndPad(llvm::StringRef str, size_t visible_length, // Output text that may contain ANSI codes, word wrapped (wrapped at whitespace) // to the given stream. The indent level of the stream is counted towards the // output line length. -// FIXME: If an ANSI code is applied to multiple words and those words are split -// across lines, the code will apply to the indentation as well as the -// text. inline void OutputWordWrappedLines(Stream &strm, llvm::StringRef text, - uint32_t output_max_columns) { + uint32_t output_max_columns, + bool use_color) { // We will indent using the stream, so leading whitespace is not significant. text = text.ltrim(); if (text.empty()) @@ -425,7 +425,17 @@ inline void OutputWordWrappedLines(Stream &strm, llvm::StringRef text, if (!first_line) strm.EOL(); first_line = false; - strm.Indent(split); + + if (use_color) { + // If we are allowed to use colour (aka ANSI codes), we can indent using + // ANSI cursor movement. This means that if an ANSI formatted range of + // text is split across two lines, the indentation is not also formatted. + // Which it would be if we just emitted spaces. + const std::string ansi_indent = + ANSI_CSI_CUF(std::to_string(strm.GetIndentLevel())); + strm << ansi_indent << split; + } else + strm.Indent(split); text = text.drop_front(split.size()).ltrim(); } diff --git a/lldb/source/Interpreter/Options.cpp b/lldb/source/Interpreter/Options.cpp index 0bda2a912e1a1..e87426c48165e 100644 --- a/lldb/source/Interpreter/Options.cpp +++ b/lldb/source/Interpreter/Options.cpp @@ -275,7 +275,8 @@ void Options::OutputFormattedUsageText(Stream &strm, actual_text.append( ansi::FormatAnsiTerminalCodes(option_def.usage_text, use_color)); - ansi::OutputWordWrappedLines(strm, actual_text, output_max_columns); + ansi::OutputWordWrappedLines(strm, actual_text, output_max_columns, + use_color); } bool Options::SupportsLongOption(const char *long_option) { diff --git a/lldb/test/API/commands/help/TestHelp.py b/lldb/test/API/commands/help/TestHelp.py index cb6e9473c1047..1ef6bc0e1cd43 100644 --- a/lldb/test/API/commands/help/TestHelp.py +++ b/lldb/test/API/commands/help/TestHelp.py @@ -325,6 +325,8 @@ def test_help_option_description_terminal_width_no_ansi(self): def test_help_option_description_terminal_width_with_ansi(self): """Test that help on commands formats option descriptions that include ANSI codes acccording to the terminal width.""" + # Note that because color is enabled, we will use ANSI cursor codes to + # indent, rather than spaces. self.runCmd("settings set use-color on") # Should fit on one line. @@ -334,7 +336,7 @@ def test_help_option_description_terminal_width_with_ansi(self): matching=True, patterns=[ # The "S" of "Set" is underlined. - r"\s+\x1b\[4mS\x1b\[0met the breakpoint only in this shared library. Can repeat this option multiple times to specify multiple shared libraries.\n" + r"\x1b\[12C\x1b\[4mS\x1b\[0met the breakpoint only in this shared library. Can repeat this option multiple times to specify multiple shared libraries.\n" ], ) @@ -343,8 +345,8 @@ def test_help_option_description_terminal_width_with_ansi(self): "help breakpoint set", matching=True, patterns=[ - r"\s+\x1b\[4mS\x1b\[0met the breakpoint only in this shared library. Can repeat this option multiple times\n" - r"\s+to specify multiple shared libraries.\n" + r"\x1b\[12C\x1b\[4mS\x1b\[0met the breakpoint only in this shared library. Can repeat this option multiple times\n" + r"\x1b\[12Cto specify multiple shared libraries.\n" ], ) diff --git a/lldb/unittests/Utility/AnsiTerminalTest.cpp b/lldb/unittests/Utility/AnsiTerminalTest.cpp index 6027b21482bdc..d04874a848422 100644 --- a/lldb/unittests/Utility/AnsiTerminalTest.cpp +++ b/lldb/unittests/Utility/AnsiTerminalTest.cpp @@ -260,7 +260,8 @@ static void TestLines(const std::string &input, int indent, const llvm::StringRef &expected) { StreamString strm; strm.SetIndentLevel(indent); - ansi::OutputWordWrappedLines(strm, input, output_max_columns); + ansi::OutputWordWrappedLines(strm, input, output_max_columns, + /*use_color=*/false); EXPECT_EQ(expected, strm.GetString()); } @@ -300,9 +301,23 @@ TEST(AnsiTerminal, OutputWordWrappedLines) { // Must remove the spaces from the end of the first line. TestLines("The quick brown fox.", 0, 15, "The quick\nbrown fox.\n"); - // FIXME: ANSI codes applied to > 1 word end up applying to all those words - // and the indent if those words are split up. We should use cursor - // positioning to do the indentation instead. - TestLines("\x1B[4mabc def\x1B[0m ghi", 2, 6, - " \x1B[4mabc\n def\x1B[0m\n ghi\n"); + // If ANSI formatting is applied to multiple words, that range of words may + // be split over multiple lines. + StreamString indented_strm; + indented_strm.SetIndentLevel(2); + ansi::OutputWordWrappedLines(indented_strm, "\x1B[4mabc def\x1B[0m ghi", 6, + /*use_color=*/false); + // The two spaces before "def" would have the previous ANSI code applied to + // them. + EXPECT_EQ(" \x1B[4mabc\n def\x1B[0m\n ghi\n", indented_strm.GetString()); + + // If we can emit ANSI, we can use cursor positions to skip forward, + // which leaves the indent unformatted. + // (in normal use the inputs are command descriptions, which already have + // ANSI removed if the terminal does not support it) + indented_strm.Clear(); + ansi::OutputWordWrappedLines(indented_strm, "\x1B[4mabc def\x1B[0m ghi", 6, + /*use_color=*/true); + EXPECT_EQ("\x1B[2C\x1B[4mabc\n\x1B[2Cdef\x1B[0m\n\x1B[2Cghi\n", + indented_strm.GetString()); } >From c2a344d840558df69c5bb3fec3da985ccc0cb9af Mon Sep 17 00:00:00 2001 From: David Spickett <[email protected]> Date: Thu, 26 Feb 2026 16:10:58 +0000 Subject: [PATCH 2/2] misc cleanup --- lldb/include/lldb/Utility/AnsiTerminal.h | 4 ++-- lldb/test/API/commands/help/TestHelp.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lldb/include/lldb/Utility/AnsiTerminal.h b/lldb/include/lldb/Utility/AnsiTerminal.h index 8dddb2a487b13..e2768dabcc31f 100644 --- a/lldb/include/lldb/Utility/AnsiTerminal.h +++ b/lldb/include/lldb/Utility/AnsiTerminal.h @@ -419,6 +419,8 @@ inline void OutputWordWrappedLines(Stream &strm, llvm::StringRef text, const uint32_t max_text_width = output_max_columns - strm.GetIndentLevel() - 1; bool first_line = true; + const std::string ansi_indent = + ANSI_CSI_CUF(std::to_string(strm.GetIndentLevel())); while (!text.empty()) { std::string split = TrimAtWordBoundary(text, max_text_width); @@ -431,8 +433,6 @@ inline void OutputWordWrappedLines(Stream &strm, llvm::StringRef text, // ANSI cursor movement. This means that if an ANSI formatted range of // text is split across two lines, the indentation is not also formatted. // Which it would be if we just emitted spaces. - const std::string ansi_indent = - ANSI_CSI_CUF(std::to_string(strm.GetIndentLevel())); strm << ansi_indent << split; } else strm.Indent(split); diff --git a/lldb/test/API/commands/help/TestHelp.py b/lldb/test/API/commands/help/TestHelp.py index 1ef6bc0e1cd43..8423d410ca306 100644 --- a/lldb/test/API/commands/help/TestHelp.py +++ b/lldb/test/API/commands/help/TestHelp.py @@ -334,9 +334,9 @@ def test_help_option_description_terminal_width_with_ansi(self): self.expect( "help breakpoint set", matching=True, - patterns=[ + substrs=[ # The "S" of "Set" is underlined. - r"\x1b\[12C\x1b\[4mS\x1b\[0met the breakpoint only in this shared library. Can repeat this option multiple times to specify multiple shared libraries.\n" + "\x1b[12C\x1b[4mS\x1b[0met the breakpoint only in this shared library. Can repeat this option multiple times to specify multiple shared libraries.\n" ], ) @@ -344,9 +344,9 @@ def test_help_option_description_terminal_width_with_ansi(self): self.expect( "help breakpoint set", matching=True, - patterns=[ - r"\x1b\[12C\x1b\[4mS\x1b\[0met the breakpoint only in this shared library. Can repeat this option multiple times\n" - r"\x1b\[12Cto specify multiple shared libraries.\n" + substrs=[ + "\x1b[12C\x1b[4mS\x1b[0met the breakpoint only in this shared library. Can repeat this option multiple times\n" + "\x1b[12Cto specify multiple shared libraries.\n" ], ) _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
