https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/121860
This is a draft PR with my prototype implementation of https://discourse.llvm.org/t/rfc-lldb-statusline/83948 >From 24fea391893de79fc0455a81c1ce6cd77b6106fc Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Fri, 3 Jan 2025 18:13:16 -0800 Subject: [PATCH 1/2] Add OptionValueFormatEntityList --- lldb/include/lldb/Interpreter/OptionValue.h | 13 +- .../Interpreter/OptionValueFormatEntityList.h | 62 +++++ lldb/include/lldb/Interpreter/OptionValues.h | 1 + lldb/include/lldb/lldb-forward.h | 1 + lldb/source/Interpreter/CMakeLists.txt | 1 + lldb/source/Interpreter/OptionValue.cpp | 24 ++ .../OptionValueFormatEntityList.cpp | 223 ++++++++++++++++++ lldb/source/Interpreter/Property.cpp | 9 + 8 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 lldb/include/lldb/Interpreter/OptionValueFormatEntityList.h create mode 100644 lldb/source/Interpreter/OptionValueFormatEntityList.cpp diff --git a/lldb/include/lldb/Interpreter/OptionValue.h b/lldb/include/lldb/Interpreter/OptionValue.h index d19c8b8fab6222..73944c133d3b4e 100644 --- a/lldb/include/lldb/Interpreter/OptionValue.h +++ b/lldb/include/lldb/Interpreter/OptionValue.h @@ -51,7 +51,8 @@ class OptionValue { eTypeString, eTypeUInt64, eTypeUUID, - eTypeFormatEntity + eTypeFormatEntity, + eTypeFormatEntityList, }; enum { @@ -72,7 +73,7 @@ class OptionValue { virtual ~OptionValue() = default; OptionValue(const OptionValue &other); - + OptionValue& operator=(const OptionValue &other); // Subclasses should override these functions @@ -249,6 +250,9 @@ class OptionValue { OptionValueFormatEntity *GetAsFormatEntity(); const OptionValueFormatEntity *GetAsFormatEntity() const; + OptionValueFormatEntityList *GetAsFormatEntityList(); + const OptionValueFormatEntityList *GetAsFormatEntityList() const; + bool AppendFileSpecValue(FileSpec file_spec); bool OptionWasSet() const { return m_value_was_set; } @@ -286,6 +290,8 @@ class OptionValue { return GetFileSpecValue(); if constexpr (std::is_same_v<T, FileSpecList>) return GetFileSpecListValue(); + if constexpr (std::is_same_v<T, std::vector<FormatEntity::Entry>>) + return GetFormatEntityList(); if constexpr (std::is_same_v<T, lldb::LanguageType>) return GetLanguageValue(); if constexpr (std::is_same_v<T, llvm::StringRef>) @@ -387,8 +393,9 @@ class OptionValue { bool SetUUIDValue(const UUID &uuid); const FormatEntity::Entry *GetFormatEntity() const; + std::vector<FormatEntity::Entry> GetFormatEntityList() const; const RegularExpression *GetRegexValue() const; - + mutable std::mutex m_mutex; }; diff --git a/lldb/include/lldb/Interpreter/OptionValueFormatEntityList.h b/lldb/include/lldb/Interpreter/OptionValueFormatEntityList.h new file mode 100644 index 00000000000000..4270bf26b54c9f --- /dev/null +++ b/lldb/include/lldb/Interpreter/OptionValueFormatEntityList.h @@ -0,0 +1,62 @@ +//===-- OptionValueFormatEntityList.h --------------------------------*- +// C++-*-===// +// +// 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_INTERPRETER_OPTIONVALUEFORMATENTITYLIST_H +#define LLDB_INTERPRETER_OPTIONVALUEFORMATENTITYLIST_H + +#include "lldb/Core/FormatEntity.h" +#include "lldb/Interpreter/OptionValue.h" + +namespace lldb_private { + +class OptionValueFormatEntityList + : public Cloneable<OptionValueFormatEntityList, OptionValue> { +public: + OptionValueFormatEntityList(); + + OptionValueFormatEntityList(const OptionValueFormatEntityList &other) + : Cloneable(other), m_current_entries(other.m_current_entries), + m_current_formats(other.m_current_formats) {} + + ~OptionValueFormatEntityList() override = default; + + // Virtual subclass pure virtual overrides + + OptionValue::Type GetType() const override { return eTypeFormatEntityList; } + + void DumpValue(const ExecutionContext *exe_ctx, Stream &strm, + uint32_t dump_mask) override; + + Status + SetValueFromString(llvm::StringRef value, + VarSetOperationType op = eVarSetOperationAssign) override; + + void Clear() override; + + std::vector<FormatEntity::Entry> GetCurrentValue() const { + std::lock_guard<std::recursive_mutex> lock(m_mutex); + return m_current_entries; + } + +protected: + llvm::Error Append(llvm::StringRef str); + llvm::Error Insert(size_t idx, llvm::StringRef str); + llvm::Error Replace(size_t idx, llvm::StringRef str); + llvm::Error Remove(size_t idx); + + lldb::OptionValueSP Clone() const override; + + std::vector<FormatEntity::Entry> m_current_entries; + std::vector<std::string> m_current_formats; + mutable std::recursive_mutex m_mutex; +}; + +} // namespace lldb_private + +#endif // LLDB_INTERPRETER_OPTIONVALUEFORMATENTITYLIST_H diff --git a/lldb/include/lldb/Interpreter/OptionValues.h b/lldb/include/lldb/Interpreter/OptionValues.h index 6efc9e1ad064ce..399119e290401b 100644 --- a/lldb/include/lldb/Interpreter/OptionValues.h +++ b/lldb/include/lldb/Interpreter/OptionValues.h @@ -22,6 +22,7 @@ #include "lldb/Interpreter/OptionValueFileSpecList.h" #include "lldb/Interpreter/OptionValueFormat.h" #include "lldb/Interpreter/OptionValueFormatEntity.h" +#include "lldb/Interpreter/OptionValueFormatEntityList.h" #include "lldb/Interpreter/OptionValueLanguage.h" #include "lldb/Interpreter/OptionValuePathMappings.h" #include "lldb/Interpreter/OptionValueProperties.h" diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h index d09edeeccaff1a..5831d62f5e9338 100644 --- a/lldb/include/lldb/lldb-forward.h +++ b/lldb/include/lldb/lldb-forward.h @@ -150,6 +150,7 @@ class OptionValueFileSpec; class OptionValueFileSpecList; class OptionValueFormat; class OptionValueFormatEntity; +class OptionValueFormatEntityList; class OptionValueLanguage; class OptionValuePathMappings; class OptionValueProperties; diff --git a/lldb/source/Interpreter/CMakeLists.txt b/lldb/source/Interpreter/CMakeLists.txt index 642263a8bda7fa..4fca862de9cfcc 100644 --- a/lldb/source/Interpreter/CMakeLists.txt +++ b/lldb/source/Interpreter/CMakeLists.txt @@ -41,6 +41,7 @@ add_lldb_library(lldbInterpreter NO_PLUGIN_DEPENDENCIES OptionValueFileSpecList.cpp OptionValueFormat.cpp OptionValueFormatEntity.cpp + OptionValueFormatEntityList.cpp OptionValueLanguage.cpp OptionValuePathMappings.cpp OptionValueProperties.cpp diff --git a/lldb/source/Interpreter/OptionValue.cpp b/lldb/source/Interpreter/OptionValue.cpp index b95f4fec339499..d7c1996b24a0cc 100644 --- a/lldb/source/Interpreter/OptionValue.cpp +++ b/lldb/source/Interpreter/OptionValue.cpp @@ -184,6 +184,18 @@ const OptionValueFormatEntity *OptionValue::GetAsFormatEntity() const { return nullptr; } +OptionValueFormatEntityList *OptionValue::GetAsFormatEntityList() { + if (GetType() == OptionValue::eTypeFormatEntityList) + return static_cast<OptionValueFormatEntityList *>(this); + return nullptr; +} + +const OptionValueFormatEntityList *OptionValue::GetAsFormatEntityList() const { + if (GetType() == OptionValue::eTypeFormatEntityList) + return static_cast<const OptionValueFormatEntityList *>(this); + return nullptr; +} + OptionValuePathMappings *OptionValue::GetAsPathMappings() { if (GetType() == OptionValue::eTypePathMap) return static_cast<OptionValuePathMappings *>(this); @@ -380,6 +392,13 @@ bool OptionValue::SetLanguageValue(lldb::LanguageType new_language) { return false; } +std::vector<FormatEntity::Entry> OptionValue::GetFormatEntityList() const { + std::lock_guard<std::mutex> lock(m_mutex); + if (const OptionValueFormatEntityList *option_value = GetAsFormatEntityList()) + return option_value->GetCurrentValue(); + return {}; +} + const FormatEntity::Entry *OptionValue::GetFormatEntity() const { std::lock_guard<std::mutex> lock(m_mutex); if (const OptionValueFormatEntity *option_value = GetAsFormatEntity()) @@ -502,6 +521,8 @@ const char *OptionValue::GetBuiltinTypeAsCString(Type t) { return "format"; case eTypeFormatEntity: return "format-string"; + case eTypeFormatEntityList: + return "format-string-list"; case eTypeLanguage: return "language"; case eTypePathMap: @@ -546,6 +567,9 @@ lldb::OptionValueSP OptionValue::CreateValueFromCStringForTypeMask( case 1u << eTypeFormatEntity: value_sp = std::make_shared<OptionValueFormatEntity>(nullptr); break; + case 1u << eTypeFormatEntityList: + value_sp = std::make_shared<OptionValueFormatEntityList>(); + break; case 1u << eTypeLanguage: value_sp = std::make_shared<OptionValueLanguage>(eLanguageTypeUnknown); break; diff --git a/lldb/source/Interpreter/OptionValueFormatEntityList.cpp b/lldb/source/Interpreter/OptionValueFormatEntityList.cpp new file mode 100644 index 00000000000000..6f7d46fa68618c --- /dev/null +++ b/lldb/source/Interpreter/OptionValueFormatEntityList.cpp @@ -0,0 +1,223 @@ +//===-- OptionValueFormatEntityList.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/Interpreter/OptionValueFormatEntityList.h" + +#include "lldb/Core/Module.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Utility/Stream.h" +#include "lldb/Utility/StringList.h" +using namespace lldb; +using namespace lldb_private; + +OptionValueFormatEntityList::OptionValueFormatEntityList() {} + +void OptionValueFormatEntityList::Clear() { + m_current_entries.clear(); + m_current_formats.clear(); + m_value_was_set = false; +} + +static void EscapeBackticks(llvm::StringRef str, std::string &dst) { + dst.clear(); + dst.reserve(str.size()); + + for (size_t i = 0, e = str.size(); i != e; ++i) { + char c = str[i]; + if (c == '`') { + if (i == 0 || str[i - 1] != '\\') + dst += '\\'; + } + dst += c; + } +} + +void OptionValueFormatEntityList::DumpValue(const ExecutionContext *exe_ctx, + Stream &strm, uint32_t dump_mask) { + if (dump_mask & eDumpOptionType) + strm.Printf("(%s)", GetTypeAsCString()); + if (dump_mask & eDumpOptionValue) { + const bool one_line = dump_mask & eDumpOptionCommand; + const size_t size = m_current_formats.size(); + if (dump_mask & eDumpOptionType) + strm.Printf(" =%s", (size > 0 && !one_line) ? "\n" : ""); + if (!one_line) + strm.IndentMore(); + for (uint32_t i = 0; i < size; ++i) { + if (!one_line) { + strm.Indent(); + strm.Printf("[%u]: ", i); + } + strm << m_current_formats[i]; + if (one_line) + strm << ' '; + } + if (!one_line) + strm.IndentLess(); + } +} + +static llvm::Expected<FormatEntity::Entry> Parse(llvm::StringRef str) { + FormatEntity::Entry entry; + Status error = FormatEntity::Parse(str, entry); + if (error.Fail()) + return error.ToError(); + return entry; +} + +llvm::Error OptionValueFormatEntityList::Append(llvm::StringRef str) { + auto maybe_entry = Parse(str); + if (!maybe_entry) + return maybe_entry.takeError(); + + m_current_entries.emplace_back(*maybe_entry); + m_current_formats.emplace_back(str); + + return llvm::Error::success(); +} + +llvm::Error OptionValueFormatEntityList::Insert(size_t idx, + llvm::StringRef str) { + if (idx >= m_current_formats.size()) + return llvm::createStringError( + "invalid file list index %s, index must be 0 through %u", idx, + m_current_formats.size()); + + auto maybe_entry = Parse(str); + if (!maybe_entry) + return maybe_entry.takeError(); + + m_current_entries.insert(m_current_entries.begin() + idx, *maybe_entry); + m_current_formats.insert(m_current_formats.begin() + idx, std::string(str)); + + return llvm::Error::success(); + return llvm::Error::success(); +} + +llvm::Error OptionValueFormatEntityList::Replace(size_t idx, + llvm::StringRef str) { + if (idx >= m_current_formats.size()) + return llvm::createStringError( + "invalid file list index %s, index must be 0 through %u", idx, + m_current_formats.size()); + + auto maybe_entry = Parse(str); + if (!maybe_entry) + return maybe_entry.takeError(); + + m_current_entries[idx] = *maybe_entry; + m_current_formats[idx] = str; + + return llvm::Error::success(); +} + +llvm::Error OptionValueFormatEntityList::Remove(size_t idx) { + if (idx >= m_current_formats.size()) + return llvm::createStringError( + "invalid fromat entry list index %s, index must be 0 through %u", idx, + m_current_formats.size()); + + m_current_formats.erase(m_current_formats.begin() + idx); + m_current_entries.erase(m_current_entries.begin() + idx); + + return llvm::Error::success(); +} + +Status +OptionValueFormatEntityList::SetValueFromString(llvm::StringRef value_str, + VarSetOperationType op) { + std::lock_guard<std::recursive_mutex> lock(m_mutex); + + Args args(value_str.str()); + const size_t argc = args.GetArgumentCount(); + + switch (op) { + case eVarSetOperationClear: + Clear(); + NotifyValueChanged(); + break; + + case eVarSetOperationReplace: + + case eVarSetOperationAssign: + Clear(); + // Fall through to append case + [[fallthrough]]; + case eVarSetOperationAppend: + if (argc > 0) { + m_value_was_set = true; + for (size_t i = 0; i < argc; ++i) { + if (llvm::Error err = Append(args.GetArgumentAtIndex(i))) + return Status::FromError(std::move(err)); + } + NotifyValueChanged(); + } else { + return Status::FromErrorString( + "assign operation takes at least one file path argument"); + } + break; + case eVarSetOperationInsertBefore: + case eVarSetOperationInsertAfter: + if (argc > 1) { + uint32_t idx; + if (!llvm::to_integer(args.GetArgumentAtIndex(0), idx)) + return Status::FromErrorStringWithFormat("invalid index %s", + args.GetArgumentAtIndex(0)); + if (op == eVarSetOperationInsertAfter) + ++idx; + for (size_t i = 1; i < argc; ++i, ++idx) { + if (llvm::Error err = Append(args.GetArgumentAtIndex(i))) + return Status::FromError(std::move(err)); + } + NotifyValueChanged(); + } else { + return Status::FromErrorString( + "insert operation takes an array index followed by " + "one or more values"); + } + break; + case eVarSetOperationRemove: + if (argc > 0) { + std::vector<int> indexes; + for (size_t i = 0; i < argc; ++i) { + int idx; + if (!llvm::to_integer(args.GetArgumentAtIndex(i), idx)) + return Status::FromErrorStringWithFormat( + "invalid array index '%s', aborting remove operation", + args.GetArgumentAtIndex(i)); + indexes.push_back(idx); + } + + if (indexes.empty()) + return Status::FromErrorString( + "remove operation takes one or more array index"); + + // Sort and then erase in reverse so indexes are always valid. + llvm::sort(indexes); + for (auto i : llvm::reverse(indexes)) { + if (auto err = Remove(i)) + return Status::FromError(std::move(err)); + } + } else { + return Status::FromErrorString( + "remove operation takes one or more array index"); + } + break; + case eVarSetOperationInvalid: + return OptionValue::SetValueFromString(value_str, op); + break; + } + + return {}; +} + +OptionValueSP OptionValueFormatEntityList::Clone() const { + std::lock_guard<std::recursive_mutex> lock(m_mutex); + return Cloneable::Clone(); +} diff --git a/lldb/source/Interpreter/Property.cpp b/lldb/source/Interpreter/Property.cpp index 56e45363be89a1..331735f167385a 100644 --- a/lldb/source/Interpreter/Property.cpp +++ b/lldb/source/Interpreter/Property.cpp @@ -161,6 +161,15 @@ Property::Property(const PropertyDefinition &definition) definition.default_cstr_value); break; + case OptionValue::eTypeFormatEntityList: + // "definition.default_uint_value" is not used for a + // OptionValue::eTypeFormatEntityList + m_value_sp = std::make_shared<OptionValueFormatEntityList>(); + if (definition.default_cstr_value) { + m_value_sp->SetValueFromString(definition.default_cstr_value); + } + break; + case OptionValue::eTypePathMap: // "definition.default_uint_value" tells us if notifications should occur // for path mappings >From 338883256bc5e832bef2b3595904206e50a589cc Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <jo...@devlieghere.com> Date: Wed, 18 Dec 2024 16:05:48 -0800 Subject: [PATCH 2/2] Add LLDB statusline --- lldb/include/lldb/Core/Debugger.h | 10 +- lldb/include/lldb/Core/Statusline.h | 73 ++++++ lldb/source/Core/CMakeLists.txt | 1 + lldb/source/Core/CoreProperties.td | 8 + lldb/source/Core/Debugger.cpp | 110 +++----- lldb/source/Core/Statusline.cpp | 247 ++++++++++++++++++ .../source/Interpreter/CommandInterpreter.cpp | 8 +- 7 files changed, 376 insertions(+), 81 deletions(-) create mode 100644 lldb/include/lldb/Core/Statusline.h create mode 100644 lldb/source/Core/Statusline.cpp diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 70f4c4216221c6..764275105ee494 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" @@ -308,6 +309,10 @@ class Debugger : public std::enable_shared_from_this<Debugger>, bool SetShowProgress(bool show_progress); + bool GetShowStatusline() const; + + std::vector<FormatEntity::Entry> GetStatuslineFormat() const; + llvm::StringRef GetShowProgressAnsiPrefix() const; llvm::StringRef GetShowProgressAnsiSuffix() const; @@ -595,6 +600,9 @@ class Debugger : public std::enable_shared_from_this<Debugger>, /// Manually stop the debugger's default event handler. void StopEventHandlerThread(); + void ShowStatusline(); + void HideStatusline(); + /// Force flushing the process's pending stdout and stderr to the debugger's /// asynchronous stdout and stderr streams. void FlushProcessOutput(Process &process, bool flush_stdout, @@ -728,7 +736,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; diff --git a/lldb/include/lldb/Core/Statusline.h b/lldb/include/lldb/Core/Statusline.h new file mode 100644 index 00000000000000..ad5c0e47c2803d --- /dev/null +++ b/lldb/include/lldb/Core/Statusline.h @@ -0,0 +1,73 @@ +//===-- 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 +// +//===----------------------------------------------------------------------===// +#include "lldb/Core/Debugger.h" +#include "llvm/ADT/SmallVector.h" +#include <string> +#ifndef LLDB_CORE_STATUSBAR_H +#define LLDB_CORE_STATUSBAR_H + +namespace lldb_private { +class Statusline { +public: + Statusline(Debugger &debugger); + ~Statusline(); + + void Enable(); + void Disable(); + + void Clear(); + void Draw(llvm::StringRef msg); + + bool StartStatuslineThread(); + void StopStatuslineThread(); + + void ReportProgress(const ProgressEventData &data); + + void TerminalSizeChanged() { m_terminal_size_has_changed = 1; } + +private: + // Update terminal dimensions. + void UpdateTerminalProperties(); + + // Set the scroll window to the given height. + void SetScrollWindow(uint64_t height); + + // Write at the given column. + void AddAtPosition(uint64_t col, llvm::StringRef str); + + // Clear the statusline (without redrawing the background). + void Reset(); + + bool IsSupported() const; + + lldb::thread_result_t StatuslineThread(); + + Debugger &m_debugger; + HostThread m_statusline_thread; + + struct ProgressReport { + uint64_t id; + std::string message; + }; + llvm::SmallVector<ProgressReport, 8> m_progress_reports; + + 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; + + std::string m_ansi_prefix = "${ansi.bg.cyan}${ansi.fg.black}"; + std::string m_ansi_suffix = "${ansi.normal}"; + llvm::SmallVector<FormatEntity::Entry> m_components; + + bool m_statusline_thread_exit = false; + std::mutex m_statusline_mutex; + std::condition_variable m_statusline_cv; +}; +} // namespace lldb_private +#endif // LLDB_CORE_STATUSBAR_H diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt index 6d14f7a87764e0..5d4576837dbe61 100644 --- a/lldb/source/Core/CMakeLists.txt +++ b/lldb/source/Core/CMakeLists.txt @@ -46,6 +46,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 d3816c3070bbc5..37b0ec231d4d48 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", "FormatEntityList">, + Global, + DefaultStringValue<"${module.file.basename} ${line.file.basename}:${line.number}:${line.column} ${thread.stop-reason}">, + 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 6ceb209269c9e7..3e83536544a4c6 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -376,6 +376,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; } @@ -392,6 +394,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; } @@ -454,6 +458,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); +} + +std::vector<FormatEntity::Entry> Debugger::GetStatuslineFormat() const { + const uint32_t idx = ePropertyStatuslineFormat; + return GetPropertyAtIndexAs<std::vector<FormatEntity::Entry>>(idx, {}); +} + bool Debugger::GetUseAutosuggestion() const { const uint32_t idx = ePropertyShowAutosuggestion; return GetPropertyAtIndexAs<bool>( @@ -989,6 +1004,7 @@ void Debugger::Clear() { ClearIOHandlers(); StopIOHandlerThread(); StopEventHandlerThread(); + HideStatusline(); m_listener_sp->Clear(); for (TargetSP target_sp : m_target_list.Targets()) { if (target_sp) { @@ -2080,84 +2096,8 @@ 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. - File &file = GetOutputFile(); - if (!file.GetIsInteractive() || !file.GetIsTerminalWithColors()) - return; - - StreamSP output = GetAsyncOutputStream(); - - // 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; - } - - // 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"); - - // Flush the output. - output->Flush(); + if (m_statusline) + m_statusline->ReportProgress(*data); } void Debugger::HandleDiagnosticEvent(const lldb::EventSP &event_sp) { @@ -2169,6 +2109,20 @@ void Debugger::HandleDiagnosticEvent(const lldb::EventSP &event_sp) { data->Dump(stream.get()); } +void Debugger::ShowStatusline() { + if (!m_statusline && GetShowStatusline()) { + m_statusline.emplace(*this); + m_statusline->StartStatuslineThread(); + } +} + +void Debugger::HideStatusline() { + if (m_statusline) { + m_statusline->StopStatuslineThread(); + m_statusline.reset(); + } +} + bool Debugger::HasIOHandlerThread() const { return m_io_handler_thread.IsJoinable(); } diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp new file mode 100644 index 00000000000000..018742ac33b584 --- /dev/null +++ b/lldb/source/Core/Statusline.cpp @@ -0,0 +1,247 @@ +//===-- 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/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 <sys/ioctl.h> +#include <termios.h> + +#define ESCAPE "\x1b" +#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; + +Statusline::Statusline(Debugger &debugger) : m_debugger(debugger) {} + +Statusline::~Statusline() { StopStatuslineThread(); } + +bool Statusline::IsSupported() const { + File &file = m_debugger.GetOutputFile(); + return file.GetIsInteractive() && file.GetIsTerminalWithColors(); +} + +void Statusline::Enable() { + if (!IsSupported()) + return; + + UpdateTerminalProperties(); + // Reduce the scroll window to make space for the status bar below. + SetScrollWindow(m_terminal_height - 1); +} + +void Statusline::Disable() { + UpdateTerminalProperties(); + // Clear the previous status bar if any. + Clear(); + // Extend the scroll window to cover the status bar. + SetScrollWindow(m_terminal_height); +} + +void Statusline::Draw(llvm::StringRef str) { + UpdateTerminalProperties(); + + const uint32_t ellipsis = 3; + if (str.size() + ellipsis >= m_terminal_width) + str = str.substr(0, m_terminal_width - ellipsis); + + StreamFile &out = m_debugger.GetOutputStream(); + out << ANSI_SAVE_CURSOR; + out.Printf(ANSI_TO_START_OF_ROW, static_cast<unsigned>(m_terminal_height)); + out << ANSI_CLEAR_LINE; + out << ansi::FormatAnsiTerminalCodes(m_ansi_prefix); + out << str; + out << std::string(m_terminal_width - str.size(), ' '); + out << ansi::FormatAnsiTerminalCodes(m_ansi_suffix); + out << ANSI_RESTORE_CURSOR; +} + +void Statusline::Reset() { + StreamFile &out = m_debugger.GetOutputStream(); + out << ANSI_SAVE_CURSOR; + out.Printf(ANSI_TO_START_OF_ROW, static_cast<unsigned>(m_terminal_height)); + out << ANSI_CLEAR_LINE; + out << ANSI_RESTORE_CURSOR; +} + +void Statusline::Clear() { Draw(""); } + +void Statusline::UpdateTerminalProperties() { + if (m_terminal_size_has_changed == 0) + return; + + // Clear the previous statusline. + Reset(); + + // Purposely ignore the terminal settings. If the setting doesn't match + // reality and we draw the status bar over existing text, we have no way to + // recover. + struct winsize window_size; + if ((isatty(STDIN_FILENO) != 0) && + ::ioctl(STDIN_FILENO, TIOCGWINSZ, &window_size) == 0) { + m_terminal_width = window_size.ws_col; + m_terminal_height = window_size.ws_row; + } + + // 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) { + StreamFile &out = m_debugger.GetOutputStream(); + out << '\n'; + out << ANSI_SAVE_CURSOR; + out.Printf(ANSI_SET_SCROLL_ROWS, static_cast<unsigned>(height)); + out << ANSI_RESTORE_CURSOR; + out.Printf(ANSI_UP_ROWS, 1); + out << ANSI_CLEAR_BELOW; + out.Flush(); + + m_scroll_height = height; +} + +lldb::thread_result_t Statusline::StatuslineThread() { + using namespace std::chrono_literals; + static constexpr const std::chrono::milliseconds g_refresh_rate = 100ms; + + bool exit = false; + std::optional<ProgressReport> progress_report; + + while (!exit) { + std::unique_lock<std::mutex> lock(m_statusline_mutex); + if (!m_statusline_cv.wait_for(lock, g_refresh_rate, + [&]() { return m_statusline_thread_exit; })) { + // We hit the timeout. First check if we're asked to exit. + if (m_statusline_thread_exit) { + exit = true; + continue; + } + + StreamString stream; + ExecutionContext exe_ctx = + m_debugger.GetCommandInterpreter().GetExecutionContext(); + SymbolContext symbol_ctx; + if (auto frame_sp = exe_ctx.GetFrameSP()) + symbol_ctx = frame_sp->GetSymbolContext(eSymbolContextEverything); + + // Add the user-configured components. + bool add_separator = false; + for (const FormatEntity::Entry& entry : m_debugger.GetStatuslineFormat()) { + if (add_separator) + stream << " | "; + add_separator = + FormatEntity::Format(entry, stream, &symbol_ctx, &exe_ctx, nullptr, + nullptr, false, false); + } + + // Add progress reports at the end, if enabled. + if (m_debugger.GetShowProgress()) { + if (m_progress_reports.empty()) { + progress_report.reset(); + } else { + progress_report.emplace(m_progress_reports.back()); + } + if (progress_report) { + if (add_separator) + stream << " | "; + stream << progress_report->message; + } + } + + Draw(stream.GetString()); + } else { + // We got notified and the predicate passed. First check if we're asked to + // exit. + if (m_statusline_thread_exit) { + exit = true; + continue; + } + } + } + + return {}; +} + +bool Statusline::StartStatuslineThread() { + Enable(); + if (!m_statusline_thread.IsJoinable()) { + m_statusline_thread_exit = false; + llvm::Expected<HostThread> statusline_thread = ThreadLauncher::LaunchThread( + "lldb.debugger.statusline", [this] { return StatuslineThread(); }); + + if (statusline_thread) { + m_statusline_thread = *statusline_thread; + } else { + LLDB_LOG_ERROR(GetLog(LLDBLog::Host), statusline_thread.takeError(), + "failed to launch host thread: {0}"); + } + } + return m_statusline_thread.IsJoinable(); +} + +void Statusline::StopStatuslineThread() { + if (m_statusline_thread.IsJoinable()) { + { + std::lock_guard<std::mutex> guard(m_statusline_mutex); + m_statusline_thread_exit = true; + } + m_statusline_cv.notify_one(); + m_statusline_thread.Join(nullptr); + } + Disable(); +} + +void Statusline::ReportProgress(const ProgressEventData &data) { + // Make a local copy of the incoming progress report, which might get modified + // below. + ProgressReport progress_report{ + data.GetID(), data.IsFinite() + ? llvm::formatv("[{0}/{1}] {2}", data.GetCompleted(), + data.GetTotal(), data.GetMessage()) + .str() + : data.GetMessage()}; + + std::lock_guard<std::mutex> guard(m_statusline_mutex); + + // Do some bookkeeping regardless of whether we're going to display + // progress reports. + 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); + } +} diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index 764dcfd1903b19..57b7efa0dc705f 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -3447,8 +3447,10 @@ CommandInterpreterRunResult CommandInterpreter::RunCommandInterpreter( m_debugger.RunIOHandlerAsync(GetIOHandler(force_create, &options)); m_result = CommandInterpreterRunResult(); - if (options.GetAutoHandleEvents()) + if (options.GetAutoHandleEvents()) { m_debugger.StartEventHandlerThread(); + m_debugger.ShowStatusline(); + } if (options.GetSpawnThread()) { m_debugger.StartIOHandlerThread(); @@ -3461,8 +3463,10 @@ CommandInterpreterRunResult CommandInterpreter::RunCommandInterpreter( m_debugger.RunIOHandlers(); m_debugger.SetIOHandlerThread(old_io_handler_thread); - if (options.GetAutoHandleEvents()) + if (options.GetAutoHandleEvents()) { m_debugger.StopEventHandlerThread(); + m_debugger.HideStatusline(); + } } return m_result; _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits