Skip to content

[lldb] Correctly restore the cursor column after resizing the statusline #145823

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

JDevlieghere
Copy link
Member

This PR ensures we correctly restore the cursor column after resizing the statusline. To ensure we have space for the statusline, we have to emit a newline to move up everything on screen. The newline causes the cursor to move to the start of the next line, which needs to be undone.

Normally, we would use escape codes to save & restore the cursor position, but that doesn't work here, as the cursor position may have (purposely) changed. Instead, we move the cursor up one line using an escape code, but we weren't restoring the column.

Interestingly, Editline was able to recover from this issue through the LineInfo struct which contains the buffer and the cursor location, which allows us to compute the column. This PR addresses the bug by relying on the active IOHandler to report its cursor position and if available, use that to restore the cursor column position.

Fixes #134064

@llvmbot
Copy link
Member

llvmbot commented Jun 26, 2025

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

This PR ensures we correctly restore the cursor column after resizing the statusline. To ensure we have space for the statusline, we have to emit a newline to move up everything on screen. The newline causes the cursor to move to the start of the next line, which needs to be undone.

Normally, we would use escape codes to save & restore the cursor position, but that doesn't work here, as the cursor position may have (purposely) changed. Instead, we move the cursor up one line using an escape code, but we weren't restoring the column.

Interestingly, Editline was able to recover from this issue through the LineInfo struct which contains the buffer and the cursor location, which allows us to compute the column. This PR addresses the bug by relying on the active IOHandler to report its cursor position and if available, use that to restore the cursor column position.

Fixes #134064


Full diff: https://github.com/llvm/llvm-project/pull/145823.diff

7 Files Affected:

  • (modified) lldb/include/lldb/Core/Debugger.h (+2)
  • (modified) lldb/include/lldb/Core/IOHandler.h (+7)
  • (modified) lldb/include/lldb/Host/Editline.h (+3)
  • (modified) lldb/source/Core/Debugger.cpp (+9)
  • (modified) lldb/source/Core/IOHandler.cpp (+9)
  • (modified) lldb/source/Core/Statusline.cpp (+26-8)
  • (modified) lldb/source/Host/common/Editline.cpp (+29-15)
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 2087ef2a11562..0052b042df789 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -133,6 +133,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
 
   void SetAsyncExecution(bool async);
 
+  std::optional<IOHandler::CursorPosition> GetIOHandlerCursorPosition();
+
   lldb::FileSP GetInputFileSP() { return m_input_file_sp; }
   File &GetInputFile() { return *m_input_file_sp; }
 
