https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/121860
>From 13524447f9af1c8d1923e9ef8cc3693a1c53253a Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Fri, 17 Jan 2025 17:10:36 -0800 Subject: [PATCH 1/5] [lldb] Implement a statusline in LLDB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a statusline to command-line LLDB to display progress events and other information related to the current state of the debugger. The statusline is a dedicated area displayed the bottom of the screen. The contents of the status line are configurable through a setting consisting of LLDB’s format strings. The statusline is configurable through the `statusline-format` setting. The default configuration shows the target name, the current file, the stop reason and the current progress event. ``` (lldb) settings show statusline-format statusline-format (format-string) = "${ansi.bg.cyan}${ansi.fg.black}{${target.file.basename}}{ | ${line.file.basename}:${line.number}:${line.column}}{ | ${thread.stop-reason}}{ | {${progress.count} }${progress.message}}" ``` The statusline is enabled by default, but can be disabled with the following setting: ``` (lldb) settings set show-statusline false ``` The statusline supersedes the current progress reporting implementation. Consequently, the following settings no longer have any effect (but continue to exist): ``` show-progress -- Whether to show progress or not if the debugger's output is an interactive color-enabled terminal. show-progress-ansi-prefix -- When displaying progress in a color-enabled terminal, use the ANSI terminal code specified in this format immediately before the progress message. show-progress-ansi-suffix -- When displaying progress in a color-enabled terminal, use the ANSI terminal code specified in this format immediately after the progress message. ``` RFC: https://discourse.llvm.org/t/rfc-lldb-statusline/83948 --- lldb/include/lldb/Core/Debugger.h | 24 ++- lldb/include/lldb/Core/FormatEntity.h | 4 +- lldb/include/lldb/Core/Statusline.h | 58 ++++++ .../Python/lldbsuite/test/lldbtest.py | 2 + lldb/source/Core/CMakeLists.txt | 1 + lldb/source/Core/CoreProperties.td | 8 + lldb/source/Core/Debugger.cpp | 159 ++++++++-------- lldb/source/Core/FormatEntity.cpp | 44 ++++- lldb/source/Core/Statusline.cpp | 171 ++++++++++++++++++ .../TestTrimmedProgressReporting.py | 51 ------ .../API/functionalities/statusline/Makefile | 3 + .../statusline/TestStatusline.py | 57 ++++++ .../API/functionalities/statusline/main.c | 11 ++ 13 files changed, 458 insertions(+), 135 deletions(-) create mode 100644 lldb/include/lldb/Core/Statusline.h create mode 100644 lldb/source/Core/Statusline.cpp delete mode 100644 lldb/test/API/functionalities/progress_reporting/TestTrimmedProgressReporting.py create mode 100644 lldb/test/API/functionalities/statusline/Makefile create mode 100644 lldb/test/API/functionalities/statusline/TestStatusline.py create mode 100644 lldb/test/API/functionalities/statusline/main.c diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 6ebc6147800e1..9e2100662c6de 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -19,6 +19,7 @@ #include "lldb/Core/FormatEntity.h" #include "lldb/Core/IOHandler.h" #include "lldb/Core/SourceManager.h" +#include "lldb/Core/Statusline.h" #include "lldb/Core/UserSettingsController.h" #include "lldb/Host/HostThread.h" #include "lldb/Host/StreamFile.h" @@ -303,6 +304,10 @@ class Debugger : public std::enable_shared_from_this<Debugger>, bool SetShowProgress(bool show_progress); + bool GetShowStatusline() const; + + const FormatEntity::Entry *GetStatuslineFormat() const; + llvm::StringRef GetShowProgressAnsiPrefix() const; llvm::StringRef GetShowProgressAnsiSuffix() const; @@ -599,11 +604,20 @@ class Debugger : public std::enable_shared_from_this<Debugger>, return m_source_file_cache; } + struct ProgressReport { + uint64_t id; + uint64_t completed; + uint64_t total; + std::string message; + }; + std::optional<ProgressReport> GetCurrentProgressReport() const; + protected: friend class CommandInterpreter; friend class REPL; friend class Progress; friend class ProgressManager; + friend class Statusline; /// Report progress events. /// @@ -656,6 +670,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>, lldb::LockableStreamFileSP GetErrorStreamSP() { return m_error_stream_sp; } /// @} + bool StatuslineSupported(); + void PushIOHandler(const lldb::IOHandlerSP &reader_sp, bool cancel_top_handler = true); @@ -732,7 +748,7 @@ class Debugger : public std::enable_shared_from_this<Debugger>, IOHandlerStack m_io_handler_stack; std::recursive_mutex m_io_handler_synchronous_mutex; - std::optional<uint64_t> m_current_event_id; + std::optional<Statusline> m_statusline; llvm::StringMap<std::weak_ptr<LogHandler>> m_stream_handlers; std::shared_ptr<CallbackLogHandler> m_callback_handler_sp; @@ -749,6 +765,12 @@ class Debugger : public std::enable_shared_from_this<Debugger>, lldb::TargetSP m_dummy_target_sp; Diagnostics::CallbackID m_diagnostics_callback_id; + /// Bookkeeping for command line progress events. + /// @{ + llvm::SmallVector<ProgressReport, 4> m_progress_reports; + mutable std::mutex m_progress_reports_mutex; + /// @} + std::mutex m_destroy_callback_mutex; lldb::callback_token_t m_destroy_callback_next_token = 0; struct DestroyCallbackInfo { diff --git a/lldb/include/lldb/Core/FormatEntity.h b/lldb/include/lldb/Core/FormatEntity.h index c9d5af1f31673..51e9ce37e54e7 100644 --- a/lldb/include/lldb/Core/FormatEntity.h +++ b/lldb/include/lldb/Core/FormatEntity.h @@ -100,7 +100,9 @@ struct Entry { LineEntryColumn, LineEntryStartAddress, LineEntryEndAddress, - CurrentPCArrow + CurrentPCArrow, + ProgressCount, + ProgressMessage, }; struct Definition { diff --git a/lldb/include/lldb/Core/Statusline.h b/lldb/include/lldb/Core/Statusline.h new file mode 100644 index 0000000000000..21bd58d933b9e --- /dev/null +++ b/lldb/include/lldb/Core/Statusline.h @@ -0,0 +1,58 @@ +//===-- Statusline.h -----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_CORE_STATUSLINE_H +#define LLDB_CORE_STATUSLINE_H + +#include "lldb/lldb-forward.h" +#include "llvm/ADT/StringRef.h" +#include <csignal> +#include <string> + +namespace lldb_private { +class Statusline { +public: + Statusline(Debugger &debugger); + ~Statusline(); + + /// Reduce the scroll window and draw the statusline. + void Enable(); + + /// Hide the statusline and extend the scroll window. + void Disable(); + + /// Redraw the statusline. If update is false, this will redraw the last + /// string. + void Redraw(bool update = true); + + /// Inform the statusline that the terminal dimensions have changed. + void TerminalSizeChanged(); + +private: + /// Draw the statusline with the given text. + void Draw(std::string msg); + + /// Update terminal dimensions. + void UpdateTerminalProperties(); + + /// Set the scroll window to the given height. + void SetScrollWindow(uint64_t height); + + /// Clear the statusline (without redrawing the background). + void Reset(); + + Debugger &m_debugger; + std::string m_last_str; + + volatile std::sig_atomic_t m_terminal_size_has_changed = 1; + uint64_t m_terminal_width = 0; + uint64_t m_terminal_height = 0; + uint64_t m_scroll_height = 0; +}; +} // namespace lldb_private +#endif // LLDB_CORE_STATUSLINE_H diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py index 81b286340560d..056ae1a0e62af 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -765,6 +765,8 @@ def setUpCommands(cls): # Disable fix-its by default so that incorrect expressions in tests don't # pass just because Clang thinks it has a fix-it. "settings set target.auto-apply-fixits false", + # Disable the statusline in PExpect tests. + "settings set show-statusline false", # Testsuite runs in parallel and the host can have also other load. "settings set plugin.process.gdb-remote.packet-timeout 60", 'settings set symbols.clang-modules-cache-path "{}"'.format( diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt index 82fb5f42f9f4b..70509ca4398c5 100644 --- a/lldb/source/Core/CMakeLists.txt +++ b/lldb/source/Core/CMakeLists.txt @@ -50,6 +50,7 @@ add_lldb_library(lldbCore Opcode.cpp PluginManager.cpp Progress.cpp + Statusline.cpp RichManglingContext.cpp SearchFilter.cpp Section.cpp diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td index d3816c3070bbc..0c6f93cb23e45 100644 --- a/lldb/source/Core/CoreProperties.td +++ b/lldb/source/Core/CoreProperties.td @@ -172,6 +172,14 @@ let Definition = "debugger" in { Global, DefaultStringValue<"${ansi.normal}">, Desc<"When displaying progress in a color-enabled terminal, use the ANSI terminal code specified in this format immediately after the progress message.">; + def ShowStatusline: Property<"show-statusline", "Boolean">, + Global, + DefaultTrue, + Desc<"Whether to show a statusline at the bottom of the terminal.">; + def StatuslineFormat: Property<"statusline-format", "FormatEntity">, + Global, + DefaultStringValue<"${ansi.bg.blue}${ansi.fg.black}{${target.file.basename}}{ | ${line.file.basename}:${line.number}:${line.column}}{ | ${thread.stop-reason}}{ | {${progress.count} }${progress.message}}">, + Desc<"List of statusline format entities.">; def UseSourceCache: Property<"use-source-cache", "Boolean">, Global, DefaultTrue, diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 585138535203d..38e0b1505edec 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -243,6 +243,19 @@ Status Debugger::SetPropertyValue(const ExecutionContext *exe_ctx, // Prompt colors changed. Ping the prompt so it can reset the ansi // terminal codes. SetPrompt(GetPrompt()); + } else if (property_path == + g_debugger_properties[ePropertyShowStatusline].name) { + // Statusline setting changed. If we have a statusline instance, update it + // now. Otherwise it will get created in the default event handler. + if (StatuslineSupported()) + m_statusline.emplace(*this); + else + m_statusline.reset(); + } else if (property_path == + g_debugger_properties[ePropertyStatuslineFormat].name) { + // Statusline format changed. Redraw the statusline. + if (m_statusline) + m_statusline->Redraw(); } else if (property_path == g_debugger_properties[ePropertyUseSourceCache].name) { // use-source-cache changed. Wipe out the cache contents if it was @@ -375,6 +388,8 @@ bool Debugger::SetTerminalWidth(uint64_t term_width) { if (auto handler_sp = m_io_handler_stack.Top()) handler_sp->TerminalSizeChanged(); + if (m_statusline) + m_statusline->TerminalSizeChanged(); return success; } @@ -391,6 +406,8 @@ bool Debugger::SetTerminalHeight(uint64_t term_height) { if (auto handler_sp = m_io_handler_stack.Top()) handler_sp->TerminalSizeChanged(); + if (m_statusline) + m_statusline->TerminalSizeChanged(); return success; } @@ -453,6 +470,17 @@ llvm::StringRef Debugger::GetShowProgressAnsiSuffix() const { idx, g_debugger_properties[idx].default_cstr_value); } +bool Debugger::GetShowStatusline() const { + const uint32_t idx = ePropertyShowStatusline; + return GetPropertyAtIndexAs<bool>( + idx, g_debugger_properties[idx].default_uint_value != 0); +} + +const FormatEntity::Entry *Debugger::GetStatuslineFormat() const { + constexpr uint32_t idx = ePropertyStatuslineFormat; + return GetPropertyAtIndexAs<const FormatEntity::Entry *>(idx); +} + bool Debugger::GetUseAutosuggestion() const { const uint32_t idx = ePropertyShowAutosuggestion; return GetPropertyAtIndexAs<bool>( @@ -1096,12 +1124,18 @@ void Debugger::SetErrorFile(FileSP file_sp) { } void Debugger::SaveInputTerminalState() { + if (m_statusline) + m_statusline->Disable(); int fd = GetInputFile().GetDescriptor(); if (fd != File::kInvalidDescriptor) m_terminal_state.Save(fd, true); } -void Debugger::RestoreInputTerminalState() { m_terminal_state.Restore(); } +void Debugger::RestoreInputTerminalState() { + m_terminal_state.Restore(); + if (m_statusline) + m_statusline->Enable(); +} ExecutionContext Debugger::GetSelectedExecutionContext() { bool adopt_selected = true; @@ -1925,6 +1959,17 @@ void Debugger::CancelForwardEvents(const ListenerSP &listener_sp) { m_forward_listener_sp.reset(); } +bool Debugger::StatuslineSupported() { + if (GetShowStatusline()) { + if (lldb::LockableStreamFileSP stream_sp = GetOutputStreamSP()) { + File &file = stream_sp->GetUnlockedFile(); + return file.GetIsInteractive() && file.GetIsRealTerminal() && + file.GetIsTerminalWithColors(); + } + } + return false; +} + lldb::thread_result_t Debugger::DefaultEventHandler() { ListenerSP listener_sp(GetListener()); ConstString broadcaster_class_target(Target::GetStaticBroadcasterClass()); @@ -1964,6 +2009,9 @@ lldb::thread_result_t Debugger::DefaultEventHandler() { // are now listening to all required events so no events get missed m_sync_broadcaster.BroadcastEvent(eBroadcastBitEventThreadIsListening); + if (!m_statusline && StatuslineSupported()) + m_statusline.emplace(*this); + bool done = false; while (!done) { EventSP event_sp; @@ -2018,8 +2066,14 @@ lldb::thread_result_t Debugger::DefaultEventHandler() { if (m_forward_listener_sp) m_forward_listener_sp->AddEvent(event_sp); } + if (m_statusline) + m_statusline->Redraw(); } } + + if (m_statusline) + m_statusline.reset(); + return {}; } @@ -2082,84 +2136,39 @@ void Debugger::HandleProgressEvent(const lldb::EventSP &event_sp) { if (!data) return; - // Do some bookkeeping for the current event, regardless of whether we're - // going to show the progress. - const uint64_t id = data->GetID(); - if (m_current_event_id) { - Log *log = GetLog(LLDBLog::Events); - if (log && log->GetVerbose()) { - StreamString log_stream; - log_stream.AsRawOstream() - << static_cast<void *>(this) << " Debugger(" << GetID() - << ")::HandleProgressEvent( m_current_event_id = " - << *m_current_event_id << ", data = { "; - data->Dump(&log_stream); - log_stream << " } )"; - log->PutString(log_stream.GetString()); - } - if (id != *m_current_event_id) - return; - if (data->GetCompleted() == data->GetTotal()) - m_current_event_id.reset(); - } else { - m_current_event_id = id; - } - - // Decide whether we actually are going to show the progress. This decision - // can change between iterations so check it inside the loop. - if (!GetShowProgress()) - return; - - // Determine whether the current output file is an interactive terminal with - // color support. We assume that if we support ANSI escape codes we support - // vt100 escape codes. - FileSP file_sp = GetOutputFileSP(); - if (!file_sp->GetIsInteractive() || !file_sp->GetIsTerminalWithColors()) - return; - - StreamUP output = GetAsyncOutputStream(); + // Make a local copy of the incoming progress report that we'll store. + ProgressReport progress_report{data->GetID(), data->GetCompleted(), + data->GetTotal(), data->GetMessage()}; - // Print over previous line, if any. - output->Printf("\r"); - - if (data->GetCompleted() == data->GetTotal()) { - // Clear the current line. - output->Printf("\x1B[2K"); - output->Flush(); - return; + // Do some bookkeeping regardless of whether we're going to display + // progress reports. + { + std::lock_guard<std::mutex> guard(m_progress_reports_mutex); + auto it = std::find_if( + m_progress_reports.begin(), m_progress_reports.end(), + [&](const auto &report) { return report.id == progress_report.id; }); + if (it != m_progress_reports.end()) { + const bool complete = data->GetCompleted() == data->GetTotal(); + if (complete) + m_progress_reports.erase(it); + else + *it = progress_report; + } else { + m_progress_reports.push_back(progress_report); + } } - // Trim the progress message if it exceeds the window's width and print it. - std::string message = data->GetMessage(); - if (data->IsFinite()) - message = llvm::formatv("[{0}/{1}] {2}", data->GetCompleted(), - data->GetTotal(), message) - .str(); - - // Trim the progress message if it exceeds the window's width and print it. - const uint32_t term_width = GetTerminalWidth(); - const uint32_t ellipsis = 3; - if (message.size() + ellipsis >= term_width) - message.resize(term_width - ellipsis); - - const bool use_color = GetUseColor(); - llvm::StringRef ansi_prefix = GetShowProgressAnsiPrefix(); - if (!ansi_prefix.empty()) - output->Printf( - "%s", ansi::FormatAnsiTerminalCodes(ansi_prefix, use_color).c_str()); - - output->Printf("%s...", message.c_str()); - - llvm::StringRef ansi_suffix = GetShowProgressAnsiSuffix(); - if (!ansi_suffix.empty()) - output->Printf( - "%s", ansi::FormatAnsiTerminalCodes(ansi_suffix, use_color).c_str()); - - // Clear until the end of the line. - output->Printf("\x1B[K\r"); + // Redraw the statusline if enabled. + if (m_statusline) + m_statusline->Redraw(); +} - // Flush the output. - output->Flush(); +std::optional<Debugger::ProgressReport> +Debugger::GetCurrentProgressReport() const { + std::lock_guard<std::mutex> guard(m_progress_reports_mutex); + if (m_progress_reports.empty()) + return std::nullopt; + return m_progress_reports.back(); } void Debugger::HandleDiagnosticEvent(const lldb::EventSP &event_sp) { diff --git a/lldb/source/Core/FormatEntity.cpp b/lldb/source/Core/FormatEntity.cpp index 7fe22994d7f7e..8a32195769357 100644 --- a/lldb/source/Core/FormatEntity.cpp +++ b/lldb/source/Core/FormatEntity.cpp @@ -166,6 +166,10 @@ constexpr Definition g_target_child_entries[] = { Entry::DefinitionWithChildren("file", EntryType::TargetFile, g_file_child_entries)}; +constexpr Definition g_progress_child_entries[] = { + Definition("count", EntryType::ProgressCount), + Definition("message", EntryType::ProgressMessage)}; + #define _TO_STR2(_val) #_val #define _TO_STR(_val) _TO_STR2(_val) @@ -259,7 +263,10 @@ constexpr Definition g_top_level_entries[] = { Entry::DefinitionWithChildren("target", EntryType::Invalid, g_target_child_entries), Entry::DefinitionWithChildren("var", EntryType::Variable, - g_var_child_entries, true)}; + g_var_child_entries, true), + Entry::DefinitionWithChildren("progress", EntryType::Invalid, + g_progress_child_entries), +}; constexpr Definition g_root = Entry::DefinitionWithChildren( "<root>", EntryType::Root, g_top_level_entries); @@ -358,6 +365,8 @@ const char *FormatEntity::Entry::TypeToCString(Type t) { ENUM_TO_CSTR(LineEntryStartAddress); ENUM_TO_CSTR(LineEntryEndAddress); ENUM_TO_CSTR(CurrentPCArrow); + ENUM_TO_CSTR(ProgressCount); + ENUM_TO_CSTR(ProgressMessage); } return "???"; } @@ -1198,12 +1207,10 @@ bool FormatEntity::Format(const Entry &entry, Stream &s, // FormatEntity::Entry::Definition encoding return false; case Entry::Type::EscapeCode: - if (exe_ctx) { - if (Target *target = exe_ctx->GetTargetPtr()) { - Debugger &debugger = target->GetDebugger(); - if (debugger.GetUseColor()) { - s.PutCString(entry.string); - } + if (Target *target = Target::GetTargetFromContexts(exe_ctx, sc)) { + Debugger &debugger = target->GetDebugger(); + if (debugger.GetUseColor()) { + s.PutCString(entry.string); } } // Always return true, so colors being disabled is transparent. @@ -1912,7 +1919,30 @@ bool FormatEntity::Format(const Entry &entry, Stream &s, return true; } return false; + + case Entry::Type::ProgressCount: + if (Target *target = Target::GetTargetFromContexts(exe_ctx, sc)) { + Debugger &debugger = target->GetDebugger(); + if (auto progress = debugger.GetCurrentProgressReport()) { + if (progress->total != UINT64_MAX) { + s.Format("[{0}/{1}]", progress->completed, progress->total); + return true; + } + } + } + return false; + + case Entry::Type::ProgressMessage: + if (Target *target = Target::GetTargetFromContexts(exe_ctx, sc)) { + Debugger &debugger = target->GetDebugger(); + if (auto progress = debugger.GetCurrentProgressReport()) { + s.PutCString(progress->message); + return true; + } + } + return false; } + return false; } diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp new file mode 100644 index 0000000000000..a6c3e778e7179 --- /dev/null +++ b/lldb/source/Core/Statusline.cpp @@ -0,0 +1,171 @@ +//===-- Statusline.cpp ---------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/Core/Statusline.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/FormatEntity.h" +#include "lldb/Host/StreamFile.h" +#include "lldb/Host/ThreadLauncher.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Symbol/SymbolContext.h" +#include "lldb/Target/StackFrame.h" +#include "lldb/Utility/AnsiTerminal.h" +#include "lldb/Utility/LLDBLog.h" +#include "lldb/Utility/Log.h" +#include "lldb/Utility/StreamString.h" +#include "llvm/Support/Locale.h" + +#define ESCAPE "\x1b" +#define ANSI_NORMAL ESCAPE "[0m" +#define ANSI_SAVE_CURSOR ESCAPE "7" +#define ANSI_RESTORE_CURSOR ESCAPE "8" +#define ANSI_CLEAR_BELOW ESCAPE "[J" +#define ANSI_CLEAR_LINE "\r\x1B[2K" +#define ANSI_SET_SCROLL_ROWS ESCAPE "[0;%ur" +#define ANSI_TO_START_OF_ROW ESCAPE "[%u;0f" +#define ANSI_UP_ROWS ESCAPE "[%dA" +#define ANSI_DOWN_ROWS ESCAPE "[%dB" +#define ANSI_FORWARD_COLS ESCAPE "\033[%dC" +#define ANSI_BACKWARD_COLS ESCAPE "\033[%dD" + +using namespace lldb; +using namespace lldb_private; + +static size_t ColumnWidth(llvm::StringRef str) { + std::string stripped = ansi::StripAnsiTerminalCodes(str); + return llvm::sys::locale::columnWidth(stripped); +} + +Statusline::Statusline(Debugger &debugger) : m_debugger(debugger) { Enable(); } + +Statusline::~Statusline() { Disable(); } + +void Statusline::TerminalSizeChanged() { + m_terminal_size_has_changed = 1; + + // This definitely isn't signal safe, but the best we can do, until we + // have proper signal-catching thread. + Redraw(/*update=*/false); +} + +void Statusline::Enable() { + UpdateTerminalProperties(); + + // Reduce the scroll window to make space for the status bar below. + SetScrollWindow(m_terminal_height - 1); + + // Draw the statusline. + Redraw(); +} + +void Statusline::Disable() { + UpdateTerminalProperties(); + + // Extend the scroll window to cover the status bar. + SetScrollWindow(m_terminal_height); +} + +void Statusline::Draw(std::string str) { + static constexpr const size_t g_ellipsis = 3; + + UpdateTerminalProperties(); + + m_last_str = str; + + size_t column_width = ColumnWidth(str); + + if (column_width + g_ellipsis >= m_terminal_width) { + // FIXME: If there are hidden characters (e.g. UTF-8, ANSI escape + // characters), this will strip the string more than necessary. Ideally we + // want to strip until column_width == m_terminal_width. + str = str.substr(0, m_terminal_width); + str.replace(m_terminal_width - g_ellipsis, g_ellipsis, "..."); + column_width = ColumnWidth(str); + } + + if (lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP()) { + LockedStreamFile locked_stream = stream_sp->Lock(); + locked_stream << ANSI_SAVE_CURSOR; + locked_stream.Printf(ANSI_TO_START_OF_ROW, + static_cast<unsigned>(m_terminal_height)); + locked_stream << ANSI_CLEAR_LINE; + locked_stream << str; + if (column_width < m_terminal_width) + locked_stream << std::string(m_terminal_width - column_width, ' '); + locked_stream << ANSI_NORMAL; + locked_stream << ANSI_RESTORE_CURSOR; + } +} + +void Statusline::Reset() { + if (lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP()) { + LockedStreamFile locked_stream = stream_sp->Lock(); + locked_stream << ANSI_SAVE_CURSOR; + locked_stream.Printf(ANSI_TO_START_OF_ROW, + static_cast<unsigned>(m_terminal_height)); + locked_stream << ANSI_CLEAR_LINE; + locked_stream << ANSI_RESTORE_CURSOR; + } +} + +void Statusline::UpdateTerminalProperties() { + if (m_terminal_size_has_changed == 0) + return; + + // Clear the previous statusline using the previous dimensions. + Reset(); + + m_terminal_width = m_debugger.GetTerminalWidth(); + m_terminal_height = m_debugger.GetTerminalHeight(); + + // Set the scroll window based on the new terminal height. + SetScrollWindow(m_terminal_height - 1); + + // Clear the flag. + m_terminal_size_has_changed = 0; +} + +void Statusline::SetScrollWindow(uint64_t height) { + if (lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP()) { + LockedStreamFile locked_stream = stream_sp->Lock(); + locked_stream << '\n'; + locked_stream << ANSI_SAVE_CURSOR; + locked_stream.Printf(ANSI_SET_SCROLL_ROWS, static_cast<unsigned>(height)); + locked_stream << ANSI_RESTORE_CURSOR; + locked_stream.Printf(ANSI_UP_ROWS, 1); + locked_stream << ANSI_CLEAR_BELOW; + } + + m_scroll_height = height; +} + +void Statusline::Redraw(bool update) { + if (!update) { + Draw(m_last_str); + return; + } + + StreamString stream; + ExecutionContext exe_ctx = + m_debugger.GetCommandInterpreter().GetExecutionContext(); + + // For colors and progress events, the format entity needs access to the + // debugger, which requires a target in the execution context. + if (!exe_ctx.HasTargetScope()) + exe_ctx.SetTargetPtr(&m_debugger.GetSelectedOrDummyTarget()); + + SymbolContext symbol_ctx; + if (auto frame_sp = exe_ctx.GetFrameSP()) + symbol_ctx = frame_sp->GetSymbolContext(eSymbolContextEverything); + + if (auto *format = m_debugger.GetStatuslineFormat()) + FormatEntity::Format(*format, stream, &symbol_ctx, &exe_ctx, nullptr, + nullptr, false, false); + + Draw(std::string(stream.GetString())); +} diff --git a/lldb/test/API/functionalities/progress_reporting/TestTrimmedProgressReporting.py b/lldb/test/API/functionalities/progress_reporting/TestTrimmedProgressReporting.py deleted file mode 100644 index 3cf7b9d210089..0000000000000 --- a/lldb/test/API/functionalities/progress_reporting/TestTrimmedProgressReporting.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Test trimming long progress report in tiny terminal windows -""" - -import os -import tempfile -import re - -import lldb -from lldbsuite.test.decorators import * -from lldbsuite.test.lldbtest import * -from lldbsuite.test.lldbpexpect import PExpectTest - - -class TestTrimmedProgressReporting(PExpectTest): - def do_test(self, term_width, pattern_list): - self.build() - # Start with a small window - self.launch(use_colors=True) - self.expect("set set show-progress true") - self.expect( - "set show show-progress", substrs=["show-progress (boolean) = true"] - ) - self.expect("set set term-width " + str(term_width)) - self.expect( - "set show term-width", - substrs=["term-width (unsigned) = " + str(term_width)], - ) - - self.child.send("file " + self.getBuildArtifact("a.out") + "\n") - self.child.expect(pattern_list) - - # PExpect uses many timeouts internally and doesn't play well - # under ASAN on a loaded machine.. - @skipIfAsan - @skipIfEditlineSupportMissing - def test_trimmed_progress_message(self): - self.do_test(19, ["Locating e...", "Parsing sy..."]) - - # PExpect uses many timeouts internally and doesn't play well - # under ASAN on a loaded machine.. - @skipIfAsan - @skipIfEditlineSupportMissing - def test_long_progress_message(self): - self.do_test( - 80, - [ - "Locating external symbol file", - "Parsing symbol table", - ], - ) diff --git a/lldb/test/API/functionalities/statusline/Makefile b/lldb/test/API/functionalities/statusline/Makefile new file mode 100644 index 0000000000000..10495940055b6 --- /dev/null +++ b/lldb/test/API/functionalities/statusline/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py new file mode 100644 index 0000000000000..ab2a6826ba5d3 --- /dev/null +++ b/lldb/test/API/functionalities/statusline/TestStatusline.py @@ -0,0 +1,57 @@ +import lldb +import re + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test.lldbpexpect import PExpectTest + + +class TestStatusline(PExpectTest): + def do_setup(self): + # Create a target and run to a breakpoint. + exe = self.getBuildArtifact("a.out") + self.expect( + "target create {}".format(exe), substrs=["Current executable set to"] + ) + self.expect('breakpoint set -p "Break here"', substrs=["Breakpoint 1"]) + self.expect("run", substrs=["stop reason"]) + + # PExpect uses many timeouts internally and doesn't play well + # under ASAN on a loaded machine.. + @skipIfAsan + def test(self): + """Basic test for the statusline.""" + self.build() + self.launch() + self.do_setup() + + # Change the terminal dimensions. + terminal_height = 10 + terminal_width = 60 + self.child.setwinsize(terminal_height, terminal_width) + + # Enable the statusline and check for the control character and that we + # can see the target, the location and the stop reason. + self.expect( + "set set show-statusline true", + [ + "\x1b[0;{}r".format(terminal_height - 1), + "a.out | main.c:4:15 | breakpoint 1.1 ", + ], + ) + + # Change the terminal dimensions and make sure it's reflected immediately. + self.child.setwinsize(terminal_height, 20) + self.child.expect(re.escape("a.out | main.c:4:...")) + self.child.setwinsize(terminal_height, terminal_width) + + # Change the format. + self.expect( + 'set set statusline-format "target = {${target.file.basename}}"', + ["target = a.out"], + ) + + # Hide the statusline and check or the control character. + self.expect( + "set set show-statusline false", ["\x1b[0;{}r".format(terminal_height)] + ) diff --git a/lldb/test/API/functionalities/statusline/main.c b/lldb/test/API/functionalities/statusline/main.c new file mode 100644 index 0000000000000..762cd38be8a2a --- /dev/null +++ b/lldb/test/API/functionalities/statusline/main.c @@ -0,0 +1,11 @@ +int bar(int b) { return b * b; } + +int foo(int f) { + int b = bar(f); // Break here + return b; +} + +int main() { + int f = foo(42); + return f; +} >From 56784922d635ff66d8bb723b7d55a21d8db56534 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Mon, 10 Mar 2025 11:52:14 -0700 Subject: [PATCH 2/5] Redraw the statusline when editline has taken control of the screen --- lldb/include/lldb/Core/Debugger.h | 3 +++ lldb/include/lldb/Core/IOHandler.h | 2 ++ lldb/include/lldb/Host/Editline.h | 8 ++++++++ lldb/source/Core/Debugger.cpp | 15 ++++++++------- lldb/source/Core/IOHandler.cpp | 6 ++++++ lldb/source/Host/common/Editline.cpp | 3 +++ 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 9e2100662c6de..c79a75ab61564 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -417,6 +417,9 @@ class Debugger : public std::enable_shared_from_this<Debugger>, /// Decrement the "interrupt requested" counter. void CancelInterruptRequest(); + /// Redraw the statusline if enabled. + void RedrawStatusline(bool update = true); + /// This is the correct way to query the state of Interruption. /// If you are on the RunCommandInterpreter thread, it will check the /// command interpreter state, and if it is on another thread it will diff --git a/lldb/include/lldb/Core/IOHandler.h b/lldb/include/lldb/Core/IOHandler.h index fc0c676883b4a..794d229bc1337 100644 --- a/lldb/include/lldb/Core/IOHandler.h +++ b/lldb/include/lldb/Core/IOHandler.h @@ -406,6 +406,8 @@ class IOHandlerEditline : public IOHandler { std::optional<std::string> SuggestionCallback(llvm::StringRef line); void AutoCompleteCallback(CompletionRequest &request); + + void RedrawCallback(); #endif protected: diff --git a/lldb/include/lldb/Host/Editline.h b/lldb/include/lldb/Host/Editline.h index 8964d37be8823..705ec9c49f7c7 100644 --- a/lldb/include/lldb/Host/Editline.h +++ b/lldb/include/lldb/Host/Editline.h @@ -102,6 +102,8 @@ using SuggestionCallbackType = using CompleteCallbackType = llvm::unique_function<void(CompletionRequest &)>; +using RedrawCallbackType = llvm::unique_function<void()>; + /// Status used to decide when and how to start editing another line in /// multi-line sessions. enum class EditorStatus { @@ -194,6 +196,11 @@ class Editline { m_suggestion_callback = std::move(callback); } + /// Register a callback for redrawing the statusline. + void SetRedrawCallback(RedrawCallbackType callback) { + m_redraw_callback = std::move(callback); + } + /// Register a callback for the tab key void SetAutoCompleteCallback(CompleteCallbackType callback) { m_completion_callback = std::move(callback); @@ -409,6 +416,7 @@ class Editline { CompleteCallbackType m_completion_callback; SuggestionCallbackType m_suggestion_callback; + RedrawCallbackType m_redraw_callback; bool m_color; std::string m_prompt_ansi_prefix; diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index a8d8343b030f7..f3e98b3ab2d39 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -258,8 +258,7 @@ Status Debugger::SetPropertyValue(const ExecutionContext *exe_ctx, } else if (property_path == g_debugger_properties[ePropertyStatuslineFormat].name) { // Statusline format changed. Redraw the statusline. - if (m_statusline) - m_statusline->Redraw(); + RedrawStatusline(); } else if (property_path == g_debugger_properties[ePropertyUseSourceCache].name) { // use-source-cache changed. Wipe out the cache contents if it was @@ -1155,6 +1154,11 @@ void Debugger::RestoreInputTerminalState() { m_statusline->Enable(); } +void Debugger::RedrawStatusline(bool update) { + if (m_statusline) + m_statusline->Redraw(update); +} + ExecutionContext Debugger::GetSelectedExecutionContext() { bool adopt_selected = true; ExecutionContextRef exe_ctx_ref(GetSelectedTarget().get(), adopt_selected); @@ -2084,8 +2088,7 @@ lldb::thread_result_t Debugger::DefaultEventHandler() { if (m_forward_listener_sp) m_forward_listener_sp->AddEvent(event_sp); } - if (m_statusline) - m_statusline->Redraw(); + RedrawStatusline(); } } @@ -2176,9 +2179,7 @@ void Debugger::HandleProgressEvent(const lldb::EventSP &event_sp) { } } - // Redraw the statusline if enabled. - if (m_statusline) - m_statusline->Redraw(); + RedrawStatusline(); } std::optional<Debugger::ProgressReport> diff --git a/lldb/source/Core/IOHandler.cpp b/lldb/source/Core/IOHandler.cpp index 98d14758f1987..d336cb0592d5b 100644 --- a/lldb/source/Core/IOHandler.cpp +++ b/lldb/source/Core/IOHandler.cpp @@ -258,6 +258,7 @@ IOHandlerEditline::IOHandlerEditline( m_editline_up->SetAutoCompleteCallback([this](CompletionRequest &request) { this->AutoCompleteCallback(request); }); + m_editline_up->SetRedrawCallback([this]() { this->RedrawCallback(); }); if (debugger.GetUseAutosuggestion()) { m_editline_up->SetSuggestionCallback([this](llvm::StringRef line) { @@ -439,6 +440,11 @@ IOHandlerEditline::SuggestionCallback(llvm::StringRef line) { void IOHandlerEditline::AutoCompleteCallback(CompletionRequest &request) { m_delegate.IOHandlerComplete(*this, request); } + +void IOHandlerEditline::RedrawCallback() { + m_debugger.RedrawStatusline(/*update=*/false); +} + #endif const char *IOHandlerEditline::GetPrompt() { diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index 5f7a8b0190a1d..29abaf7c65f28 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -567,6 +567,9 @@ int Editline::GetCharacter(EditLineGetCharType *c) { m_needs_prompt_repaint = false; } + if (m_redraw_callback) + m_redraw_callback(); + if (m_multiline_enabled) { // Detect when the number of rows used for this input line changes due to // an edit >From 0b22fb34b894c8baa185a1f44731b451d095b73e Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Mon, 10 Mar 2025 13:17:00 -0700 Subject: [PATCH 3/5] Address David's feedback --- .../Python/lldbsuite/test/lldbtest.py | 5 +- lldb/source/Core/CoreProperties.td | 2 +- lldb/source/Core/FormatEntity.cpp | 6 +-- lldb/source/Core/Statusline.cpp | 50 ++++++++++--------- .../statusline/TestStatusline.py | 4 +- .../API/functionalities/statusline/main.c | 4 +- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py index dc265d510964d..0d60025556049 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -765,14 +765,15 @@ def setUpCommands(cls): # Disable fix-its by default so that incorrect expressions in tests don't # pass just because Clang thinks it has a fix-it. "settings set target.auto-apply-fixits false", - # Disable the statusline in PExpect tests. - "settings set show-statusline false", # Testsuite runs in parallel and the host can have also other load. "settings set plugin.process.gdb-remote.packet-timeout 60", 'settings set symbols.clang-modules-cache-path "{}"'.format( configuration.lldb_module_cache_dir ), + # Disable colors by default. "settings set use-color false", + # Disable the statusline by default. + "settings set show-statusline false", ] # Set any user-overridden settings. diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td index 0c6f93cb23e45..01a04f9e79095 100644 --- a/lldb/source/Core/CoreProperties.td +++ b/lldb/source/Core/CoreProperties.td @@ -179,7 +179,7 @@ let Definition = "debugger" in { def StatuslineFormat: Property<"statusline-format", "FormatEntity">, Global, DefaultStringValue<"${ansi.bg.blue}${ansi.fg.black}{${target.file.basename}}{ | ${line.file.basename}:${line.number}:${line.column}}{ | ${thread.stop-reason}}{ | {${progress.count} }${progress.message}}">, - Desc<"List of statusline format entities.">; + Desc<"The default statusline format string.">; def UseSourceCache: Property<"use-source-cache", "Boolean">, Global, DefaultTrue, diff --git a/lldb/source/Core/FormatEntity.cpp b/lldb/source/Core/FormatEntity.cpp index 8a32195769357..04dea7efde54d 100644 --- a/lldb/source/Core/FormatEntity.cpp +++ b/lldb/source/Core/FormatEntity.cpp @@ -1922,8 +1922,7 @@ bool FormatEntity::Format(const Entry &entry, Stream &s, case Entry::Type::ProgressCount: if (Target *target = Target::GetTargetFromContexts(exe_ctx, sc)) { - Debugger &debugger = target->GetDebugger(); - if (auto progress = debugger.GetCurrentProgressReport()) { + if (auto progress = target->GetDebugger().GetCurrentProgressReport()) { if (progress->total != UINT64_MAX) { s.Format("[{0}/{1}]", progress->completed, progress->total); return true; @@ -1934,8 +1933,7 @@ bool FormatEntity::Format(const Entry &entry, Stream &s, case Entry::Type::ProgressMessage: if (Target *target = Target::GetTargetFromContexts(exe_ctx, sc)) { - Debugger &debugger = target->GetDebugger(); - if (auto progress = debugger.GetCurrentProgressReport()) { + if (auto progress = target->GetDebugger().GetCurrentProgressReport()) { s.PutCString(progress->message); return true; } diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp index a6c3e778e7179..21e4780fbfc24 100644 --- a/lldb/source/Core/Statusline.cpp +++ b/lldb/source/Core/Statusline.cpp @@ -71,7 +71,11 @@ void Statusline::Disable() { } void Statusline::Draw(std::string str) { - static constexpr const size_t g_ellipsis = 3; + lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP(); + if (!stream_sp) + return; + + static constexpr const size_t g_ellipsis_len = 3; UpdateTerminalProperties(); @@ -79,38 +83,38 @@ void Statusline::Draw(std::string str) { size_t column_width = ColumnWidth(str); - if (column_width + g_ellipsis >= m_terminal_width) { + if (column_width + g_ellipsis_len >= m_terminal_width) { // FIXME: If there are hidden characters (e.g. UTF-8, ANSI escape // characters), this will strip the string more than necessary. Ideally we // want to strip until column_width == m_terminal_width. str = str.substr(0, m_terminal_width); - str.replace(m_terminal_width - g_ellipsis, g_ellipsis, "..."); + str.replace(m_terminal_width - g_ellipsis_len, g_ellipsis_len, "..."); column_width = ColumnWidth(str); } - if (lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP()) { - LockedStreamFile locked_stream = stream_sp->Lock(); - locked_stream << ANSI_SAVE_CURSOR; - locked_stream.Printf(ANSI_TO_START_OF_ROW, - static_cast<unsigned>(m_terminal_height)); - locked_stream << ANSI_CLEAR_LINE; - locked_stream << str; - if (column_width < m_terminal_width) - locked_stream << std::string(m_terminal_width - column_width, ' '); - locked_stream << ANSI_NORMAL; - locked_stream << ANSI_RESTORE_CURSOR; - } + LockedStreamFile locked_stream = stream_sp->Lock(); + locked_stream << ANSI_SAVE_CURSOR; + locked_stream.Printf(ANSI_TO_START_OF_ROW, + static_cast<unsigned>(m_terminal_height)); + locked_stream << ANSI_CLEAR_LINE; + locked_stream << str; + if (column_width < m_terminal_width) + locked_stream << std::string(m_terminal_width - column_width, ' '); + locked_stream << ANSI_NORMAL; + locked_stream << ANSI_RESTORE_CURSOR; } void Statusline::Reset() { - if (lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP()) { - LockedStreamFile locked_stream = stream_sp->Lock(); - locked_stream << ANSI_SAVE_CURSOR; - locked_stream.Printf(ANSI_TO_START_OF_ROW, - static_cast<unsigned>(m_terminal_height)); - locked_stream << ANSI_CLEAR_LINE; - locked_stream << ANSI_RESTORE_CURSOR; - } + lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP(); + if (!stream_sp) + return; + + LockedStreamFile locked_stream = stream_sp->Lock(); + locked_stream << ANSI_SAVE_CURSOR; + locked_stream.Printf(ANSI_TO_START_OF_ROW, + static_cast<unsigned>(m_terminal_height)); + locked_stream << ANSI_CLEAR_LINE; + locked_stream << ANSI_RESTORE_CURSOR; } void Statusline::UpdateTerminalProperties() { diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py index ab2a6826ba5d3..5d37d53d80b87 100644 --- a/lldb/test/API/functionalities/statusline/TestStatusline.py +++ b/lldb/test/API/functionalities/statusline/TestStatusline.py @@ -36,13 +36,13 @@ def test(self): "set set show-statusline true", [ "\x1b[0;{}r".format(terminal_height - 1), - "a.out | main.c:4:15 | breakpoint 1.1 ", + "a.out | main.c:2:11 | breakpoint 1.1 ", ], ) # Change the terminal dimensions and make sure it's reflected immediately. self.child.setwinsize(terminal_height, 20) - self.child.expect(re.escape("a.out | main.c:4:...")) + self.child.expect(re.escape("a.out | main.c:2:...")) self.child.setwinsize(terminal_height, terminal_width) # Change the format. diff --git a/lldb/test/API/functionalities/statusline/main.c b/lldb/test/API/functionalities/statusline/main.c index 762cd38be8a2a..7182181ba2492 100644 --- a/lldb/test/API/functionalities/statusline/main.c +++ b/lldb/test/API/functionalities/statusline/main.c @@ -1,7 +1,5 @@ -int bar(int b) { return b * b; } - int foo(int f) { - int b = bar(f); // Break here + int b = f * f; // Break here return b; } >From 74417cc0daac2a14583f68894959c8caa48e009c Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Mon, 10 Mar 2025 15:05:07 -0700 Subject: [PATCH 4/5] [lldb] Rewrite stripping and padding and add a unit test --- lldb/include/lldb/Core/Statusline.h | 5 ++ lldb/source/Core/Statusline.cpp | 53 ++++++++++++++----- .../statusline/TestStatusline.py | 4 +- lldb/unittests/Core/CMakeLists.txt | 1 + lldb/unittests/Core/StatuslineTest.cpp | 42 +++++++++++++++ 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 lldb/unittests/Core/StatuslineTest.cpp diff --git a/lldb/include/lldb/Core/Statusline.h b/lldb/include/lldb/Core/Statusline.h index 21bd58d933b9e..1f61173c6bff8 100644 --- a/lldb/include/lldb/Core/Statusline.h +++ b/lldb/include/lldb/Core/Statusline.h @@ -12,6 +12,7 @@ #include "lldb/lldb-forward.h" #include "llvm/ADT/StringRef.h" #include <csignal> +#include <cstdint> #include <string> namespace lldb_private { @@ -33,6 +34,10 @@ class Statusline { /// Inform the statusline that the terminal dimensions have changed. void TerminalSizeChanged(); +protected: + /// Pad and trim the given string to fit to the given width. + static std::string TrimAndPad(std::string str, size_t width); + private: /// Draw the statusline with the given text. void Draw(std::string msg); diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp index 21e4780fbfc24..f19a387958b4b 100644 --- a/lldb/source/Core/Statusline.cpp +++ b/lldb/source/Core/Statusline.cpp @@ -18,7 +18,10 @@ #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Log.h" #include "lldb/Utility/StreamString.h" +#include "llvm/ADT/StringRef.h" #include "llvm/Support/Locale.h" +#include <algorithm> +#include <cstdint> #define ESCAPE "\x1b" #define ANSI_NORMAL ESCAPE "[0m" @@ -70,27 +73,51 @@ void Statusline::Disable() { SetScrollWindow(m_terminal_height); } +std::string Statusline::TrimAndPad(std::string str, size_t max_width) { + size_t column_width = ColumnWidth(str); + + // Trim the string. + if (column_width > max_width) { + size_t min_width_idx = max_width; + size_t min_width = column_width; + + // Use a StringRef for more efficient slicing in the loop below. + llvm::StringRef str_ref = str; + + // Keep extending the string to find the minimum column width to make sure + // we include as many ANSI escape characters or Unicode code units as + // possible. This is far from the most efficient way to do this, but it's + // means our stripping code doesn't need to be ANSI and Unicode aware and + // should be relatively cold code path. + for (size_t i = column_width; i < str.length(); ++i) { + size_t stripped_width = ColumnWidth(str_ref.take_front(i)); + if (stripped_width <= column_width) { + min_width = stripped_width; + min_width_idx = i; + } + } + + str = str.substr(0, min_width_idx); + column_width = min_width; + } + + // Pad the string. + if (column_width < max_width) + str.append(max_width - column_width, ' '); + + return str; +} + void Statusline::Draw(std::string str) { lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP(); if (!stream_sp) return; - static constexpr const size_t g_ellipsis_len = 3; - UpdateTerminalProperties(); m_last_str = str; - size_t column_width = ColumnWidth(str); - - if (column_width + g_ellipsis_len >= m_terminal_width) { - // FIXME: If there are hidden characters (e.g. UTF-8, ANSI escape - // characters), this will strip the string more than necessary. Ideally we - // want to strip until column_width == m_terminal_width. - str = str.substr(0, m_terminal_width); - str.replace(m_terminal_width - g_ellipsis_len, g_ellipsis_len, "..."); - column_width = ColumnWidth(str); - } + str = TrimAndPad(str, m_terminal_width); LockedStreamFile locked_stream = stream_sp->Lock(); locked_stream << ANSI_SAVE_CURSOR; @@ -98,8 +125,6 @@ void Statusline::Draw(std::string str) { static_cast<unsigned>(m_terminal_height)); locked_stream << ANSI_CLEAR_LINE; locked_stream << str; - if (column_width < m_terminal_width) - locked_stream << std::string(m_terminal_width - column_width, ' '); locked_stream << ANSI_NORMAL; locked_stream << ANSI_RESTORE_CURSOR; } diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py index 5d37d53d80b87..a58dc5470ed6d 100644 --- a/lldb/test/API/functionalities/statusline/TestStatusline.py +++ b/lldb/test/API/functionalities/statusline/TestStatusline.py @@ -41,8 +41,8 @@ def test(self): ) # Change the terminal dimensions and make sure it's reflected immediately. - self.child.setwinsize(terminal_height, 20) - self.child.expect(re.escape("a.out | main.c:2:...")) + self.child.setwinsize(terminal_height, 25) + self.child.expect(re.escape("a.out | main.c:2:11 | bre")) self.child.setwinsize(terminal_height, terminal_width) # Change the format. diff --git a/lldb/unittests/Core/CMakeLists.txt b/lldb/unittests/Core/CMakeLists.txt index 60265f794b5e8..579d8304e1b32 100644 --- a/lldb/unittests/Core/CMakeLists.txt +++ b/lldb/unittests/Core/CMakeLists.txt @@ -11,6 +11,7 @@ add_lldb_unittest(LLDBCoreTests RichManglingContextTest.cpp SourceLocationSpecTest.cpp SourceManagerTest.cpp + StatuslineTest.cpp TelemetryTest.cpp UniqueCStringMapTest.cpp diff --git a/lldb/unittests/Core/StatuslineTest.cpp b/lldb/unittests/Core/StatuslineTest.cpp new file mode 100644 index 0000000000000..38f612c882f25 --- /dev/null +++ b/lldb/unittests/Core/StatuslineTest.cpp @@ -0,0 +1,42 @@ +//===-- StatuslineTest.cpp ------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/Core/Statusline.h" +#include "gtest/gtest.h" + +using namespace lldb_private; + +class TestStatusline : public Statusline { +public: + using Statusline::TrimAndPad; +}; + +TEST(StatuslineTest, TestTrimAndPad) { + // Test basic ASCII. + EXPECT_EQ(" ", TestStatusline::TrimAndPad("", 5)); + EXPECT_EQ("foo ", TestStatusline::TrimAndPad("foo", 5)); + EXPECT_EQ("fooba", TestStatusline::TrimAndPad("fooba", 5)); + EXPECT_EQ("fooba", TestStatusline::TrimAndPad("foobar", 5)); + + // Simple test that ANSI escape codes don't contribute to the visible width. + EXPECT_EQ("\x1B[30m ", TestStatusline::TrimAndPad("\x1B[30m", 5)); + EXPECT_EQ("\x1B[30mfoo ", TestStatusline::TrimAndPad("\x1B[30mfoo", 5)); + EXPECT_EQ("\x1B[30mfooba", TestStatusline::TrimAndPad("\x1B[30mfooba", 5)); + EXPECT_EQ("\x1B[30mfooba", TestStatusline::TrimAndPad("\x1B[30mfoobar", 5)); + + // Test that we include as many escape codes as we can. + EXPECT_EQ("fooba\x1B[30m", TestStatusline::TrimAndPad("fooba\x1B[30m", 5)); + EXPECT_EQ("fooba\x1B[30m\x1B[34m", + TestStatusline::TrimAndPad("fooba\x1B[30m\x1B[34m", 5)); + EXPECT_EQ("fooba\x1B[30m\x1B[34m", + TestStatusline::TrimAndPad("fooba\x1B[30m\x1B[34mr", 5)); + + // Test Unicode. + EXPECT_EQ("❤️ ", TestStatusline::TrimAndPad("❤️", 5)); + EXPECT_EQ(" ❤️", TestStatusline::TrimAndPad(" ❤️", 5)); +} >From 9e3648018eb4e9e86848fe725846c1be1834abbe Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Mon, 10 Mar 2025 16:53:20 -0700 Subject: [PATCH 5/5] Fix spurious newline bug --- lldb/include/lldb/Core/Statusline.h | 10 +++++--- lldb/source/Core/Statusline.cpp | 37 +++++++++++++++++++---------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/lldb/include/lldb/Core/Statusline.h b/lldb/include/lldb/Core/Statusline.h index 1f61173c6bff8..c1449f0f69081 100644 --- a/lldb/include/lldb/Core/Statusline.h +++ b/lldb/include/lldb/Core/Statusline.h @@ -45,8 +45,13 @@ class Statusline { /// Update terminal dimensions. void UpdateTerminalProperties(); - /// Set the scroll window to the given height. - void SetScrollWindow(uint64_t height); + enum ScrollWindowMode { + ScrollWindowExtend, + ScrollWindowShrink, + }; + + /// Set the scroll window for the given mode. + void UpdateScrollWindow(ScrollWindowMode mode); /// Clear the statusline (without redrawing the background). void Reset(); @@ -57,7 +62,6 @@ class Statusline { volatile std::sig_atomic_t m_terminal_size_has_changed = 1; uint64_t m_terminal_width = 0; uint64_t m_terminal_height = 0; - uint64_t m_scroll_height = 0; }; } // namespace lldb_private #endif // LLDB_CORE_STATUSLINE_H diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp index f19a387958b4b..f535c02b1dfe7 100644 --- a/lldb/source/Core/Statusline.cpp +++ b/lldb/source/Core/Statusline.cpp @@ -60,7 +60,7 @@ void Statusline::Enable() { UpdateTerminalProperties(); // Reduce the scroll window to make space for the status bar below. - SetScrollWindow(m_terminal_height - 1); + UpdateScrollWindow(ScrollWindowShrink); // Draw the statusline. Redraw(); @@ -70,7 +70,7 @@ void Statusline::Disable() { UpdateTerminalProperties(); // Extend the scroll window to cover the status bar. - SetScrollWindow(m_terminal_height); + UpdateScrollWindow(ScrollWindowExtend); } std::string Statusline::TrimAndPad(std::string str, size_t max_width) { @@ -153,24 +153,35 @@ void Statusline::UpdateTerminalProperties() { m_terminal_height = m_debugger.GetTerminalHeight(); // Set the scroll window based on the new terminal height. - SetScrollWindow(m_terminal_height - 1); + UpdateScrollWindow(ScrollWindowShrink); // Clear the flag. m_terminal_size_has_changed = 0; } -void Statusline::SetScrollWindow(uint64_t height) { - if (lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP()) { - LockedStreamFile locked_stream = stream_sp->Lock(); - locked_stream << '\n'; - locked_stream << ANSI_SAVE_CURSOR; - locked_stream.Printf(ANSI_SET_SCROLL_ROWS, static_cast<unsigned>(height)); - locked_stream << ANSI_RESTORE_CURSOR; - locked_stream.Printf(ANSI_UP_ROWS, 1); +void Statusline::UpdateScrollWindow(ScrollWindowMode mode) { + lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP(); + if (!stream_sp) + return; + + const unsigned scroll_height = + (mode == ScrollWindowExtend) ? m_terminal_height : m_terminal_height - 1; + + LockedStreamFile locked_stream = stream_sp->Lock(); + locked_stream << ANSI_SAVE_CURSOR; + locked_stream.Printf(ANSI_SET_SCROLL_ROWS, scroll_height); + locked_stream << ANSI_RESTORE_CURSOR; + switch (mode) { + case ScrollWindowExtend: + // Clear the screen below to hide the old statusline. locked_stream << ANSI_CLEAR_BELOW; + break; + case ScrollWindowShrink: + // Move everything on the screen up. + locked_stream.Printf(ANSI_UP_ROWS, 1); + locked_stream << '\n'; + break; } - - m_scroll_height = height; } void Statusline::Redraw(bool update) { _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits