https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/123430
>From 10e89226a485d73acfb07ad6d72f3004d270a2f5 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Fri, 17 Jan 2025 16:51:21 -0800 Subject: [PATCH 1/4] [lldb] Support format string in the prompt Implement ansi::StripAnsiTerminalCodes and fix a long standing bug where using format strings in lldb's prompt resulted in an incorrect prompt column width. --- lldb/include/lldb/Utility/AnsiTerminal.h | 38 +++++++++++++++++++++ lldb/source/Host/common/Editline.cpp | 4 ++- lldb/test/API/terminal/TestEditline.py | 16 +++++++++ lldb/unittests/Utility/AnsiTerminalTest.cpp | 15 ++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/lldb/include/lldb/Utility/AnsiTerminal.h b/lldb/include/lldb/Utility/AnsiTerminal.h index 67795971d2ca89..22601e873dfe9e 100644 --- a/lldb/include/lldb/Utility/AnsiTerminal.h +++ b/lldb/include/lldb/Utility/AnsiTerminal.h @@ -171,7 +171,45 @@ inline std::string FormatAnsiTerminalCodes(llvm::StringRef format, } return fmt; } + +inline std::string StripAnsiTerminalCodes(llvm::StringRef str) { + std::string stripped; + while (!str.empty()) { + llvm::StringRef left, right; + + std::tie(left, right) = str.split(ANSI_ESC_START); + stripped += left; + + // ANSI_ESC_START not found. + if (left == str && right.empty()) + break; + + auto end = llvm::StringRef::npos; + for (size_t i = 0; i < right.size(); i++) { + char c = right[i]; + if (c == 'm' || c == 'G') { + end = i; + break; + } + if (isdigit(c) || c == ';') + continue; + + break; + } + + // ANSI_ESC_END not found. + if (end != llvm::StringRef::npos) { + str = right.substr(end + 1); + continue; + } + + stripped += ANSI_ESC_START; + str = right; + } + return stripped; } + +} // namespace ansi } // namespace lldb_private #endif diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index 6e35b15d69651d..d31fe3af946b37 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -14,6 +14,7 @@ #include "lldb/Host/Editline.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/Host.h" +#include "lldb/Utility/AnsiTerminal.h" #include "lldb/Utility/CompletionRequest.h" #include "lldb/Utility/FileSpec.h" #include "lldb/Utility/LLDBAssert.h" @@ -85,7 +86,8 @@ bool IsOnlySpaces(const EditLineStringType &content) { } static size_t ColumnWidth(llvm::StringRef str) { - return llvm::sys::locale::columnWidth(str); + std::string stripped = ansi::StripAnsiTerminalCodes(str); + return llvm::sys::locale::columnWidth(stripped); } static int GetOperation(HistoryOperation op) { diff --git a/lldb/test/API/terminal/TestEditline.py b/lldb/test/API/terminal/TestEditline.py index aa7d827e599441..40f8a50e06bf6d 100644 --- a/lldb/test/API/terminal/TestEditline.py +++ b/lldb/test/API/terminal/TestEditline.py @@ -69,6 +69,22 @@ def test_prompt_color(self): # Column: 1....6.8 self.child.expect(re.escape("\x1b[31m(lldb) \x1b[0m\x1b[8G")) + @skipIfAsan + @skipIfEditlineSupportMissing + def test_prompt_format_color(self): + """Test that we can change the prompt color with a format string.""" + self.launch(use_colors=True) + # Clear the prefix and suffix setting to simplify the output. + self.child.send('settings set prompt-ansi-prefix ""\n') + self.child.send('settings set prompt-ansi-suffix ""\n') + self.child.send('settings set prompt "${ansi.fg.red}(lldb)${ansi.normal} "\n') + self.child.send("foo") + # Make sure this change is reflected immediately. Check that the color + # is set (31) and the cursor position (8) is correct. + # Prompt: (lldb) _ + # Column: 1....6.8 + self.child.expect(re.escape("\x1b[31m(lldb)\x1b[0m foo")) + @skipIfAsan @skipIfEditlineSupportMissing def test_prompt_no_color(self): diff --git a/lldb/unittests/Utility/AnsiTerminalTest.cpp b/lldb/unittests/Utility/AnsiTerminalTest.cpp index a6dbfd61061420..1ba9565c3f6af3 100644 --- a/lldb/unittests/Utility/AnsiTerminalTest.cpp +++ b/lldb/unittests/Utility/AnsiTerminalTest.cpp @@ -16,16 +16,21 @@ TEST(AnsiTerminal, Empty) { EXPECT_EQ("", ansi::FormatAnsiTerminalCodes("")); } TEST(AnsiTerminal, WhiteSpace) { EXPECT_EQ(" ", ansi::FormatAnsiTerminalCodes(" ")); + EXPECT_EQ(" ", ansi::StripAnsiTerminalCodes(" ")); } TEST(AnsiTerminal, AtEnd) { EXPECT_EQ("abc\x1B[30m", ansi::FormatAnsiTerminalCodes("abc${ansi.fg.black}")); + + EXPECT_EQ("abc", ansi::StripAnsiTerminalCodes("abc\x1B[30m")); } TEST(AnsiTerminal, AtStart) { EXPECT_EQ("\x1B[30mabc", ansi::FormatAnsiTerminalCodes("${ansi.fg.black}abc")); + + EXPECT_EQ("abc", ansi::StripAnsiTerminalCodes("\x1B[30mabc")); } TEST(AnsiTerminal, KnownPrefix) { @@ -45,10 +50,20 @@ TEST(AnsiTerminal, Incomplete) { TEST(AnsiTerminal, Twice) { EXPECT_EQ("\x1B[30m\x1B[31mabc", ansi::FormatAnsiTerminalCodes("${ansi.fg.black}${ansi.fg.red}abc")); + + EXPECT_EQ("abc", ansi::StripAnsiTerminalCodes("\x1B[30m\x1B[31mabc")); } TEST(AnsiTerminal, Basic) { EXPECT_EQ( "abc\x1B[31mabc\x1B[0mabc", ansi::FormatAnsiTerminalCodes("abc${ansi.fg.red}abc${ansi.normal}abc")); + + EXPECT_EQ("abcabcabc", + ansi::StripAnsiTerminalCodes("abc\x1B[31mabc\x1B[0mabc")); +} + +TEST(AnsiTerminal, InvalidEscapeCode) { + EXPECT_EQ("abc\x1B[31kabcabc", + ansi::StripAnsiTerminalCodes("abc\x1B[31kabc\x1B[0mabc")); } >From bb799e95f19140deeae6a9750e8188018aeffc9b Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Mon, 20 Jan 2025 11:58:26 -0800 Subject: [PATCH 2/4] Address code review feedback --- lldb/include/lldb/Utility/AnsiTerminal.h | 25 ++++++------------------ lldb/test/API/terminal/TestEditline.py | 7 +++---- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/lldb/include/lldb/Utility/AnsiTerminal.h b/lldb/include/lldb/Utility/AnsiTerminal.h index 22601e873dfe9e..1939c49c7b859c 100644 --- a/lldb/include/lldb/Utility/AnsiTerminal.h +++ b/lldb/include/lldb/Utility/AnsiTerminal.h @@ -184,27 +184,14 @@ inline std::string StripAnsiTerminalCodes(llvm::StringRef str) { if (left == str && right.empty()) break; - auto end = llvm::StringRef::npos; - for (size_t i = 0; i < right.size(); i++) { - char c = right[i]; - if (c == 'm' || c == 'G') { - end = i; - break; - } - if (isdigit(c) || c == ';') - continue; - - break; - } - - // ANSI_ESC_END not found. - if (end != llvm::StringRef::npos) { + size_t end = right.find_first_not_of("0123456789;"); + if (end < right.size() && (right[end] == 'm' || right[end] == 'G')) { str = right.substr(end + 1); - continue; + } else { + // ANSI_ESC_END not found. + stripped += ANSI_ESC_START; + str = right; } - - stripped += ANSI_ESC_START; - str = right; } return stripped; } diff --git a/lldb/test/API/terminal/TestEditline.py b/lldb/test/API/terminal/TestEditline.py index 40f8a50e06bf6d..60baccf7304d62 100644 --- a/lldb/test/API/terminal/TestEditline.py +++ b/lldb/test/API/terminal/TestEditline.py @@ -2,7 +2,6 @@ Test that the lldb editline handling is configured correctly. """ - import lldb from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * @@ -75,9 +74,9 @@ def test_prompt_format_color(self): """Test that we can change the prompt color with a format string.""" self.launch(use_colors=True) # Clear the prefix and suffix setting to simplify the output. - self.child.send('settings set prompt-ansi-prefix ""\n') - self.child.send('settings set prompt-ansi-suffix ""\n') - self.child.send('settings set prompt "${ansi.fg.red}(lldb)${ansi.normal} "\n') + self.expect('settings set prompt-ansi-prefix ""') + self.expect('settings set prompt-ansi-suffix ""') + self.expect('settings set prompt "${ansi.fg.red}(lldb)${ansi.normal} "') self.child.send("foo") # Make sure this change is reflected immediately. Check that the color # is set (31) and the cursor position (8) is correct. >From 0a8bae84bdded39ea3f36e1bda45f527db0d396e Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Tue, 21 Jan 2025 14:13:14 -0800 Subject: [PATCH 3/4] Fix TestEditline.py --- lldb/include/lldb/Host/Editline.h | 15 ++++++++++----- lldb/source/Core/IOHandler.cpp | 22 +++++++++------------- lldb/source/Host/common/Editline.cpp | 6 +++--- lldb/test/API/terminal/TestEditline.py | 4 ++-- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/lldb/include/lldb/Host/Editline.h b/lldb/include/lldb/Host/Editline.h index 26deba38f8471c..27b863870090cb 100644 --- a/lldb/include/lldb/Host/Editline.h +++ b/lldb/include/lldb/Host/Editline.h @@ -152,7 +152,7 @@ using namespace line_editor; class Editline { public: Editline(const char *editor_name, FILE *input_file, FILE *output_file, - FILE *error_file, std::recursive_mutex &output_mutex); + FILE *error_file, bool color, std::recursive_mutex &output_mutex); ~Editline(); @@ -212,19 +212,23 @@ class Editline { } void SetPromptAnsiPrefix(std::string prefix) { - m_prompt_ansi_prefix = std::move(prefix); + if (m_color) + m_prompt_ansi_prefix = std::move(prefix); } void SetPromptAnsiSuffix(std::string suffix) { - m_prompt_ansi_suffix = std::move(suffix); + if (m_color) + m_prompt_ansi_suffix = std::move(suffix); } void SetSuggestionAnsiPrefix(std::string prefix) { - m_suggestion_ansi_prefix = std::move(prefix); + if (m_color) + m_suggestion_ansi_prefix = std::move(prefix); } void SetSuggestionAnsiSuffix(std::string suffix) { - m_suggestion_ansi_suffix = std::move(suffix); + if (m_color) + m_suggestion_ansi_suffix = std::move(suffix); } /// Prompts for and reads a single line of user input. @@ -400,6 +404,7 @@ class Editline { CompleteCallbackType m_completion_callback; SuggestionCallbackType m_suggestion_callback; + bool m_color; std::string m_prompt_ansi_prefix; std::string m_prompt_ansi_suffix; std::string m_suggestion_ansi_prefix; diff --git a/lldb/source/Core/IOHandler.cpp b/lldb/source/Core/IOHandler.cpp index 695c2481e353db..ca06b52b874db6 100644 --- a/lldb/source/Core/IOHandler.cpp +++ b/lldb/source/Core/IOHandler.cpp @@ -264,7 +264,7 @@ IOHandlerEditline::IOHandlerEditline( if (use_editline) { m_editline_up = std::make_unique<Editline>(editline_name, GetInputFILE(), GetOutputFILE(), GetErrorFILE(), - GetOutputMutex()); + m_color, GetOutputMutex()); m_editline_up->SetIsInputCompleteCallback( [this](Editline *editline, StringList &lines) { return this->IsInputCompleteCallback(editline, lines); @@ -278,12 +278,10 @@ IOHandlerEditline::IOHandlerEditline( m_editline_up->SetSuggestionCallback([this](llvm::StringRef line) { return this->SuggestionCallback(line); }); - if (m_color) { - m_editline_up->SetSuggestionAnsiPrefix(ansi::FormatAnsiTerminalCodes( - debugger.GetAutosuggestionAnsiPrefix())); - m_editline_up->SetSuggestionAnsiSuffix(ansi::FormatAnsiTerminalCodes( - debugger.GetAutosuggestionAnsiSuffix())); - } + m_editline_up->SetSuggestionAnsiPrefix(ansi::FormatAnsiTerminalCodes( + debugger.GetAutosuggestionAnsiPrefix())); + m_editline_up->SetSuggestionAnsiSuffix(ansi::FormatAnsiTerminalCodes( + debugger.GetAutosuggestionAnsiSuffix())); } // See if the delegate supports fixing indentation const char *indent_chars = delegate.IOHandlerGetFixIndentationCharacters(); @@ -478,12 +476,10 @@ bool IOHandlerEditline::SetPrompt(llvm::StringRef prompt) { #if LLDB_ENABLE_LIBEDIT if (m_editline_up) { m_editline_up->SetPrompt(m_prompt.empty() ? nullptr : m_prompt.c_str()); - if (m_color) { - m_editline_up->SetPromptAnsiPrefix( - ansi::FormatAnsiTerminalCodes(m_debugger.GetPromptAnsiPrefix())); - m_editline_up->SetPromptAnsiSuffix( - ansi::FormatAnsiTerminalCodes(m_debugger.GetPromptAnsiSuffix())); - } + m_editline_up->SetPromptAnsiPrefix( + ansi::FormatAnsiTerminalCodes(m_debugger.GetPromptAnsiPrefix())); + m_editline_up->SetPromptAnsiSuffix( + ansi::FormatAnsiTerminalCodes(m_debugger.GetPromptAnsiSuffix())); } #endif return true; diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index d31fe3af946b37..73da1d84816187 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -612,7 +612,7 @@ int Editline::GetCharacter(EditLineGetCharType *c) { } const char *Editline::Prompt() { - if (!m_prompt_ansi_prefix.empty() || !m_prompt_ansi_suffix.empty()) + if (m_color) m_needs_prompt_repaint = true; return m_current_prompt.c_str(); } @@ -1473,11 +1473,11 @@ Editline *Editline::InstanceFor(EditLine *editline) { } Editline::Editline(const char *editline_name, FILE *input_file, - FILE *output_file, FILE *error_file, + FILE *output_file, FILE *error_file, bool color, std::recursive_mutex &output_mutex) : m_editor_status(EditorStatus::Complete), m_input_file(input_file), m_output_file(output_file), m_error_file(error_file), - m_input_connection(fileno(input_file), false), + m_input_connection(fileno(input_file), false), m_color(color), m_output_mutex(output_mutex) { // Get a shared history instance m_editor_name = (editline_name == nullptr) ? "lldb-tmp" : editline_name; diff --git a/lldb/test/API/terminal/TestEditline.py b/lldb/test/API/terminal/TestEditline.py index 60baccf7304d62..ddaa441d5f7c1d 100644 --- a/lldb/test/API/terminal/TestEditline.py +++ b/lldb/test/API/terminal/TestEditline.py @@ -76,13 +76,13 @@ def test_prompt_format_color(self): # Clear the prefix and suffix setting to simplify the output. self.expect('settings set prompt-ansi-prefix ""') self.expect('settings set prompt-ansi-suffix ""') - self.expect('settings set prompt "${ansi.fg.red}(lldb)${ansi.normal} "') + self.expect('settings set prompt "${ansi.fg.red}(lldb) ${ansi.normal}"') self.child.send("foo") # Make sure this change is reflected immediately. Check that the color # is set (31) and the cursor position (8) is correct. # Prompt: (lldb) _ # Column: 1....6.8 - self.child.expect(re.escape("\x1b[31m(lldb)\x1b[0m foo")) + self.child.expect(re.escape("\x1b[31m(lldb) \x1b[0m\x1b[8Gfoo")) @skipIfAsan @skipIfEditlineSupportMissing >From f0e56466fdef75d5edc1be6c82554ae90b773983 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Tue, 21 Jan 2025 19:10:50 -0800 Subject: [PATCH 4/4] Update EditlineTest.cpp --- lldb/unittests/Editline/EditlineTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lldb/unittests/Editline/EditlineTest.cpp b/lldb/unittests/Editline/EditlineTest.cpp index 333ad77a0a16fd..1327b587e7c3d6 100644 --- a/lldb/unittests/Editline/EditlineTest.cpp +++ b/lldb/unittests/Editline/EditlineTest.cpp @@ -118,7 +118,7 @@ EditlineAdapter::EditlineAdapter() // Create an Editline instance. _editline_sp.reset(new lldb_private::Editline( "gtest editor", *_el_secondary_file, *_el_secondary_file, - *_el_secondary_file, output_mutex)); + *_el_secondary_file, /*color=*/false, output_mutex)); _editline_sp->SetPrompt("> "); // Hookup our input complete callback. _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits