[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