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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> Date: Sun, 14 Jun 2026 11:18:06 -0700 Subject: [PATCH 4/4] Update lldb/source/Core/Statusline.cpp Co-authored-by: Pavel Labath <[email protected]> --- 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; _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
