[Lldb-commits] [lldb] [lldb] Recompute the statusline on resize without clearing the screen (PR #202691)

Jonas Devlieghere via lldb-commits lldb-commits at lists.llvm.org
Sun Jun 14 11:18:15 PDT 2026


https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/202691

>From a097f86601ddcb3f7019d53df4f80dbff8c1d65c Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Fri, 5 Jun 2026 18:41:13 -0700
Subject: [PATCH 1/4] [lldb] Recompute the statusline on resize without
 clearing the screen

On a terminal resize the statusline cleared the whole screen (ESC[2J)
and redrew, because recomputing in place was buggy: the statusline
wrapped and duplicated. The clear also wiped the visible scrollback
on every resize.

Recompute instead. After a resize the terminal still shows the old
statusline: a width shrink reflows the full-width line into
ceil(prev_width / width) rows at the bottom; growing taller leaves it
stranded at its old row. Clear only the rows it can still occupy and
redraw, preserving the scrollback above. Disable autowrap while drawing
so a line briefly wider than the terminal is clipped at the margin
rather than wrapping onto the row above.

Removing the clear exposed that Editline::Refresh() redrew the prompt
via libedit's EL_REFRESH, which uses a cursor model that is stale once
the statusline has moved the cursor, tiling the prompt once per resize.
Repaint the prompt and input from our own tracked position instead.

Fixes #146919
---
 lldb/include/lldb/Core/Statusline.h           |  7 ++-
 lldb/source/Core/Statusline.cpp               | 44 ++++++++++++++++---
 lldb/source/Host/common/Editline.cpp          | 12 ++++-
 .../statusline/TestStatusline.py              | 36 +++++++++++++++
 4 files changed, 90 insertions(+), 9 deletions(-)

diff --git a/lldb/include/lldb/Core/Statusline.h b/lldb/include/lldb/Core/Statusline.h
index eacdec306ccf9..1c716d9e1e8be 100644
--- a/lldb/include/lldb/Core/Statusline.h
+++ b/lldb/include/lldb/Core/Statusline.h
@@ -48,8 +48,11 @@ class Statusline {
     ResizeStatusline,
   };
 
-  /// Set the scroll window for the given mode.
-  void UpdateScrollWindow(ScrollWindowMode mode);
+  /// Set the scroll window for the given mode. On a resize, \p prev_width and
+  /// \p prev_height are the dimensions the statusline was last drawn at, used
+  /// to clear the rows it still occupies.
+  void UpdateScrollWindow(ScrollWindowMode mode, uint64_t prev_width = 0,
+                          uint64_t prev_height = 0);
 
   Debugger &m_debugger;
 
diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp
index d49735b3c64c2..609b63059d386 100644
--- a/lldb/source/Core/Statusline.cpp
+++ b/lldb/source/Core/Statusline.cpp
@@ -29,6 +29,8 @@
 #define ANSI_TO_START_OF_ROW ESCAPE "[%u;1f"
 #define ANSI_REVERSE_VIDEO ESCAPE "[7m"
 #define ANSI_UP_ROWS ESCAPE "[%dA"
+#define ANSI_DISABLE_AUTO_WRAP ESCAPE "[?7l"
+#define ANSI_ENABLE_AUTO_WRAP ESCAPE "[?7h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -40,10 +42,15 @@ Statusline::Statusline(Debugger &debugger)
 Statusline::~Statusline() { Disable(); }
 
 void Statusline::TerminalSizeChanged() {
+  // The dimensions the statusline was last drawn at, needed to clear it before
+  // redrawing at the new size.
+  const uint64_t prev_width = m_terminal_width;
+  const uint64_t prev_height = m_terminal_height;
+
   m_terminal_width = m_debugger.GetTerminalWidth();
   m_terminal_height = m_debugger.GetTerminalHeight();
 
-  UpdateScrollWindow(ResizeStatusline);
+  UpdateScrollWindow(ResizeStatusline, prev_width, prev_height);
 
   // Redraw the old statusline.
   Redraw(std::nullopt);
@@ -71,6 +78,9 @@ void Statusline::Draw(std::string str) {
 
   LockedStreamFile locked_stream = stream_sp->Lock();
   locked_stream << ANSI_SAVE_CURSOR;
+  // A statusline wider than the terminal (e.g. a stale width mid-resize) would
+  // wrap onto the row above; with autowrap off it is clipped at the margin.
+  locked_stream << ANSI_DISABLE_AUTO_WRAP;
   locked_stream.Printf(ANSI_TO_START_OF_ROW,
                        static_cast<unsigned>(m_terminal_height));
 
@@ -81,10 +91,12 @@ void Statusline::Draw(std::string str) {
 
   locked_stream << str;
   locked_stream << ANSI_NORMAL;
+  locked_stream << ANSI_ENABLE_AUTO_WRAP;
   locked_stream << ANSI_RESTORE_CURSOR;
 }
 
-void Statusline::UpdateScrollWindow(ScrollWindowMode mode) {
+void Statusline::UpdateScrollWindow(ScrollWindowMode mode, uint64_t prev_width,
+                                    uint64_t prev_height) {
   assert(m_terminal_width != 0 && m_terminal_height != 0);
 
   lldb::LockableStreamFileSP stream_sp = m_debugger.GetOutputStreamSP();
@@ -114,13 +126,33 @@ void Statusline::UpdateScrollWindow(ScrollWindowMode mode) {
       // Clear the screen below to hide the old statusline.
       locked_stream << ANSI_CLEAR_BELOW;
       break;
-    case ResizeStatusline:
-      // Clear the screen and update the scroll window.
-      // FIXME: Find a better solution (#146919).
-      locked_stream << ANSI_CLEAR_SCREEN;
+    case ResizeStatusline: {
+      // The old statusline is still on screen after a resize: a width shrink
+      // reflows that full-width line into ceil(prev_width / width) rows at the
+      // bottom, and growing taller strands it at its old row. Clear from the
+      // topmost row it can occupy to the bottom (preserving the scrollback
+      // above), then re-establish the scroll region. DECSTBM homes the cursor,
+      // so save and restore it.
+      const unsigned height = static_cast<unsigned>(m_terminal_height);
+      unsigned reflow = 1;
+      if (prev_width > m_terminal_width && m_terminal_width > 0)
+        reflow = (prev_width + m_terminal_width - 1) / m_terminal_width;
+      if (reflow >= height)
+        reflow = height - 1;
+      unsigned first_row = height - reflow + 1;
+      if (prev_height > 0 && prev_height < first_row)
+        first_row = static_cast<unsigned>(prev_height);
+      if (first_row < 1)
+        first_row = 1;
+
+      locked_stream << ANSI_SAVE_CURSOR;
+      locked_stream.Printf(ANSI_TO_START_OF_ROW, first_row);
+      locked_stream << ANSI_CLEAR_BELOW;
       locked_stream.Printf(ANSI_SET_SCROLL_ROWS, reduced_scroll_rows);
+      locked_stream << ANSI_RESTORE_CURSOR;
       break;
     }
+    }
   }
   m_debugger.RefreshIOHandler();
 }
diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp
index 833516b0b2c2d..fcf516c1c61c8 100644
--- a/lldb/source/Host/common/Editline.cpp
+++ b/lldb/source/Host/common/Editline.cpp
@@ -1721,7 +1721,17 @@ void Editline::Refresh() {
   if (!m_editline || !m_output_stream_sp)
     return;
   LockedStreamFile locked_stream = m_output_stream_sp->Lock();
-  el_set(m_editline, EL_REFRESH);
+  if (m_editor_status == EditorStatus::Editing) {
+    // EL_REFRESH redraws from libedit's cursor model, which is stale once the
+    // statusline has moved the cursor, so it reprints the prompt at the wrong
+    // column. Repaint from our own tracked position instead.
+    SaveEditedLine();
+    MoveCursor(CursorLocation::EditingCursor, CursorLocation::BlockStart);
+    DisplayInput();
+    MoveCursor(CursorLocation::BlockEnd, CursorLocation::EditingCursor);
+  } else {
+    el_set(m_editline, EL_REFRESH);
+  }
 }
 
 bool Editline::CompleteCharacter(char ch, EditLineGetCharType &out) {
diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py
index f5c6a817d1309..727942d6a1eac 100644
--- a/lldb/test/API/functionalities/statusline/TestStatusline.py
+++ b/lldb/test/API/functionalities/statusline/TestStatusline.py
@@ -117,6 +117,42 @@ def test_resize(self):
         self.child.expect(re.escape("\x1b[1;19r"))
         self.child.expect("(lldb)")
 
+    @skipIfEditlineSupportMissing
+    def test_resize_recomputes_without_clearing(self):
+        """Resizing should recompute the statusline in place, not clear the
+        entire screen. Clearing the screen (ESC[2J) was the old workaround for
+        the statusline wrapping/duplicating on resize; it also wiped the visible
+        scrollback on every resize."""
+        self.launch()
+        self.resize()
+        self.expect("set set show-statusline true", ["no target"])
+
+        # Capture the output emitted during the resize. The tee handles both
+        # str- and bytes-mode spawns.
+        class Tee:
+            def __init__(self):
+                self.data = b""
+
+            def write(self, s):
+                self.data += s if isinstance(s, bytes) else s.encode(
+                    "latin-1", "replace"
+                )
+                return len(s)
+
+            def flush(self):
+                pass
+
+        tee = Tee()
+        self.child.logfile_read = tee
+        self.resize(20, 60)
+        # Wait for the resize redraw to finish before inspecting the capture.
+        self.child.expect(re.escape("\x1b[1;19r"))
+        self.child.expect("(lldb)")
+        self.child.logfile_read = None
+
+        # The resize recomputes in place; it must not clear the whole screen.
+        self.assertNotIn(b"\x1b[2J", tee.data)
+
     @skipUnlessPlatform(["linux"])
     def test_target_symbols_add(self):
         """Regression test: adding symbols while the statusline is enabled

>From 48726f6ddef0752c5b3de1e6bc71b8b95319b7ae Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Tue, 9 Jun 2026 08:45:19 -0700
Subject: [PATCH 2/4] Formatting

---
 lldb/test/API/functionalities/statusline/TestStatusline.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py
index 727942d6a1eac..57658ed74772d 100644
--- a/lldb/test/API/functionalities/statusline/TestStatusline.py
+++ b/lldb/test/API/functionalities/statusline/TestStatusline.py
@@ -134,8 +134,8 @@ def __init__(self):
                 self.data = b""
 
             def write(self, s):
-                self.data += s if isinstance(s, bytes) else s.encode(
-                    "latin-1", "replace"
+                self.data += (
+                    s if isinstance(s, bytes) else s.encode("latin-1", "replace")
                 )
                 return len(s)
 

>From debc66d559a480a5fc07dc29585b8fc5cfd94c7d Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Wed, 10 Jun 2026 11:23:14 -0700
Subject: [PATCH 3/4] Speculative fix for Pavel's bug

---
 lldb/source/Core/Statusline.cpp               | 11 ++++
 .../statusline/TestStatusline.py              | 56 +++++++++++++------
 2 files changed, 51 insertions(+), 16 deletions(-)

diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp
index 609b63059d386..035fcfae5d801 100644
--- a/lldb/source/Core/Statusline.cpp
+++ b/lldb/source/Core/Statusline.cpp
@@ -145,6 +145,17 @@ void Statusline::UpdateScrollWindow(ScrollWindowMode mode, uint64_t prev_width,
       if (first_row < 1)
         first_row = 1;
 
+      // A height shrink can leave the prompt on the row the statusline is about
+      // to occupy, because the terminal reclaims the row below the cursor
+      // instead of scrolling the cursor up. Scroll up one row to lift the
+      // prompt clear, like EnableStatusline; the overlap is only ever the
+      // single statusline row, and this is a no-op unless the cursor sits at
+      // the bottom of the scroll region.
+      if (prev_height > m_terminal_height) {
+        locked_stream << '\n';
+        locked_stream.Printf(ANSI_UP_ROWS, 1);
+      }
+
       locked_stream << ANSI_SAVE_CURSOR;
       locked_stream.Printf(ANSI_TO_START_OF_ROW, first_row);
       locked_stream << ANSI_CLEAR_BELOW;
diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py
index 57658ed74772d..8891a561419cf 100644
--- a/lldb/test/API/functionalities/statusline/TestStatusline.py
+++ b/lldb/test/API/functionalities/statusline/TestStatusline.py
@@ -11,6 +11,21 @@
 from lldbgdbserverutils import get_lldb_server_exe
 
 
+class CaptureTee:
+    """Accumulate everything pexpect reads, for inspecting the bytes emitted
+    during a resize. Handles both str- and bytes-mode spawns."""
+
+    def __init__(self):
+        self.data = b""
+
+    def write(self, s):
+        self.data += s if isinstance(s, bytes) else s.encode("latin-1", "replace")
+        return len(s)
+
+    def flush(self):
+        pass
+
+
 # PExpect uses many timeouts internally and doesn't play well
 # under ASAN on a loaded machine..
 @skipIfAsan
@@ -127,22 +142,8 @@ def test_resize_recomputes_without_clearing(self):
         self.resize()
         self.expect("set set show-statusline true", ["no target"])
 
-        # Capture the output emitted during the resize. The tee handles both
-        # str- and bytes-mode spawns.
-        class Tee:
-            def __init__(self):
-                self.data = b""
-
-            def write(self, s):
-                self.data += (
-                    s if isinstance(s, bytes) else s.encode("latin-1", "replace")
-                )
-                return len(s)
-
-            def flush(self):
-                pass
-
-        tee = Tee()
+        # Capture the output emitted during the resize.
+        tee = CaptureTee()
         self.child.logfile_read = tee
         self.resize(20, 60)
         # Wait for the resize redraw to finish before inspecting the capture.
@@ -153,6 +154,29 @@ def flush(self):
         # The resize recomputes in place; it must not clear the whole screen.
         self.assertNotIn(b"\x1b[2J", tee.data)
 
+    @skipIfEditlineSupportMissing
+    def test_resize_height_shrink_makes_room(self):
+        """On a height shrink the terminal can leave the prompt on the row the
+        statusline is about to occupy. The resize scrolls the content up by the
+        lost rows (a newline per row, then a cursor-up) to lift the prompt
+        clear, the same way enabling the statusline makes room for it."""
+        self.launch()
+        # Stay above the 10-row minimum terminal height on both sides.
+        self.resize(20, 60)
+        self.expect("set set show-statusline true", ["no target"])
+
+        tee = CaptureTee()
+        self.child.logfile_read = tee
+        # Shrink the height by one row.
+        self.resize(19, 60)
+        # Wait for the resize redraw (new scroll region) to finish.
+        self.child.expect(re.escape("\x1b[1;18r"))
+        self.child.expect("(lldb)")
+        self.child.logfile_read = None
+
+        # Room is made by scrolling up: a newline followed by a cursor-up.
+        self.assertIn(b"\n\x1b[1A", tee.data)
+
     @skipUnlessPlatform(["linux"])
     def test_target_symbols_add(self):
         """Regression test: adding symbols while the statusline is enabled

>From de09d992ed25b0020f5c82ef431d746d5e7329dc Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Sun, 14 Jun 2026 11:18:06 -0700
Subject: [PATCH 4/4] Update lldb/source/Core/Statusline.cpp

Co-authored-by: Pavel Labath <pavel at labath.sk>
---
 lldb/source/Core/Statusline.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp
index 035fcfae5d801..562913c9a033d 100644
--- a/lldb/source/Core/Statusline.cpp
+++ b/lldb/source/Core/Statusline.cpp
@@ -136,7 +136,7 @@ void Statusline::UpdateScrollWindow(ScrollWindowMode mode, uint64_t prev_width,
       const unsigned height = static_cast<unsigned>(m_terminal_height);
       unsigned reflow = 1;
       if (prev_width > m_terminal_width && m_terminal_width > 0)
-        reflow = (prev_width + m_terminal_width - 1) / m_terminal_width;
+        reflow = llvm::divideCeil(prev_width, m_terminal_width);
       if (reflow >= height)
         reflow = height - 1;
       unsigned first_row = height - reflow + 1;



More information about the lldb-commits mailing list