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 &current) 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 &current) 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

Reply via email to