Author: David Spickett Date: 2026-03-02T09:58:26Z New Revision: 900f70258b905d0b5d9eaecc74cc9130e79f899d
URL: https://github.com/llvm/llvm-project/commit/900f70258b905d0b5d9eaecc74cc9130e79f899d DIFF: https://github.com/llvm/llvm-project/commit/900f70258b905d0b5d9eaecc74cc9130e79f899d.diff LOG: [lldb] Indent option help with ANSI cursor codes when possible. (#183558) 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. --------- Co-authored-by: Jonas Devlieghere <[email protected]> Added: Modified: lldb/include/lldb/Utility/AnsiTerminal.h lldb/source/Interpreter/Options.cpp lldb/test/API/commands/help/TestHelp.py lldb/unittests/Utility/AnsiTerminalTest.cpp Removed: ################################################################################ diff --git a/lldb/include/lldb/Utility/AnsiTerminal.h b/lldb/include/lldb/Utility/AnsiTerminal.h index 4ab6ef1eb1be7..68054454dc202 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()) @@ -419,13 +419,24 @@ 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); 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. + 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..8423d410ca306 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. @@ -332,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"\s+\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" ], ) @@ -342,9 +344,9 @@ def test_help_option_description_terminal_width_with_ansi(self): self.expect( "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" + 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" ], ) 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()); } _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