diff --git a/lldb/include/lldb/Core/IOHandler.h b/lldb/include/lldb/Core/IOHandler.h
index 2fb3d7a7c9cc3..d8f5c32106986 100644
--- a/lldb/include/lldb/Core/IOHandler.h
+++ b/lldb/include/lldb/Core/IOHandler.h
@@ -113,6 +113,11 @@ class IOHandler {
 
   virtual const char *GetHelpPrologue() { return nullptr; }
 
+  using CursorPosition = std::pair<size_t, size_t>;
+  virtual std::optional<CursorPosition> GetCursorPosition() const {
+    return std::nullopt;
+  }
+
   int GetInputFD();
 
   int GetOutputFD();
@@ -404,6 +409,8 @@ class IOHandlerEditline : public IOHandler {
 
   void PrintAsync(const char *s, size_t len, bool is_stdout) override;
 
+  virtual std::optional<CursorPosition> GetCursorPosition() const override;
+
 private:
 #if LLDB_ENABLE_LIBEDIT
   bool IsInputCompleteCallback(Editline *editline, StringList &lines);
diff --git a/lldb/include/lldb/Host/Editline.h b/lldb/include/lldb/Host/Editline.h
index c202a76758e13..388f30c9db15a 100644
--- a/lldb/include/lldb/Host/Editline.h
+++ b/lldb/include/lldb/Host/Editline.h
@@ -267,6 +267,9 @@ class Editline {
 
   size_t GetTerminalHeight() { return m_terminal_height; }
 
+  using CursorPosition = std::pair<size_t, size_t>;
+  std::optional<CursorPosition> GetCursorPosition();
+
 private:
   /// Sets the lowest line number for multi-line editing sessions.  A value of
   /// zero suppresses line number printing in the prompt.
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 445baf1f63785..622147b629df8 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -1240,6 +1240,15 @@ void Debugger::DispatchInputEndOfFile() {
     reader_sp->GotEOF();
 }
 
+std::optional<IOHandler::CursorPosition>
+Debugger::GetIOHandlerCursorPosition() {
+  std::lock_guard<std::recursive_mutex> guard(m_io_handler_stack.GetMutex());
+  IOHandlerSP reader_sp(m_io_handler_stack.Top());
+  if (reader_sp)
+    return reader_sp->GetCursorPosition();
+  return std::nullopt;
+}
+
 void Debugger::ClearIOHandlers() {
   // The bottom input reader should be the main debugger input reader.  We do
   // not want to close that one here.
diff --git a/lldb/source/Core/IOHandler.cpp b/lldb/source/Core/IOHandler.cpp
index 8aac507eaa0c2..d01499649bb3e 100644
--- a/lldb/source/Core/IOHandler.cpp
+++ b/lldb/source/Core/IOHandler.cpp
@@ -634,6 +634,15 @@ void IOHandlerEditline::GotEOF() {
 #endif
 }
 
+std::optional<IOHandler::CursorPosition>
+IOHandlerEditline::GetCursorPosition() const {
+#if LLDB_ENABLE_LIBEDIT
+  if (m_editline_up)
+    return m_editline_up->GetCursorPosition();
+#endif
+  return std::nullopt;
+}
+
 void IOHandlerEditline::PrintAsync(const char *s, size_t len, bool is_stdout) {
 #if LLDB_ENABLE_LIBEDIT
   if (m_editline_up) {
diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp
index 8a8640805cac0..6393c71d77ecb 100644
--- a/lldb/source/Core/Statusline.cpp
+++ b/lldb/source/Core/Statusline.cpp
@@ -28,6 +28,7 @@
 #define ANSI_TO_START_OF_ROW ESCAPE "[%u;1f"
 #define ANSI_REVERSE_VIDEO ESCAPE "[7m"
 #define ANSI_UP_ROWS ESCAPE "[%dA"
+#define ANSI_SET_COLUMN_N ESCAPE "[%uG"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -102,20 +103,37 @@ void Statusline::UpdateScrollWindow(ScrollWindowMode mode) {
   const unsigned scroll_height =
       (mode == DisableStatusline) ? m_terminal_height : m_terminal_height - 1;
 
+  std::optional<IOHandler::CursorPosition> cursor_position =
+      m_debugger.GetIOHandlerCursorPosition();
+
   LockedStreamFile locked_stream = stream_sp->Lock();
+
+  if (mode == EnableStatusline) {
+    // Move everything on the screen up to make space for the statusline. This
+    // is going to move the cursor to the start of the next line which we need
+    // to undo.
+    locked_stream << '\n';
+
+    // First move the cursor back up. We can't use ANSI_SAVE/RESTORE_CURSOR
+    // here, because the old and new position differ if everything on the screen
+    // moved up.
+    locked_stream.Printf(ANSI_UP_ROWS, 1);
+
+    // Finally move the cursor back to the correct column, if the IOHandler was
+    // able to tell us where that was.
+    if (cursor_position)
+      locked_stream.Printf(ANSI_SET_COLUMN_N,
+                           static_cast<unsigned>(cursor_position->first));
+  }
+
+  // Adjust the scroll window.
   locked_stream << ANSI_SAVE_CURSOR;
   locked_stream.Printf(ANSI_SET_SCROLL_ROWS, scroll_height);
   locked_stream << ANSI_RESTORE_CURSOR;
-  switch (mode) {
-  case EnableStatusline:
-    // Move everything on the screen up.
-    locked_stream.Printf(ANSI_UP_ROWS, 1);
-    locked_stream << '\n';
-    break;
-  case DisableStatusline:
+
+  if (mode == DisableStatusline) {
     // Clear the screen below to hide the old statusline.
     locked_stream << ANSI_CLEAR_BELOW;
-    break;
   }
 }
 
diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp
index b251ded6c3793..b7e7b720443db 100644
--- a/lldb/source/Host/common/Editline.cpp
+++ b/lldb/source/Host/common/Editline.cpp
@@ -122,22 +122,21 @@ static int GetOperation(HistoryOperation op) {
   //  - The H_FIRST returns the most recent entry in the history.
   //
   // The naming of the enum entries match the semantic meaning.
-  switch(op) {
-    case HistoryOperation::Oldest:
-      return H_LAST;
-    case HistoryOperation::Older:
-      return H_NEXT;
-    case HistoryOperation::Current:
-      return H_CURR;
-    case HistoryOperation::Newer:
-      return H_PREV;
-    case HistoryOperation::Newest:
-      return H_FIRST;
+  switch (op) {
+  case HistoryOperation::Oldest:
+    return H_LAST;
+  case HistoryOperation::Older:
+    return H_NEXT;
+  case HistoryOperation::Current:
+    return H_CURR;
+  case HistoryOperation::Newer:
+    return H_PREV;
+  case HistoryOperation::Newest:
+    return H_FIRST;
   }
   llvm_unreachable("Fully covered switch!");
 }
 
-
 EditLineStringType CombineLines(const std::vector<EditLineStringType> &lines) {
   EditLineStringStreamType combined_stream;
   for (EditLineStringType line : lines) {
@@ -313,8 +312,8 @@ class EditlineHistory {
   /// Path to the history file.
   std::string m_path;
 };
-}
-}
+} // namespace line_editor
+} // namespace lldb_private
 
 // Editline private methods
 
@@ -398,6 +397,20 @@ int Editline::GetLineIndexForLocation(CursorLocation location, int cursor_row) {
   return line;
 }
 
+std::optional<Editline::CursorPosition> Editline::GetCursorPosition() {
+  if (!m_editline)
+    return std::nullopt;
+
+  const LineInfoW *info = el_wline(m_editline);
+  if (!info)
+    return std::nullopt;
+
+  const size_t editline_cursor_col =
+      (int)((info->cursor - info->buffer) + GetPromptWidth()) + 1;
+
+  return {{editline_cursor_col, 0}};
+}
+
 void Editline::MoveCursor(CursorLocation from, CursorLocation to) {
   const LineInfoW *info = el_wline(m_editline);
   int editline_cursor_position =
@@ -1151,7 +1164,8 @@ unsigned char Editline::TabCommand(int ch) {
       to_add.push_back(' ');
       el_deletestr(m_editline, request.GetCursorArgumentPrefix().size());
       el_insertstr(m_editline, to_add.c_str());
-      // Clear all the autosuggestion parts if the only single space can be completed.
+      // Clear all the autosuggestion parts if the only single space can be
+      // completed.
       if (to_add == " ")
         return CC_REDISPLAY;
       return CC_REFRESH;

This PR ensures we correctly restore the cursor column after resizing
the statusline. To ensure we have space for the statusline, we have to
emit a newline to move up everything on screen. The newline causes the
cursor to move to the start of the next line, which needs to be undone.

Normally, we would use escape codes to save & restore the cursor
position, but that doesn't work here, as the cursor position may have
(purposely) changed. Instead, we move the cursor up one line using an
escape code, but we weren't restoring the column.

Interestingly, Editline was able to recover from this issue through the
LineInfo struct which contains the buffer and the cursor location, which
allows us to compute the column. This PR addresses the bug by relying on
the active IOHandler to report its cursor position and if available, use
that to restore the cursor column position.

Fixes llvm#134064
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

(lldb) prompt sometimes not printed on startup when statusline is enabled
2 participants