From 1eb795413df3c38b688d42d6595a8e1503f6d139 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Mon, 30 Jun 2025 14:34:35 -0700 Subject: [PATCH] [lldb] Correctly restore the cursor column after resizing the statusline (#146132) 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 having Editline "refresh" the cursor position. Fixes #134064 --- lldb/include/lldb/Core/Debugger.h | 2 ++ lldb/include/lldb/Core/IOHandler.h | 4 ++++ lldb/include/lldb/Host/Editline.h | 2 ++ lldb/source/Core/Debugger.cpp | 7 +++++++ lldb/source/Core/IOHandler.cpp | 7 +++++++ lldb/source/Core/Statusline.cpp | 19 +++++++++++-------- lldb/source/Host/common/Editline.cpp | 7 +++++++ .../statusline/TestStatusline.py | 19 +++++++++++++++++-- 8 files changed, 57 insertions(+), 10 deletions(-) diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 2087ef2a1156..504f936fe317 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -227,6 +227,8 @@ public: const char *GetIOHandlerHelpPrologue(); + void RefreshIOHandler(); + void ClearIOHandlers(); bool EnableLog(llvm::StringRef channel, diff --git a/lldb/include/lldb/Core/IOHandler.h b/lldb/include/lldb/Core/IOHandler.h index 2fb3d7a7c9cc..2672bbe5da2b 100644 --- a/lldb/include/lldb/Core/IOHandler.h +++ b/lldb/include/lldb/Core/IOHandler.h @@ -90,6 +90,8 @@ public: virtual void TerminalSizeChanged() {} + virtual void Refresh() {} + virtual const char *GetPrompt() { // Prompt support isn't mandatory return nullptr; @@ -404,6 +406,8 @@ public: void PrintAsync(const char *s, size_t len, bool is_stdout) override; + void Refresh() 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 c202a76758e1..947ad3bfe5ec 100644 --- a/lldb/include/lldb/Host/Editline.h +++ b/lldb/include/lldb/Host/Editline.h @@ -267,6 +267,8 @@ public: size_t GetTerminalHeight() { return m_terminal_height; } + void Refresh(); + 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 445baf1f6378..ed674ee1275c 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -1445,6 +1445,13 @@ bool Debugger::PopIOHandler(const IOHandlerSP &pop_reader_sp) { return true; } +void Debugger::RefreshIOHandler() { + std::lock_guard guard(m_io_handler_stack.GetMutex()); + IOHandlerSP reader_sp(m_io_handler_stack.Top()); + if (reader_sp) + reader_sp->Refresh(); +} + StreamUP Debugger::GetAsyncOutputStream() { return std::make_unique(*this, StreamAsynchronousIO::STDOUT); diff --git a/lldb/source/Core/IOHandler.cpp b/lldb/source/Core/IOHandler.cpp index 8aac507eaa0c..f65a1113f359 100644 --- a/lldb/source/Core/IOHandler.cpp +++ b/lldb/source/Core/IOHandler.cpp @@ -663,3 +663,10 @@ void IOHandlerEditline::PrintAsync(const char *s, size_t len, bool is_stdout) { #endif } } + +void IOHandlerEditline::Refresh() { +#if LLDB_ENABLE_LIBEDIT + if (m_editline_up) + m_editline_up->Refresh(); +#endif +} diff --git a/lldb/source/Core/Statusline.cpp b/lldb/source/Core/Statusline.cpp index 8a8640805cac..8ec57c9fa5ba 100644 --- a/lldb/source/Core/Statusline.cpp +++ b/lldb/source/Core/Statusline.cpp @@ -103,20 +103,23 @@ void Statusline::UpdateScrollWindow(ScrollWindowMode mode) { (mode == DisableStatusline) ? m_terminal_height : m_terminal_height - 1; LockedStreamFile locked_stream = stream_sp->Lock(); + + if (mode == EnableStatusline) { + // Move everything on the screen up. + locked_stream << '\n'; + locked_stream.Printf(ANSI_UP_ROWS, 1); + } + 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; } + + m_debugger.RefreshIOHandler(); } void Statusline::Redraw(bool update) { diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index 431f931d5f3f..4720d3b4c29a 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -1709,6 +1709,13 @@ void Editline::PrintAsync(lldb::LockableStreamFileSP stream_sp, const char *s, } } +void Editline::Refresh() { + if (!m_editline || !m_output_stream_sp) + return; + LockedStreamFile locked_stream = m_output_stream_sp->Lock(); + MoveCursor(CursorLocation::EditingCursor, CursorLocation::EditingCursor); +} + bool Editline::CompleteCharacter(char ch, EditLineGetCharType &out) { #if !LLDB_EDITLINE_USE_WCHAR if (ch == (char)EOF) diff --git a/lldb/test/API/functionalities/statusline/TestStatusline.py b/lldb/test/API/functionalities/statusline/TestStatusline.py index 68c5ad3090cf..e2c2ed548ed0 100644 --- a/lldb/test/API/functionalities/statusline/TestStatusline.py +++ b/lldb/test/API/functionalities/statusline/TestStatusline.py @@ -27,10 +27,12 @@ class TestStatusline(PExpectTest): self.expect("run", substrs=["stop reason"]) self.resize() - def resize(self): + def resize(self, height=None, width=None): + height = self.TERMINAL_HEIGHT if not height else height + width = self.TERMINAL_WIDTH if not width else width # Change the terminal dimensions. When we launch the tests, we reset # all the settings, leaving the terminal dimensions unset. - self.child.setwinsize(self.TERMINAL_HEIGHT, self.TERMINAL_WIDTH) + self.child.setwinsize(height, width) def test(self): """Basic test for the statusline.""" @@ -104,3 +106,16 @@ class TestStatusline(PExpectTest): self.resize() self.expect("set set show-statusline true", ["no target"]) + + @skipIfEditlineSupportMissing + def test_resize(self): + """Test that move the cursor when resizing.""" + self.launch(timeout=self.TIMEOUT) + self.resize() + self.expect("set set show-statusline true", ["no target"]) + self.resize(20, 60) + # Check for the newline followed by the escape code to move the cursor + # up one line. + self.child.expect(re.escape("\n\x1b[1A")) + # Check for the escape code to move the cursor back to column 8. + self.child.expect(re.escape("\x1b[8G"))