[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
This commit is contained in:
Jonas Devlieghere
2025-06-30 14:34:35 -07:00
committed by GitHub
parent 0d1392e979
commit 1eb795413d
8 changed files with 57 additions and 10 deletions

View File

@@ -227,6 +227,8 @@ public:
const char *GetIOHandlerHelpPrologue();
void RefreshIOHandler();
void ClearIOHandlers();
bool EnableLog(llvm::StringRef channel,

View File

@@ -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);

View File

@@ -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.

View File

@@ -1445,6 +1445,13 @@ bool Debugger::PopIOHandler(const IOHandlerSP &pop_reader_sp) {
return true;
}
void Debugger::RefreshIOHandler() {
std::lock_guard<std::recursive_mutex> 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<StreamAsynchronousIO>(*this,
StreamAsynchronousIO::STDOUT);

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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"))