Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion src/painting/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,40 @@ impl Painter {
size
}
};
let prompt_selector = select_prompt_row(suspended_state, self.stdout.cursor_position()?);
self.anchor_prompt(suspended_state)
}

/// Establishes the prompt's start row for a new line editor invocation by
/// asking the terminal where the cursor is.
fn anchor_prompt(&mut self, suspended_state: Option<&PainterSuspendedState>) -> Result<()> {
// The terminal may not answer the cursor-position query in time
// (crossterm gives it a fixed 2s): a terminal busy repainting, a
// multiplexer briefly holding the reply, a slow remote link. That is
// a transient condition, not a broken terminal, and this was the only
// query site that still aborted `read_line` on it. Degrade instead:
// print a newline so the prompt at least starts at column 0 on a row
// of its own, keep the row as `Stale` so the next paint's drift check
// asks again (and itself tolerates no answer), and carry on.
//
// The substitute row must be the bottom of the screen, never the
// last-known row. The drift check only re-anchors when the cursor
// turns out to be *above* the cached row, so a guess can only be
// repaired if it errs high. The newline just printed has already
// moved the cursor past the last-known row in the common REPL case,
// and `clear_from_anchor` erases down from the anchor, so an
// undershooting guess wipes the output above the prompt with no way
// to recover. No row is greater than the bottom, so it is the only
// guess that always lands on the repairable side.
let position = match self.stdout.cursor_position() {
Ok(position) => position,
Err(_) => {
self.print_crlf()?;
self.prompt_start_row =
PromptStartRow::Stale(self.screen_height().saturating_sub(1));
return Ok(());
}
};
let prompt_selector = select_prompt_row(suspended_state, position);
let new_row = match prompt_selector {
PromptRowSelector::UseExistingPrompt { start_row } => start_row,
PromptRowSelector::MakeNewPrompt { new_row } => {
Expand Down Expand Up @@ -1529,6 +1562,35 @@ mod tests {
);
}

// Test writers never answer the cursor-position query (see
// `W::cursor_position`), which is exactly the terminal-didn't-reply case.
// Anchoring must then degrade rather than fail the whole `read_line`,
// and must guess the bottom row: the drift check only repairs a guess
// that errs high, and the newline printed first has already moved the
// cursor past any last-known row.
#[test]
fn test_anchor_prompt_without_answer_assumes_bottom_over_last_known_row() {
let mut painter = Painter::new(W::capture());
painter.terminal_size = (20, 10);
painter.prompt_start_row.mark_verified(4);

painter.anchor_prompt(None).unwrap();

assert_eq!(painter.prompt_start_row, PromptStartRow::Stale(9));
assert_eq!(painter.stdout.captured(), b"\r\n");
}

#[test]
fn test_anchor_prompt_without_answer_and_no_row_assumes_bottom() {
let mut painter = Painter::new(W::capture());
painter.terminal_size = (20, 10);

painter.anchor_prompt(None).unwrap();

assert_eq!(painter.prompt_start_row, PromptStartRow::Stale(9));
assert_eq!(painter.stdout.captured(), b"\r\n");
}

fn base_snapshot() -> RenderSnapshot {
RenderSnapshot {
screen_width: 20,
Expand Down
Loading