[Lldb-commits] [lldb] [lldb] Indent option help with ANSI cursor codes when possible. (PR #183558)
David Spickett via lldb-commits
lldb-commits at lists.llvm.org
Mon Mar 2 01:51:36 PST 2026
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 <david.spickett at arm.com>
Date: Thu, 26 Feb 2026 15:48:42 +0000
Subject: [PATCH 1/3] [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 <david.spickett at arm.com>
Date: Thu, 26 Feb 2026 16:10:58 +0000
Subject: [PATCH 2/3] 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"
],
)
>From 7e5b53b0b0726c712d490bc4e94afd2790a066ff Mon Sep 17 00:00:00 2001
From: David Spickett <david.spickett at linaro.org>
Date: Mon, 2 Mar 2026 09:51:27 +0000
Subject: [PATCH 3/3] Apply suggestion from @JDevlieghere
Co-authored-by: Jonas Devlieghere <jonas at devlieghere.com>
---
lldb/include/lldb/Utility/AnsiTerminal.h | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lldb/include/lldb/Utility/AnsiTerminal.h b/lldb/include/lldb/Utility/AnsiTerminal.h
index e2768dabcc31f..68054454dc202 100644
--- a/lldb/include/lldb/Utility/AnsiTerminal.h
+++ b/lldb/include/lldb/Utility/AnsiTerminal.h
@@ -434,8 +434,9 @@ inline void OutputWordWrappedLines(Stream &strm, llvm::StringRef text,
// 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
+ } else {
strm.Indent(split);
+ }
text = text.drop_front(split.size()).ltrim();
}
More information about the lldb-commits
mailing list