feat(ui): horizontal-scroll long input so the cursor stays visible (#23, ADR-0046 DA3)
A command longer than the input field used to clip silently at the right edge, hiding the cursor and the command tail. Now the single logical input line scrolls horizontally to keep the cursor in view, with muted `<` / `>` markers at the reserved edge columns signalling hidden content on either side. The offset is a pure function of (line length, cursor column, field width, previous offset) — input_scroll_offset — so the view only moves when the cursor would leave the window, and one column is held on each side for the markers so a marker never hides the cursor. The stored App::input_scroll_offset resets when the buffer is replaced wholesale (submit, history recall). The ADR-0027 6-column indicator reserve is preserved. Tests: pure-offset cases, tail-visible + head-visible render checks, and the reset-on-submit/history check. One layout snapshot now shows a long command's tail instead of its clipped head.
This commit is contained in:
+30
@@ -237,6 +237,11 @@ pub struct App {
|
||||
/// Byte offset into `input` where the next character will be
|
||||
/// inserted. Always lies on a UTF-8 character boundary.
|
||||
pub input_cursor: usize,
|
||||
/// First visible display column of the input line when it is too
|
||||
/// long to fit the input panel (ADR-0046 DA3). The renderer keeps
|
||||
/// the cursor in view by adjusting this; it resets to 0 whenever the
|
||||
/// buffer is replaced wholesale (submit / history navigation).
|
||||
pub input_scroll_offset: usize,
|
||||
pub output: VecDeque<OutputLine>,
|
||||
pub hint: Option<String>,
|
||||
/// The validity indicator's currently-visible verdict
|
||||
@@ -439,6 +444,7 @@ impl App {
|
||||
messages_verbosity: crate::friendly::Verbosity::default(),
|
||||
input: String::new(),
|
||||
input_cursor: 0,
|
||||
input_scroll_offset: 0,
|
||||
output: VecDeque::with_capacity(OUTPUT_CAPACITY),
|
||||
hint: None,
|
||||
input_indicator: None,
|
||||
@@ -1232,6 +1238,7 @@ impl App {
|
||||
self.history_cursor = Some(next_index);
|
||||
self.input = self.history[next_index].clone();
|
||||
self.input_cursor = self.input.len();
|
||||
self.input_scroll_offset = 0;
|
||||
}
|
||||
|
||||
/// Move forwards in history (towards newer entries; eventually
|
||||
@@ -1250,6 +1257,7 @@ impl App {
|
||||
self.input = self.history_draft.take().unwrap_or_default();
|
||||
}
|
||||
self.input_cursor = self.input.len();
|
||||
self.input_scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn cancel_history_navigation(&mut self) {
|
||||
@@ -1284,6 +1292,7 @@ impl App {
|
||||
fn submit(&mut self) -> Vec<Action> {
|
||||
let raw = std::mem::take(&mut self.input);
|
||||
self.input_cursor = 0;
|
||||
self.input_scroll_offset = 0;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -5089,6 +5098,27 @@ mod tests {
|
||||
assert_eq!(app.input_cursor, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_scroll_offset_resets_when_the_buffer_is_replaced() {
|
||||
// ADR-0046 DA3: the horizontal scroll offset must not leak from
|
||||
// one command to the next. Submitting and recalling from history
|
||||
// both replace the buffer wholesale, so both reset it.
|
||||
let mut app = App::new();
|
||||
type_str(&mut app, "a long command line that would have scrolled");
|
||||
app.input_scroll_offset = 25;
|
||||
submit(&mut app);
|
||||
assert_eq!(app.input_scroll_offset, 0, "submit resets the input scroll");
|
||||
|
||||
// Recall the submitted line from history — also a reset.
|
||||
type_str(&mut app, "another draft line entirely");
|
||||
app.input_scroll_offset = 25;
|
||||
app.update(key(KeyCode::Up));
|
||||
assert_eq!(
|
||||
app.input_scroll_offset, 0,
|
||||
"history recall resets the input scroll"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_up_scrolls_output_back() {
|
||||
let mut app = App::new();
|
||||
|
||||
Reference in New Issue
Block a user