diff --git a/sonny/README.md b/sonny/README.md index 5b5175919..9e8c794c3 100644 --- a/sonny/README.md +++ b/sonny/README.md @@ -651,7 +651,9 @@ chat) takes them back to the composer, and Ctrl+\, or Tab with nothing typed and no guess showing, puts them in the pane again; the pane's title names the key that does. Sonny binds no Shift+Tab: multiplexers switch tabs with it. A window too narrow for -both shows the one the keys are in. Ctrl+F finds text in +both shows the one the keys are in. With nothing typed, Home goes up to +the conversation's first turn, drawing the turns over it on the way, and +End comes back to the newest. Ctrl+F finds text in the conversation, from the newest place back: ↑ or Enter steps older, ↓ newer, and Esc stops where you are. diff --git a/sonny/src/main.rs b/sonny/src/main.rs index e6bf216bb..9bfb25360 100644 --- a/sonny/src/main.rs +++ b/sonny/src/main.rs @@ -117,6 +117,11 @@ const HISTORY_ITEMS: u64 = 200; /// How much older history is drawn at a time as the view nears its top: /// a page drawn while the wheel turns, so small enough not to stall it. const OLDER_ITEMS: u64 = 50; +/// How many nodes the way up to the first turn reads at a time, cut +/// into pages of [`OLDER_ITEMS`]. +const WALKED_ITEMS: u64 = 400; +/// About how many pages the way up to the first turn reads between frames. +const WALKED_PAGES: usize = 32; /// The widest the conversation is written, however wide the window: a /// reply reads as a column, not a band across the screen. Prose reads /// best at 45 to 90 characters a line, but replies carry code, paths and @@ -1191,6 +1196,36 @@ impl Paged { } } +/// The reaction a call to react makes in this window, not another +/// agent's. +fn own_reaction(item: &Value) -> Option<(String, bool)> { + (item["arguments"].as_str()) + .and_then(|a| serde_json::from_str::(a).ok()) + .filter(|arguments| reactions::target(arguments).is_none()) + .as_ref() + .and_then(reactions::call) +} + +/// What `items` give back undrawn, as drawing them as older items would: +/// what the calls before them returned. +fn unpaired(items: impl IntoIterator) -> Vec<(i64, Value)> { + let mut calls = std::collections::HashSet::new(); + let mut unpaired = Vec::new(); + for (node, item) in items { + let id = item["call_id"].as_str().unwrap_or("").to_owned(); + match item["type"].as_str() { + Some("function_call") + if item["name"] != reactions::TOOL || own_reaction(&item).is_some() => + { + calls.insert(id); + } + Some("function_call_output") if !calls.remove(&id) => unpaired.push((node, item)), + _ => {} + } + } + unpaired +} + /// What the bot is saying: its rows stay in the viewport until what comes /// next tells whether they were a step on the way, before a call, or its /// reply, at the turn's end. Rows past what the viewport holds are written @@ -1622,6 +1657,15 @@ struct App { /// A read of the bot's items came back short: what was drawn from it /// is not all there was. short_read: std::cell::Cell, + /// Home, with nothing typed, goes on up to the first turn as the pages + /// over it are drawn, until a key or the wheel says otherwise. + to_first: bool, + /// How many rows the pages drawn took, and for how many items: how + /// tall a page not drawn yet is guessed to be. + page_rows: (usize, usize), + /// Ctrl+F steps to the next place, older or newer, once the pages let + /// go before it are drawn again: those looked through so far. + find_step: Option<(bool, Vec)>, /// The window's width, the pane's included. full_width: u16, /// The pane beside the conversation on the full screen, with what the @@ -2338,6 +2382,9 @@ impl App { pages: HashMap::new(), next_page: 0, short_read: std::cell::Cell::new(false), + to_first: false, + page_rows: (0, 0), + find_step: None, full_width: 0, pane: None, pane_keys: false, @@ -2412,20 +2459,7 @@ impl App { self.margin = margin::Margin::load(self.reactions_key.as_deref()); self.picked = reactions::Picked::load(self.reactions_key.as_deref()); self.history().await; - // Known to start the conversation only once its newest page is - // read, the welcome goes above it. - let mut welcome = self.welcome(); - if !welcome.is_empty() { - if self.any_history { - welcome.push(Line::default()); - } - self.any_history = true; - // What marks the history's rows moves down with them. - for (at, _) in &mut self.marks { - *at += welcome.len(); - } - self.pending.splice(0..0, welcome); - } + self.welcome_above(); // A turn already running: its clock, and any call it waits on. if let Some(turn) = self.running.as_ref().map(|r| r.turn) { status_report("working", &self.bot); @@ -2575,8 +2609,43 @@ impl App { if self.older.is_some() && self.screen.near_top() { self.older().await; } - // A page let go drawn again before the view reaches it. - if let Some(page) = self.screen.wanted() { + if self.to_first { + self.screen.scroll(isize::MAX); + self.to_first = self.repaint || self.older.is_some() && self.screen.near_top(); + // The next page asked for once this one is drawn, not a tick on. + if self.to_first && !self.repaint { + self.woken.notify_one(); + } + } + if let Some((older, looked)) = &mut self.find_step { + let older = *older; + match (self.screen.find_gap(older, looked)).filter(|_| self.finding.is_some()) { + // A page at a time, the screen drawn between. + Some(page) => { + looked.push(page); + self.draw_again(page).await; + // A read cut short steps over what it did not draw. + match self.short_read.get() { + true => { + self.find_step = None; + self.screen.find_next(older); + } + false => self.woken.notify_one(), + } + } + None => { + self.find_step = None; + self.screen.find_next(older); + } + } + } + // A page let go drawn again before the view reaches it, or + // once the way up to the first turn, or a step finding, is + // done: never letting go a page found in before it is shown. + if !self.to_first + && self.find_step.is_none() + && let Some(page) = self.screen.wanted() + { self.draw_again(page).await; } self.jumped().await; @@ -2921,60 +2990,113 @@ impl App { let unpaired = std::mem::take(&mut self.unpaired); self.history_since(newest, true).await; self.unpaired = unpaired; - self.screen.blank(pages); + self.screen.prepend_blank(pages); return Ok(()); } - let welcome = self.welcome(); - self.commit(welcome); // Back to the first node this window drew, not only the newest page. match self.first_node { - Some(first) => self.history_since(first - 1, true).await, - None => self.history().await, + Some(first) => { + let welcome = self.welcome(); + self.commit(welcome); + self.history_since(first - 1, true).await; + } + None => { + self.history().await; + self.welcome_above(); + } } Ok(()) } /// The page of items before the oldest drawn, drawn on its own and /// put above the rest, the view left where it is: the conversation - /// drawn so far is not drawn again, and a turn running goes on. + /// drawn so far is not drawn again, and a turn running goes on. On + /// the way up to the first turn, the pages over it are blank rows as + /// tall as they may be, a few at a time, drawn once the view nears + /// them. async fn older(&mut self) { - let Some(from) = self.older else { - return; - }; - let asked = json!({"bot": self.bot, "from": from, "limit": OLDER_ITEMS}); - let page = match self.client.request("history_nodes", asked).await { - Ok(page) => page, - Err(error) => { - self.notice = Some(error.to_string()); - return; + // Newest first, those on the way up, by their ids and rows. + let mut blanks: Vec<(u64, usize)> = Vec::new(); + while let Some(from) = self.older + && blanks.len() < WALKED_PAGES + { + // On the way up, as many nodes as a read gives, cut into pages. + let limit = match self.to_first { + true => WALKED_ITEMS, + false => OLDER_ITEMS, + }; + let asked = json!({"bot": self.bot, "from": from, "limit": limit}); + let page = match self.client.request("history_nodes", asked).await { + Ok(page) => page, + Err(error) => { + self.notice = Some(error.to_string()); + // Not asked again at once: the way up stops. + self.to_first = false; + break; + } + }; + let mut nodes: Vec = Vec::new(); + let mut turns: HashMap = HashMap::new(); + for n in page["nodes"].as_array().into_iter().flatten() { + let Some(node) = n["node"].as_i64() else { + continue; + }; + nodes.push(node); + if let Some(turn) = n["turn"].as_i64() { + turns.insert(node, turn); + } } - }; - let mut nodes: Vec = Vec::new(); - let mut turns: HashMap = HashMap::new(); - for n in page["nodes"].as_array().into_iter().flatten() { - let Some(node) = n["node"].as_i64() else { - continue; + nodes.reverse(); + self.older = page["next_from"].as_i64(); + if nodes.is_empty() { + self.older = None; + break; + } + self.short_read.set(false); + let mut items = match self.to_first { + true => self.items_at(&nodes).await, + false => Vec::new(), }; - nodes.push(node); - if let Some(turn) = n["turn"].as_i64() { - turns.insert(node, turn); + // A read cut short: the rest of the way drawn a page at a time. + if self.short_read.get() { + self.older = Some(from); + self.to_first = false; + break; + } + while !nodes.is_empty() { + let cut = nodes.len().saturating_sub(OLDER_ITEMS as usize); + let ours = nodes.split_off(cut); + self.first_node = Some(ours[0]); + let turns = (ours.iter()) + .filter_map(|node| Some((*node, *turns.get(node)?))) + .collect(); + let after = std::mem::take(&mut self.unpaired); + let first = self.older.is_none() && nodes.is_empty(); + let paged = Paged::new(ours, turns, &after, first); + let id = self.next_page; + self.next_page += 1; + let cut = items.partition_point(|(node, _)| *node < paged.nodes[0]); + let read = items.split_off(cut); + if !self.to_first || paged.first { + blanks.reverse(); + self.screen.prepend_blank(blanks); + let (older, unpaired) = self.draw_page(&paged, after).await; + self.unpaired = unpaired; + self.page_rows.0 += older.rows(); + self.page_rows.1 += paged.nodes.len(); + self.pages.insert(id, paged); + self.screen.prepend(id, older); + return; + } + // Only read: what its calls returned is all drawing it gives. + self.unpaired = unpaired(read.into_iter().chain(after)); + let (rows, items) = self.page_rows; + blanks.push((id, paged.nodes.len() * rows.max(1) / items.max(1))); + self.pages.insert(id, paged); } } - nodes.reverse(); - self.older = page["next_from"].as_i64(); - let Some(&oldest) = nodes.first() else { - self.older = None; - return; - }; - self.first_node = Some(oldest); - let after = std::mem::take(&mut self.unpaired); - let paged = Paged::new(nodes, turns, &after, self.older.is_none()); - let (older, unpaired) = self.draw_page(&paged, after).await; - self.unpaired = unpaired; - let id = self.next_page; - self.next_page += 1; - self.pages.insert(id, paged); - self.screen.prepend(id, older); + blanks.reverse(); + self.screen.prepend_blank(blanks); } /// The page `id`, let go as the view moved away, drawn again as it @@ -3804,7 +3926,23 @@ impl App { } } - /// Commit a finished cell, a blank line apart from the one before. + /// The welcome over the newest page just read, known to start the + /// conversation only once it is. + fn welcome_above(&mut self) { + let mut welcome = self.welcome(); + if !welcome.is_empty() { + if self.any_history { + welcome.push(Line::default()); + } + self.any_history = true; + // What marks the history's rows moves down with them. + for (at, _) in &mut self.marks { + *at += welcome.len(); + } + self.pending.splice(0..0, welcome); + } + } + /// The welcome, when this window has one and draws the conversation /// from its first item: none above a page with older ones before it. fn welcome(&self) -> Vec> { @@ -3814,6 +3952,7 @@ impl App { } } + /// Commit a finished cell, a blank line apart from the one before. fn commit(&mut self, lines: Vec>) { self.commit_after(lines, true); } @@ -4074,13 +4213,7 @@ impl App { // Counted once it is known to have been taken. // One naming another agent is drawn in that agent's window. (Some("function_call"), _) if item["name"] == reactions::TOOL => { - if let Some(reaction) = item["arguments"] - .as_str() - .and_then(|a| serde_json::from_str::(a).ok()) - .filter(|arguments| reactions::target(arguments).is_none()) - .as_ref() - .and_then(reactions::call) - { + if let Some(reaction) = own_reaction(&item) { let id = item["call_id"].as_str().unwrap_or("").to_owned(); self.react_calls.insert(id, reaction); } @@ -5570,6 +5703,20 @@ impl App { /// What the full screen takes before the rest: the mouse, a copy of /// the selection, and the keys that scroll. Whether it took `event`. fn full_screen(&mut self, event: &Event) -> bool { + use ratatui::crossterm::event::MouseEventKind; + // A key, or the wheel, ends the way up to the first turn. + match event { + Event::Key(key) if key.kind != KeyEventKind::Release => self.to_first = false, + Event::Mouse(mouse) + if matches!( + mouse.kind, + MouseEventKind::ScrollUp | MouseEventKind::ScrollDown + ) => + { + self.to_first = false + } + _ => {} + } let key = match event { // A paste while finding is more to find. // A paste in the pane's program is its own. @@ -5589,6 +5736,7 @@ impl App { Event::Paste(text) if let Some(query) = &mut self.finding => { query.extend(text.chars().map(|c| if c.is_control() { ' ' } else { c })); self.screen.find(query); + self.find_step = None; return true; } Event::Mouse(mouse) => { @@ -5819,23 +5967,29 @@ impl App { match key.code { KeyCode::Esc => self.finding = None, _ if copy => self.finding = None, - KeyCode::Up | KeyCode::Enter => self.screen.find_next(true), - KeyCode::Char('f' | 'p') if ctrl => self.screen.find_next(true), - KeyCode::Down => self.screen.find_next(false), - KeyCode::Char('n') if ctrl => self.screen.find_next(false), + // The pages let go on the way drawn first, so what they + // hold is found too. + KeyCode::Up | KeyCode::Enter => self.find_step = Some((true, Vec::new())), + KeyCode::Char('f' | 'p') if ctrl => self.find_step = Some((true, Vec::new())), + KeyCode::Down => self.find_step = Some((false, Vec::new())), + KeyCode::Char('n') if ctrl => self.find_step = Some((false, Vec::new())), + // Another text: a step for the last one goes no further. KeyCode::Backspace => { query.pop(); self.screen.find(query); + self.find_step = None; } KeyCode::Char(c) if !ctrl => { query.push(c); self.screen.find(query); + self.find_step = None; } KeyCode::PageUp => self.page_up(), KeyCode::PageDown => self.screen.page(false), _ => {} } if self.finding.is_none() { + self.find_step = None; self.screen.end_find(); } return true; @@ -5865,6 +6019,28 @@ impl App { KeyCode::Esc if self.screen.unselect() || self.input.unselect() => {} KeyCode::PageUp => self.page_up(), KeyCode::PageDown => self.screen.page(false), + // With nothing typed, Home goes to the first turn, the pages + // over it drawn on the way, and End back to the newest. + KeyCode::Home | KeyCode::End + if self.input.is_empty() + && self.picker.is_none() + && self.choosing.is_none() + && self.reacting.is_none() => + { + self.to_first = key.code == KeyCode::Home; + match self.to_first { + // The oldest rows went to make room: drawn anew from + // the newest page, the way up starts over. + true if self.screen.trimmed() => { + self.first_node = None; + self.older = None; + self.unpaired.clear(); + self.repaint = true; + } + true => self.screen.scroll(isize::MAX), + false => self.screen.latest(), + } + } // Sending goes back to the newest rows, where its answer comes. KeyCode::Enter => { self.screen.latest(); @@ -7537,6 +7713,7 @@ impl App { frame.render_widget(Paragraph::new(line), composer); frame.set_cursor_position((composer.x + 5 + query.width() as u16, composer.y)); let place = match self.screen.found() { + _ if self.find_step.is_some() => " looking further · ".to_owned(), Some((_, 0)) if !query.is_empty() => " not found · ".to_owned(), Some((at, count)) if count > 0 => format!(" {at} of {count} · "), _ => " ".to_owned(), @@ -8282,6 +8459,29 @@ fn responses(item: &Value) -> Vec { mod tests { use super::*; + #[test] + fn a_page_only_read_gives_back_what_drawing_it_would() { + let call = |id: &str, name: &str, arguments: &str| json!({"type": "function_call", "call_id": id, "name": name, "arguments": arguments}); + let result = |id: &str| json!({"type": "function_call_output", "call_id": id}); + let items = vec![ + (1, result("before")), + (2, call("ran", "shell", "{}")), + (3, result("ran")), + (4, call("here", "react", r#"{"emoji":"👍"}"#)), + (5, call("there", "react", r#"{"emoji":"👍","bot":"other"}"#)), + (6, result("here")), + (6, result("there")), + ]; + let after = vec![(9, result("newer"))]; + let back: Vec = unpaired(items.into_iter().chain(after)) + .into_iter() + .map(|(node, _)| node) + .collect(); + // A result before its call, one for another agent's reaction, and + // one a newer page could not pair are for older pages. + assert_eq!(back, vec![1, 6, 9]); + } + #[test] fn another_projects_lead_and_its_tasks_are_never_tasks_here() { let lineage: std::collections::HashSet = ["ws.lead".into(), "ws.fix".into()].into(); diff --git a/sonny/src/screen.rs b/sonny/src/screen.rs index 4dc4fa2a3..f98b3b4fb 100644 --- a/sonny/src/screen.rs +++ b/sonny/src/screen.rs @@ -535,6 +535,11 @@ impl Screen { self.back = 0; } + /// How many rows it holds, wrapped or not. + pub fn rows(&self) -> usize { + self.rows.len() + } + /// How many rows the view is up from the newest. pub fn back(&self) -> usize { self.back @@ -546,6 +551,11 @@ impl Screen { self.back = back; } + /// The oldest rows went to make room, and those above are not drawn. + pub fn trimmed(&self) -> bool { + self.trimmed + } + /// The view is a few screens from the first row, or nearer, and the /// rows above it can still be drawn: time to ask for older ones. pub fn near_top(&self) -> bool { @@ -570,7 +580,44 @@ impl Screen { ); self.placed = None; self.swap(0, older.rows); - self.let_go(); + self.let_go(None); + } + + /// Blank rows above the first row standing in for `pages`, from the + /// first, each as many rows as it says, until the view nears it. + pub fn prepend_blank(&mut self, pages: Vec<(u64, usize)>) { + if pages.is_empty() { + return; + } + let pages: Vec = (pages.into_iter()) + .map(|(id, rows)| Page { + id, + rows: rows.max(1), + drawn: false, + }) + .collect(); + let rows = pages.iter().map(|page| page.rows).sum(); + // One page as tall as them all, then each its own. + self.pages.insert( + 0, + Page { + id: 0, + rows: 0, + drawn: false, + }, + ); + self.placed = None; + let blank = Arc::new(Line::default()); + let rows = (0..rows).map(|_| Row { + line: blank.clone(), + rest: None, + link: None, + tag: None, + buttons: Vec::new(), + }); + self.swap(0, rows.collect()); + self.pages.splice(0..1, pages); + self.placed = None; } /// The page `id`, let go before, drawn again with the rows `older` was @@ -581,7 +628,7 @@ impl Screen { }; self.pages[page].drawn = true; self.swap(page, older.rows); - self.let_go(); + self.let_go(Some(page)); } /// The pages of older items, from the first, and how many rows each @@ -593,44 +640,6 @@ impl Screen { .collect() } - /// Blank rows over the rest for each of `pages`, as tall as it was, - /// to be drawn again as the view nears it: the view stays as many - /// rows up from the newest. - pub fn blank(&mut self, pages: Vec<(u64, usize)>) { - let blank = Arc::new(Line::default()); - let rows: usize = pages.iter().map(|(_, rows)| rows).sum(); - self.rows.splice( - 0..0, - (0..rows).map(|_| Row { - line: blank.clone(), - rest: None, - link: None, - tag: None, - buttons: Vec::new(), - }), - ); - // Rows wrapped already are wrapped again under them. - self.shown.clear(); - self.wrapped = 0; - self.relaid = 0; - self.selection = None; - self.linked = None; - for spot in [&mut self.go_to, &mut self.keep].into_iter().flatten() { - spot.0 += rows; - } - if let Some(top) = &mut self.top_row { - *top += rows; - } - let pages = pages.into_iter().map(|(id, rows)| Page { - id, - rows, - drawn: false, - }); - self.pages.splice(0..0, pages); - self.placed = None; - self.laid += 1; - } - /// A page let go that the view is near, the nearest: to draw again. pub fn wanted(&mut self) -> Option { let height = self.area.height as usize; @@ -730,8 +739,9 @@ impl Screen { } /// Past what pages keep drawn, those farthest from the view let go, - /// each for blank rows as tall, so nothing moves. - fn let_go(&mut self) { + /// each for blank rows as tall, so nothing moves; never `kept`, just + /// drawn. + fn let_go(&mut self, kept: Option) { let placed = self.placed(); let drawn = |pages: &[Page]| -> usize { (pages.iter().zip(&placed)) @@ -748,7 +758,9 @@ impl Screen { let mut far: Vec = (0..self.pages.len()) .filter(|&p| { let at = &placed[p]; - self.pages[p].drawn && !(at.start < near.end && near.start < at.end) + self.pages[p].drawn + && Some(p) != kept + && !(at.start < near.end && near.start < at.end) }) .collect(); far.sort_by_key(|&p| std::cmp::Reverse(placed[p].start.abs_diff(bottom))); @@ -995,6 +1007,7 @@ impl Screen { /// The next place the text found is: older, or newer, round to the /// other end past the last. pub fn find_next(&mut self, older: bool) { + let bottom = self.shown.len() - self.back.min(self.shown.len()); let Some(finding) = &mut self.finding else { return; }; @@ -1002,13 +1015,73 @@ impl Screen { if count == 0 { return; } - finding.at = match older { - true => (finding.at + count - 1) % count, - false => (finding.at + 1) % count, + let above = (finding.hits).partition_point(|(start, _)| start.0 < bottom); + finding.at = match (older, finding.at < count) { + (true, true) => (finding.at + count - 1) % count, + (false, true) => (finding.at + 1) % count, + // No place yet, as when none was until pages were drawn: the + // nearest the view, that way. + (true, false) => (above + count - 1) % count, + (false, false) => above % count, }; self.reveal(); } + /// A page let go between the place found and the next one, `older` + /// or newer, round past the last as stepping goes, the nearest it, + /// but those `looked` through already: to draw before stepping, so + /// what it holds is found too. + pub fn find_gap(&mut self, older: bool, looked: &[u64]) -> Option { + // Nothing looked for, nothing to draw for it. + let finding = (self.finding.as_ref()) + .filter(|f| !f.query.is_empty() && f.at != AGAIN && f.laid == self.laid)?; + let bottom = self.shown.len() - self.back.min(self.shown.len()); + let starts: Vec = finding.hits.iter().map(|(start, _)| start.0).collect(); + let at = finding.at; + // The place shown and those either side; with none, the view's. + let (here, prev, next) = match starts.get(at) { + Some(&here) => ( + here, + at.checked_sub(1).map(|at| starts[at]), + starts.get(at + 1).copied(), + ), + None => ( + bottom, + starts.iter().rev().find(|s| **s < bottom).copied(), + starts.iter().find(|s| **s >= bottom).copied(), + ), + }; + let end = self.shown.len(); + // Up to the next place; past the last, round from the other end. + let ranges = match older { + true => [ + (prev.unwrap_or(0), here), + match prev { + Some(_) => (end, end), + None => (starts.last().copied().unwrap_or(here), end), + }, + ], + false => [ + (here, next.unwrap_or(end)), + match next { + Some(_) => (0, 0), + None => (0, starts.first().copied().unwrap_or(here)), + }, + ], + }; + let placed = self.placed(); + ranges.into_iter().find_map(|(from, to)| { + let gaps = (self.pages.iter().zip(&placed)) + .filter(|(page, at)| !page.drawn && at.start < to && from < at.end) + .filter(|(page, _)| !looked.contains(&page.id)); + let nearest = match older { + true => gaps.max_by_key(|(_, at)| at.start), + false => gaps.min_by_key(|(_, at)| at.start), + }; + nearest.map(|(page, _)| page.id) + }) + } + /// Stop finding, the view left where it is. pub fn end_find(&mut self) { self.finding = None; @@ -2259,7 +2332,7 @@ mod tests { // rows as tall as they were, the view on the newest. screen.clear(); screen.push(lines(20_000..20_050, "new")); - screen.blank(pages); + screen.prepend_blank(pages); let buf = drawn(&mut screen, area); assert_eq!(row(&buf, 4).trim_end(), "new 20049"); assert_eq!(screen.height(20), 9_050); @@ -2279,6 +2352,102 @@ mod tests { assert_eq!(row(&buf, 0).trim_end(), "row 0"); } + #[test] + fn pages_put_above_blank_wait_for_the_view_and_are_drawn_in_place() { + let area = Rect::new(0, 0, 20, 5); + let (mut screen, _) = screen(rows(20_000..20_050), 20, 5); + drawn(&mut screen, area); + // Guessed at 100 rows each; the view goes up to the first. + screen.prepend_blank(vec![(1, 100), (2, 100), (3, 100)]); + let buf = drawn(&mut screen, area); + assert_eq!(row(&buf, 4).trim_end(), "row 20049"); + assert_eq!(screen.height(20), 350); + screen.scroll(isize::MAX); + drawn(&mut screen, area); + assert_eq!(screen.wanted(), Some(1)); + // Drawn shorter than guessed, at the top all the same. + screen.fill(1, written(rows(0..30))); + let buf = drawn(&mut screen, area); + assert_eq!(row(&buf, 0).trim_end(), "row 0"); + assert_eq!(screen.height(20), 280); + assert_eq!(screen.wanted(), Some(2)); + } + + #[test] + fn find_draws_the_pages_let_go_on_the_way_to_the_next_place() { + let area = Rect::new(0, 0, 20, 200); + let (mut screen, _) = screen(lines(0..50, "tail"), 20, 200); + screen.prepend(1, written(lines(0..7_000, "middle"))); + screen.prepend(2, written(lines(0..7_000, "old"))); + drawn(&mut screen, area); + assert!(!screen.pages[0].drawn && screen.pages[1].drawn); + screen.find("old 1"); + drawn(&mut screen, area); + // Nothing found in the rows drawn; either way round, the page let go is. + assert_eq!(screen.found(), Some((0, 0))); + assert_eq!(screen.find_gap(false, &[]), Some(2)); + assert_eq!(screen.find_gap(true, &[]), Some(2)); + screen.fill(2, written(lines(0..7_000, "old"))); + drawn(&mut screen, area); + assert!(screen.pages[0].drawn); + assert_eq!(screen.find_gap(true, &[2]), None); + screen.find_next(true); + let buf = drawn(&mut screen, area); + assert!((0..200).any(|y| row(&buf, y).starts_with("old 1"))); + // Looked through already, a page let go again is not again. + assert_eq!(screen.find_gap(true, &[2]), None); + } + + #[test] + fn find_from_nothing_found_lands_on_the_nearest_place_that_way() { + let area = Rect::new(0, 0, 20, 200); + for (older, place) in [(false, 1), (true, 11)] { + let (mut screen, _) = screen(lines(0..50, "tail"), 20, 200); + screen.prepend(1, written(lines(0..7_000, "middle"))); + screen.prepend(2, written(lines(0..7_000, "old"))); + drawn(&mut screen, area); + // Nothing typed yet, nothing to draw for it. + screen.find(""); + drawn(&mut screen, area); + assert_eq!(screen.find_gap(older, &[]), None); + screen.find("old 699"); + drawn(&mut screen, area); + assert_eq!(screen.found(), Some((0, 0))); + assert_eq!(screen.find_gap(older, &[]), Some(2)); + screen.fill(2, written(lines(0..7_000, "old"))); + drawn(&mut screen, area); + assert_eq!(screen.find_gap(older, &[2]), None); + // Newer than the view, round from the first row: the oldest + // place; older, the newest above it. + screen.find_next(older); + assert_eq!(screen.found(), Some((place, 11))); + } + } + + #[test] + fn find_goes_round_through_the_pages_let_go_past_the_last_place() { + let area = Rect::new(0, 0, 20, 200); + let (mut screen, _) = screen(lines(0..50, "tail"), 20, 200); + screen.prepend(1, written(lines(0..7_000, "middle"))); + screen.prepend(2, written(lines(0..7_000, "tail"))); + drawn(&mut screen, area); + assert!(!screen.pages[0].drawn); + screen.find("tail 7"); + drawn(&mut screen, area); + assert_eq!(screen.found(), Some((1, 1))); + // Newer than the newest place, round from the first row. + assert_eq!(screen.find_gap(false, &[]), Some(2)); + // Older than the oldest, round from the newest row: none below. + assert_eq!(screen.find_gap(true, &[]), Some(2)); + screen.fill(2, written(lines(0..7_000, "tail"))); + drawn(&mut screen, area); + assert_eq!(screen.find_gap(false, &[2]), None); + screen.find_next(false); + let buf = drawn(&mut screen, area); + assert!((0..200).any(|y| row(&buf, y).trim_end() == "tail 7")); + assert_eq!(screen.found().map(|(at, _)| at), Some(1)); + } + #[test] fn a_picked_line_is_kept_in_view_with_rows_under_it() { let lines: Vec<_> = (0..20).map(|i| Line::from(format!("row {i}"))).collect();