https://github.com/Teemperor created https://github.com/llvm/llvm-project/pull/206939
None >From 3a6887dfb0750f11843ec8f1d8c19b6d0edd6447 Mon Sep 17 00:00:00 2001 From: Raphael Isemann <[email protected]> Date: Wed, 1 Jul 2026 11:12:32 +0100 Subject: [PATCH] [lldb] Add a command history with context for autosuggestions --- .../Interpreter/CommandHistoryWithContext.h | 116 +++++++++ .../lldb/Interpreter/CommandInterpreter.h | 8 + lldb/source/Interpreter/CMakeLists.txt | 1 + .../Interpreter/CommandHistoryWithContext.cpp | 224 ++++++++++++++++++ .../source/Interpreter/CommandInterpreter.cpp | 70 +++++- lldb/unittests/Interpreter/CMakeLists.txt | 1 + .../TestCommandHistoryWithContext.cpp | 182 ++++++++++++++ 7 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 lldb/include/lldb/Interpreter/CommandHistoryWithContext.h create mode 100644 lldb/source/Interpreter/CommandHistoryWithContext.cpp create mode 100644 lldb/unittests/Interpreter/TestCommandHistoryWithContext.cpp diff --git a/lldb/include/lldb/Interpreter/CommandHistoryWithContext.h b/lldb/include/lldb/Interpreter/CommandHistoryWithContext.h new file mode 100644 index 0000000000000..4b905aa3407e5 --- /dev/null +++ b/lldb/include/lldb/Interpreter/CommandHistoryWithContext.h @@ -0,0 +1,116 @@ +//===-- CommandHistoryWithContext.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_COMMANDHISTORYWITHCONTEXT_H +#define LLDB_INTERPRETER_COMMANDHISTORYWITHCONTEXT_H + +#include <cstdint> +#include <deque> +#include <mutex> +#include <optional> +#include <string> + +#include "lldb/Utility/FileSpec.h" +#include "lldb/lldb-private.h" + +namespace lldb_private { + +/// The context in which a command was executed. +/// +/// This is recorded alongside every command so that autosuggestions can prefer +/// commands that were previously run in a context similar to the current one. +struct CommandExecutionContext { + /// Basename of the target's executable (e.g. "a.out"). Empty if there is no + /// target. + std::string target_name; + /// Basename of the source file at the current stop location. Empty if there + /// is no execution context with source information. + std::string file; + /// Line number at the current stop location, or 0 if unknown. + uint32_t line = 0; + /// Base name of the function at the current stop location. Empty if there is + /// no execution context. + std::string function_name; + /// Current working directory of LLDB when the command was executed. + std::string working_directory; +}; + +/// A command history that records the CommandExecutionContext in which each +/// command was executed and persists to a JSON file. +/// +/// Each distinct command is stored once, together with how many times it has +/// been invoked and the context of its most recent invocation. At most +/// GetMaxEntries() commands are retained, both in memory and on disk. When the +/// limit is exceeded, the least frequently invoked command is evicted, breaking +/// ties by evicting the oldest of them. The recorded context is used to rank +/// autosuggestions (see FindSuggestion). +class CommandHistoryWithContext { +public: + /// \param history_file + /// The JSON file used to persist the history. May be an empty FileSpec, + /// in which case the history is kept in memory only. + explicit CommandHistoryWithContext(FileSpec history_file); + + /// The maximum number of commands retained in the history. + static constexpr size_t GetMaxEntries() { return 200; } + + /// Record that \p command was executed in \p context and persist the updated + /// history to disk. + /// + /// If \p command was seen before, its invocation count is incremented, its + /// recorded context is refreshed to \p context, and it becomes the most + /// recently used entry. Otherwise a new entry is added, evicting the least + /// frequently invoked existing entry (oldest first on ties) if the history is + /// full. + void AppendCommand(llvm::StringRef command, + const CommandExecutionContext &context); + + /// Find a suggestion that completes \p line, or std::nullopt if none exists. + /// + /// Only history entries whose command starts with \p line are considered. + /// Among those, the following preference order is applied relative to the + /// \p current context: + /// 1. the most recent command run at the same source file and line, else + /// 2. the most recent command run in the same function, else + /// 3. the most recent command run against the same target. + /// If none of these context matches apply (e.g. there is no execution + /// context), the most recent command that starts with \p line is returned. + /// + /// The returned string is the remainder of the matching command after + /// \p line, i.e. the text to append to what the user already typed. + std::optional<std::string> + FindSuggestion(llvm::StringRef line, + const CommandExecutionContext ¤t) const; + + /// Load the history from the JSON file, replacing the in-memory history. A + /// missing or malformed file is treated as an empty history. + void Load(); + + /// Write the history to the JSON file. + void Save() const; + +private: + struct Entry { + std::string command; + CommandExecutionContext context; + /// The number of times this command has been invoked. + uint64_t invocation_count = 1; + }; + + /// Implementation of Save() that assumes \c m_mutex is already held. + void SaveLocked() const; + + mutable std::recursive_mutex m_mutex; + /// Oldest command at the front, most recent at the back. + std::deque<Entry> m_entries; + FileSpec m_history_file; +}; + +} // namespace lldb_private + +#endif // LLDB_INTERPRETER_COMMANDHISTORYWITHCONTEXT_H diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h index 6fd5b5285c27d..6a3074dea9c9a 100644 --- a/lldb/include/lldb/Interpreter/CommandInterpreter.h +++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h @@ -13,6 +13,7 @@ #include "lldb/Core/IOHandler.h" #include "lldb/Interpreter/CommandAlias.h" #include "lldb/Interpreter/CommandHistory.h" +#include "lldb/Interpreter/CommandHistoryWithContext.h" #include "lldb/Interpreter/CommandObject.h" #include "lldb/Interpreter/ScriptInterpreter.h" #include "lldb/Utility/Args.h" @@ -727,6 +728,10 @@ class CommandInterpreter : public Broadcaster, void RestoreExecutionContext(); + /// Capture the current execution context (target, stop location, function, + /// and working directory) for recording alongside a command in the history. + CommandExecutionContext GetCurrentCommandContext(); + void SourceInitFile(FileSpec file, CommandReturnObject &result); // Completely resolves aliases and abbreviations, returning a pointer to the @@ -784,6 +789,9 @@ class CommandInterpreter : public Broadcaster, CommandObject::CommandMap m_user_mw_dict; // Stores user-defined multiword commands CommandHistory m_command_history; + /// Command history annotated with the execution context of each command, + /// persisted to a JSON file and used to rank autosuggestions. + CommandHistoryWithContext m_command_history_with_context; std::string m_repeat_command; // Stores the command that will be executed for // an empty command string. lldb::IOHandlerSP m_command_io_handler_sp; diff --git a/lldb/source/Interpreter/CMakeLists.txt b/lldb/source/Interpreter/CMakeLists.txt index 4bb59d7550b0d..d5db590039f81 100644 --- a/lldb/source/Interpreter/CMakeLists.txt +++ b/lldb/source/Interpreter/CMakeLists.txt @@ -15,6 +15,7 @@ add_subdirectory(Interfaces) add_lldb_library(lldbInterpreter NO_PLUGIN_DEPENDENCIES CommandAlias.cpp CommandHistory.cpp + CommandHistoryWithContext.cpp CommandInterpreter.cpp CommandObject.cpp CommandOptionValidators.cpp diff --git a/lldb/source/Interpreter/CommandHistoryWithContext.cpp b/lldb/source/Interpreter/CommandHistoryWithContext.cpp new file mode 100644 index 0000000000000..ab98cfba74d10 --- /dev/null +++ b/lldb/source/Interpreter/CommandHistoryWithContext.cpp @@ -0,0 +1,224 @@ +//===-- CommandHistoryWithContext.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/CommandHistoryWithContext.h" + +#include "lldb/Utility/LLDBLog.h" +#include "lldb/Utility/Log.h" + +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +using namespace lldb; +using namespace lldb_private; + +CommandHistoryWithContext::CommandHistoryWithContext(FileSpec history_file) + : m_history_file(std::move(history_file)) {} + +void CommandHistoryWithContext::AppendCommand( + llvm::StringRef command, const CommandExecutionContext &context) { + std::lock_guard<std::recursive_mutex> guard(m_mutex); + + // If this command was seen before, bump its invocation count, refresh its + // context, and move it to the back so it becomes the most recently used. + for (auto it = m_entries.begin(); it != m_entries.end(); ++it) { + if (it->command != command) + continue; + Entry entry = std::move(*it); + m_entries.erase(it); + ++entry.invocation_count; + entry.context = context; + m_entries.push_back(std::move(entry)); + SaveLocked(); + return; + } + + m_entries.push_back(Entry{command.str(), context, /*invocation_count=*/1}); + + // Enforce the size cap by evicting the least frequently invoked entry. On a + // tie, evict the oldest (front-most) of them: iterating from the front and + // only replacing the victim on a strictly smaller count keeps the oldest + // minimum-count entry selected. + while (m_entries.size() > GetMaxEntries()) { + auto victim = m_entries.begin(); + for (auto it = std::next(m_entries.begin()); it != m_entries.end(); ++it) { + if (it->invocation_count < victim->invocation_count) + victim = it; + } + m_entries.erase(victim); + } + + SaveLocked(); +} + +std::optional<std::string> +CommandHistoryWithContext::FindSuggestion( + llvm::StringRef line, const CommandExecutionContext ¤t) const { + std::lock_guard<std::recursive_mutex> guard(m_mutex); + + if (line.empty()) + return std::nullopt; + + // The best candidate for each tier of the preference list. Because we walk + // the history from most recent to oldest and only assign each pointer once, + // every pointer ends up referring to the most recent matching command. + const Entry *by_file_line = nullptr; + const Entry *by_function = nullptr; + const Entry *by_target = nullptr; + const Entry *by_prefix = nullptr; + + const bool have_file_line = !current.file.empty() && current.line != 0; + + for (auto it = m_entries.rbegin(); it != m_entries.rend(); ++it) { + const Entry &entry = *it; + // A suggestion must complete what the user already typed. + if (!llvm::StringRef(entry.command).starts_with(line)) + continue; + + const CommandExecutionContext &ctx = entry.context; + + if (!by_prefix) + by_prefix = &entry; + + if (!by_file_line && have_file_line && ctx.file == current.file && + ctx.line == current.line) + by_file_line = &entry; + + if (!by_function && !current.function_name.empty() && + ctx.function_name == current.function_name) + by_function = &entry; + + if (!by_target && !current.target_name.empty() && + ctx.target_name == current.target_name) + by_target = &entry; + } + + // Preference: same file/line, then same function, then same target, and + // finally fall back to the most recent command sharing the prefix. + const Entry *chosen = by_file_line ? by_file_line + : by_function ? by_function + : by_target ? by_target + : by_prefix; + if (!chosen) + return std::nullopt; + + return llvm::StringRef(chosen->command).drop_front(line.size()).str(); +} + +void CommandHistoryWithContext::Load() { + std::lock_guard<std::recursive_mutex> guard(m_mutex); + m_entries.clear(); + + if (!m_history_file) + return; + + Log *log = GetLog(LLDBLog::Host); + const std::string path = m_history_file.GetPath(); + + llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> buffer_or_err = + llvm::MemoryBuffer::getFile(path); + if (!buffer_or_err) + return; // The file most likely does not exist yet. + + llvm::Expected<llvm::json::Value> value = + llvm::json::parse((*buffer_or_err)->getBuffer()); + if (!value) { + LLDB_LOG_ERROR(log, value.takeError(), + "failed to parse command history file: {0}"); + return; + } + + const llvm::json::Object *root = value->getAsObject(); + if (!root) + return; + + const llvm::json::Array *commands = root->getArray("commands"); + if (!commands) + return; + + for (const llvm::json::Value &v : *commands) { + const llvm::json::Object *obj = v.getAsObject(); + if (!obj) + continue; + + Entry entry; + entry.command = obj->getString("command").value_or("").str(); + if (entry.command.empty()) + continue; + entry.context.target_name = obj->getString("target").value_or("").str(); + entry.context.file = obj->getString("file").value_or("").str(); + entry.context.line = + static_cast<uint32_t>(obj->getInteger("line").value_or(0)); + entry.context.function_name = + obj->getString("function").value_or("").str(); + entry.context.working_directory = obj->getString("cwd").value_or("").str(); + // Restore the invocation count, defaulting to 1 for older files or bad + // values so every retained command counts as invoked at least once. + uint64_t count = static_cast<uint64_t>(obj->getInteger("count").value_or(1)); + entry.invocation_count = count ? count : 1; + m_entries.push_back(std::move(entry)); + } + + // Guard against an oversized file left behind by another version. + while (m_entries.size() > GetMaxEntries()) + m_entries.pop_front(); +} + +void CommandHistoryWithContext::Save() const { + std::lock_guard<std::recursive_mutex> guard(m_mutex); + SaveLocked(); +} + +void CommandHistoryWithContext::SaveLocked() const { + if (!m_history_file) + return; + + Log *log = GetLog(LLDBLog::Host); + const std::string path = m_history_file.GetPath(); + + // Make sure the containing (cache) directory exists. + llvm::StringRef parent = llvm::sys::path::parent_path(path); + if (!parent.empty()) { + if (std::error_code ec = llvm::sys::fs::create_directories(parent)) { + LLDB_LOG(log, "failed to create command history directory {0}: {1}", + parent, ec.message()); + return; + } + } + + llvm::json::Array commands; + for (const Entry &entry : m_entries) { + commands.push_back(llvm::json::Object{ + {"command", entry.command}, + {"target", entry.context.target_name}, + {"file", entry.context.file}, + {"line", static_cast<int64_t>(entry.context.line)}, + {"function", entry.context.function_name}, + {"cwd", entry.context.working_directory}, + {"count", static_cast<int64_t>(entry.invocation_count)}, + }); + } + + llvm::json::Value root = llvm::json::Object{ + {"version", 1}, + {"commands", std::move(commands)}, + }; + + std::error_code ec; + llvm::raw_fd_ostream os(path, ec, llvm::sys::fs::OF_Text); + if (ec) { + LLDB_LOG(log, "failed to write command history file {0}: {1}", path, + ec.message()); + return; + } + os << llvm::formatv("{0}", root); +} diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index 9887d24112c20..aebc7e2682fae 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -78,9 +78,12 @@ #include "lldb/Interpreter/Property.h" #include "lldb/Utility/Args.h" +#include "lldb/Symbol/SymbolContext.h" #include "lldb/Target/Language.h" #include "lldb/Target/Process.h" +#include "lldb/Target/StackFrame.h" #include "lldb/Target/StopInfo.h" +#include "lldb/Target/Target.h" #include "lldb/Target/TargetList.h" #include "lldb/Target/Thread.h" #include "lldb/Target/UnixSignals.h" @@ -88,6 +91,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/SmallString.h" +#include "llvm/Support/FileSystem.h" #include "llvm/Support/FormatAdapters.h" #include "llvm/Support/Path.h" #include "llvm/Support/PrettyStackTrace.h" @@ -127,6 +131,20 @@ llvm::StringRef CommandInterpreter::GetStaticBroadcasterClass() { return class_name; } +/// Return the path of the JSON file used to persist the context-annotated +/// command history. It lives next to the other persistent LLDB caches, in the +/// "lldb" subdirectory of the user's cache directory (e.g. ~/.cache/lldb on +/// Linux or ~/Library/Caches/lldb on macOS). An empty FileSpec is returned if +/// the cache directory cannot be determined, disabling persistence. +static FileSpec GetCommandHistoryFilePath() { + llvm::SmallString<128> path; + if (!llvm::sys::path::cache_directory(path)) + return FileSpec(); + llvm::sys::path::append(path, "lldb"); + llvm::sys::path::append(path, "command-history.json"); + return FileSpec(path); +} + CommandInterpreter::CommandInterpreter(Debugger &debugger, bool synchronous_execution) : Broadcaster(debugger.GetBroadcasterManager(), @@ -135,6 +153,7 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger, IOHandlerDelegate(IOHandlerDelegate::Completion::LLDBCommand), m_debugger(debugger), m_synchronous_execution(true), m_skip_lldbinit_files(false), m_skip_app_init_files(false), + m_command_history_with_context(GetCommandHistoryFilePath()), m_comment_char('#'), m_batch_command_mode(false), m_truncation_warning(eNoOmission), m_max_depth_warning(eNoOmission), m_command_source_depth(0) { @@ -144,6 +163,9 @@ CommandInterpreter::CommandInterpreter(Debugger &debugger, SetSynchronous(synchronous_execution); CheckInWithManager(); m_collection_sp->Initialize(g_interpreter_properties_def); + // Restore the context-annotated command history from previous sessions so + // that autosuggestions can draw on it immediately. + m_command_history_with_context.Load(); } bool CommandInterpreter::GetExpandRegexAliases() const { @@ -2269,8 +2291,11 @@ bool CommandInterpreter::HandleCommand(const char *command_line, } } - if (add_to_history) + if (add_to_history) { m_command_history.AppendString(original_command_string); + m_command_history_with_context.AppendCommand(original_command_string, + GetCurrentCommandContext()); + } const std::size_t actual_cmd_name_len = cmd_obj->GetCommandName().size(); if (actual_cmd_name_len < command_string.length()) @@ -2390,17 +2415,46 @@ void CommandInterpreter::HandleCompletion(CompletionRequest &request) { HandleCompletionMatches(request); } +CommandExecutionContext CommandInterpreter::GetCurrentCommandContext() { + CommandExecutionContext context; + + // Record LLDB's current working directory. + llvm::SmallString<256> cwd; + if (!llvm::sys::fs::current_path(cwd)) + context.working_directory = std::string(cwd); + + ExecutionContext exe_ctx(GetExecutionContext()); + + // Record the basename of the target's executable. + if (Target *target = exe_ctx.GetTargetPtr()) { + if (Module *exe_module = target->GetExecutableModulePointer()) + context.target_name = + exe_module->GetFileSpec().GetFilename().str(); + } + + // Record the current stop location: source file and line, plus the base name + // of the function we are stopped in. + if (StackFrame *frame = exe_ctx.GetFramePtr()) { + SymbolContext sc = frame->GetSymbolContext( + eSymbolContextFunction | eSymbolContextLineEntry | eSymbolContextSymbol); + context.function_name = + sc.GetFunctionName(Mangled::ePreferDemangledWithoutArguments) + .GetString(); + if (sc.line_entry.IsValid()) { + context.file = sc.line_entry.GetFile().GetFilename().str(); + context.line = sc.line_entry.line; + } + } + + return context; +} + std::optional<std::string> CommandInterpreter::GetAutoSuggestionForCommand(llvm::StringRef line) { if (line.empty()) return std::nullopt; - const size_t s = m_command_history.GetSize(); - for (int i = s - 1; i >= 0; --i) { - llvm::StringRef entry = m_command_history.GetStringAtIndex(i); - if (entry.consume_front(line)) - return entry.str(); - } - return std::nullopt; + return m_command_history_with_context.FindSuggestion( + line, GetCurrentCommandContext()); } void CommandInterpreter::UpdatePrompt(llvm::StringRef new_prompt) { diff --git a/lldb/unittests/Interpreter/CMakeLists.txt b/lldb/unittests/Interpreter/CMakeLists.txt index 7eec76105aad2..751421cca92c8 100644 --- a/lldb/unittests/Interpreter/CMakeLists.txt +++ b/lldb/unittests/Interpreter/CMakeLists.txt @@ -1,6 +1,7 @@ add_lldb_unittest(InterpreterTests TestCommandPaths.cpp TestCommandReturnObject.cpp + TestCommandHistoryWithContext.cpp TestCompletion.cpp TestOptionArgParser.cpp TestOptions.cpp diff --git a/lldb/unittests/Interpreter/TestCommandHistoryWithContext.cpp b/lldb/unittests/Interpreter/TestCommandHistoryWithContext.cpp new file mode 100644 index 0000000000000..dde8b2940dddf --- /dev/null +++ b/lldb/unittests/Interpreter/TestCommandHistoryWithContext.cpp @@ -0,0 +1,182 @@ +//===-- TestCommandHistoryWithContext.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/CommandHistoryWithContext.h" +#include "lldb/Utility/FileSpec.h" + +#include "llvm/Support/FileSystem.h" + +#include "gtest/gtest.h" + +using namespace lldb_private; + +static CommandExecutionContext MakeContext(std::string target, std::string file, + uint32_t line, + std::string function) { + CommandExecutionContext context; + context.target_name = std::move(target); + context.file = std::move(file); + context.line = line; + context.function_name = std::move(function); + return context; +} + +TEST(CommandHistoryWithContextTest, EmptyHistory) { + CommandHistoryWithContext history((FileSpec())); + EXPECT_EQ(history.FindSuggestion("print", CommandExecutionContext()), + std::nullopt); +} + +TEST(CommandHistoryWithContextTest, EmptyLineHasNoSuggestion) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print x", CommandExecutionContext()); + EXPECT_EQ(history.FindSuggestion("", CommandExecutionContext()), + std::nullopt); +} + +TEST(CommandHistoryWithContextTest, SuggestionIsRemainderAfterPrefix) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print hello", CommandExecutionContext()); + EXPECT_EQ(history.FindSuggestion("print ", CommandExecutionContext()), + std::optional<std::string>("hello")); +} + +TEST(CommandHistoryWithContextTest, FallsBackToMostRecentPrefixMatch) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print older", CommandExecutionContext()); + history.AppendCommand("print newer", CommandExecutionContext()); + // With no meaningful current context, the most recent prefix match wins. + EXPECT_EQ(history.FindSuggestion("print ", CommandExecutionContext()), + std::optional<std::string>("newer")); +} + +TEST(CommandHistoryWithContextTest, PrefersSameFileAndLine) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print at_a", MakeContext("a.out", "a.c", 10, "foo")); + history.AppendCommand("print at_b", MakeContext("a.out", "b.c", 20, "bar")); + history.AppendCommand("print at_c", MakeContext("a.out", "c.c", 30, "baz")); + + // Even though "print at_c" is the most recent, the command from the same + // file and line must be preferred. + CommandExecutionContext current = MakeContext("a.out", "a.c", 10, "bar"); + EXPECT_EQ(history.FindSuggestion("print ", current), + std::optional<std::string>("at_a")); +} + +TEST(CommandHistoryWithContextTest, PrefersMostRecentSameFileAndLine) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print first", MakeContext("a.out", "a.c", 10, "foo")); + history.AppendCommand("print second", MakeContext("a.out", "a.c", 10, "foo")); + + CommandExecutionContext current = MakeContext("a.out", "a.c", 10, "foo"); + EXPECT_EQ(history.FindSuggestion("print ", current), + std::optional<std::string>("second")); +} + +TEST(CommandHistoryWithContextTest, PrefersSameFunctionWhenNoFileLineMatch) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print at_a", MakeContext("a.out", "a.c", 10, "foo")); + history.AppendCommand("print at_b", MakeContext("a.out", "b.c", 20, "bar")); + history.AppendCommand("print at_c", MakeContext("a.out", "c.c", 30, "baz")); + + // No entry shares the current file/line, so the same-function entry wins. + CommandExecutionContext current = MakeContext("a.out", "z.c", 99, "bar"); + EXPECT_EQ(history.FindSuggestion("print ", current), + std::optional<std::string>("at_b")); +} + +TEST(CommandHistoryWithContextTest, PrefersSameTargetWhenNoBetterMatch) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print at_a", MakeContext("a.out", "a.c", 10, "foo")); + history.AppendCommand("print at_b", MakeContext("b.out", "b.c", 20, "bar")); + + // Neither file/line nor function match; the same-target entry is chosen. + CommandExecutionContext current = MakeContext("a.out", "z.c", 99, "nope"); + EXPECT_EQ(history.FindSuggestion("print ", current), + std::optional<std::string>("at_a")); +} + +TEST(CommandHistoryWithContextTest, MergesDuplicateCommandsAndRefreshesContext) { + CommandHistoryWithContext history((FileSpec())); + history.AppendCommand("print x", MakeContext("a.out", "a.c", 10, "foo")); + // Re-invoking the same command refreshes its context to the latest one and + // makes it the most recently used entry rather than adding a duplicate. + history.AppendCommand("print x", MakeContext("a.out", "b.c", 20, "bar")); + + // The refreshed context (b.c:20) is what gets matched now. + CommandExecutionContext at_new = MakeContext("a.out", "b.c", 20, "bar"); + EXPECT_EQ(history.FindSuggestion("print ", at_new), + std::optional<std::string>("x")); +} + +TEST(CommandHistoryWithContextTest, EvictsLeastFrequentlyInvoked) { + CommandHistoryWithContext history((FileSpec())); + const size_t max = CommandHistoryWithContext::GetMaxEntries(); + // Fill the history with distinct commands, each invoked once. The trailing + // ';' keeps commands from being prefixes of one another. + for (size_t i = 0; i < max; ++i) + history.AppendCommand("c" + std::to_string(i) + ";", + CommandExecutionContext()); + + // Invoke "c0;" more often so it is the most frequently used command. + history.AppendCommand("c0;", CommandExecutionContext()); + history.AppendCommand("c0;", CommandExecutionContext()); + + // Adding a brand new command overflows the history and forces one eviction. + history.AppendCommand("newcmd;", CommandExecutionContext()); + + // The frequently invoked command and the newest command survive. + EXPECT_EQ(history.FindSuggestion("c0;", CommandExecutionContext()), + std::optional<std::string>("")); + EXPECT_EQ(history.FindSuggestion("newcmd;", CommandExecutionContext()), + std::optional<std::string>("")); + // The oldest of the least-invoked (count 1) commands, "c1;", is evicted. + EXPECT_EQ(history.FindSuggestion("c1;", CommandExecutionContext()), + std::nullopt); + // The next-oldest least-invoked command is kept. + EXPECT_EQ(history.FindSuggestion("c2;", CommandExecutionContext()), + std::optional<std::string>("")); +} + +TEST(CommandHistoryWithContextTest, CapsAtMaxEntries) { + CommandHistoryWithContext history((FileSpec())); + const size_t max = CommandHistoryWithContext::GetMaxEntries(); + // Append more than the cap; each command is distinct. + for (size_t i = 0; i < max + 50; ++i) + history.AppendCommand("cmd" + std::to_string(i), CommandExecutionContext()); + + // The oldest entries were dropped, so "cmd0" is gone but the newest remains. + EXPECT_EQ(history.FindSuggestion("cmd0 ", CommandExecutionContext()), + std::nullopt); + EXPECT_EQ(history.FindSuggestion("cmd" + std::to_string(max + 49), + CommandExecutionContext()), + std::optional<std::string>("")); +} + +TEST(CommandHistoryWithContextTest, PersistsAcrossInstances) { + llvm::SmallString<128> temp_path; + ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("lldb-history", "json", + temp_path)); + FileSpec history_file(temp_path); + + { + CommandHistoryWithContext history(history_file); + history.AppendCommand("print saved", + MakeContext("a.out", "a.c", 42, "foo")); + } + + // A fresh instance pointed at the same file should load the saved command + // together with its context. + CommandHistoryWithContext reloaded(history_file); + reloaded.Load(); + CommandExecutionContext current = MakeContext("a.out", "a.c", 42, "foo"); + EXPECT_EQ(reloaded.FindSuggestion("print ", current), + std::optional<std::string>("saved")); + + llvm::sys::fs::remove(temp_path); +} _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
