From 39761d9f40385867121543cba2160e82b1cad50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 14 Oct 2026 17:43:36 -0400 Subject: [PATCH] T10: a match whose line is kept, grown or cut stays current when held under the same line above or in its place, before a twin line elsewhere --- AGENTS.md | 2 +- src/term/find.rs | 34 +++++++++++++++++++++++++--------- src/term/find/tests.rs | 20 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 85f99bf..9fcd272 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,6 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does; - 2026-10-14: a wheel notch scrolls 3 lines, as Ghostty's discrete wheel; a trackpad scrolls its pixels. The wheel reports to a program that tracks the mouse, sends arrows on the alternate screen under mode 1007, and scrolls history otherwise. - 2026-10-14: Cmd-C and Cmd-V copy and paste on macOS, Ctrl-Shift-C and Ctrl-Shift-V elsewhere; Cmd-press, Ctrl-press off macOS, opens an OSC 8 target or a URL Ely's `links` finds in the wrapped line. Copies drop the newline alacritty ends a whole line with, as Ghostty's do. Bracketed pastes lose ESC and Ctrl-C, as alacritty's do; plain pastes end lines with CR. Typing and pasting clear the selection. Copy on select and OSC 52 writes to `p` or `s` go to the primary selection on Linux, as Ghostty's do; macOS and Windows have none, so they go to the clipboard. - 2026-10-14: a key's release goes out only when the terminal saw its press, so a key a binding takes sends no release, as in kitty. -- 2026-10-14: find opens on Cmd-F (Ctrl-Shift-F off macOS) beside the grid, outside the `Terminal` key context, so its keys never reach the program. Matches wash in Ely's `warning` color, the current one stronger; the newest is current first, Enter steps older and Shift-Enter newer. Shift-Enter binds under `TerminalFind > ElyInput`, the context of Ely's text input, whose own Shift-Enter binding takes the key. Find reads the grid as it shows: combining marks ride their cells, hidden cells are blank, wrapped rows join, words are Unicode; alacritty's own search reads no marks and knows only ASCII word edges. New output, a resize and a settings change search again; the current match stays the same occurrence: at its place in the same line, or in one grown or cut at its end, nearest where scrolling moved it; else the nearest by line, then column. Searching and following take one pass over the text and the matches. +- 2026-10-14: find opens on Cmd-F (Ctrl-Shift-F off macOS) beside the grid, outside the `Terminal` key context, so its keys never reach the program. Matches wash in Ely's `warning` color, the current one stronger; the newest is current first, Enter steps older and Shift-Enter newer. Shift-Enter binds under `TerminalFind > ElyInput`, the context of Ely's text input, whose own Shift-Enter binding takes the key. Find reads the grid as it shows: combining marks ride their cells, hidden cells are blank, wrapped rows join, words are Unicode; alacritty's own search reads no marks and knows only ASCII word edges. New output, a resize and a settings change search again; the current match stays the same occurrence: its line kept, grown or cut at its end, held under the same line above or in its place, before such a line elsewhere, nearest where scrolling moved it; else the nearest by line, then column. Searching and following take one pass over the text and the matches. - 2026-10-14: the program hears focus in and out under mode 1004 when the terminal's focus or the window's activation changes. The window wears the program's title, the app's name while it names none. A bell is logged and makes no sound. A full reset, `ESC c`, empties alacritty's title and sends only `CursorBlinkingChange`, in the parser's order, synchronized updates too; on that event the view replays the title through `set_options`, which sends it. - 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten. diff --git a/src/term/find.rs b/src/term/find.rs index 6031bbf..682a063 100644 --- a/src/term/find.rs +++ b/src/term/find.rs @@ -192,11 +192,21 @@ impl Found { start..self.breaks.get(ends).copied().unwrap_or(self.text.len()) } - /// The match that was `then`'s `at`: at its place in its line, kept, grown or cut; else the nearest. + /// The text of the line before `line`, if one is. + fn above(&self, line: &Range) -> Option<&str> { + let end = line.start.checked_sub(1)?; + Some(&self.text[self.line(end)]) + } + + /// The match that was `then`'s `at`: its line kept, grown or cut, held under the same line or in place; else the nearest. fn follow(&self, then: &Found, at: usize) -> Option { let was = then.bytes[at].start; let line = then.line(was); - let (whole, offset) = (&then.text[line.clone()], was - line.start); + let (whole, offset, above) = ( + &then.text[line.clone()], + was - line.start, + then.above(&line), + ); let place = *then.hits[at].start(); let grew = self.history as i32 - then.history as i32; // A cleared or trimmed history moves no line; a resize pulls lines back down. @@ -209,16 +219,22 @@ impl Found { for (index, bytes) in self.bytes.iter().enumerate() { let line = self.line(bytes.start); let now = &self.text[line.clone()]; - let rank = match bytes.start - line.start == offset { - true if now == whole => 0, - true if now.starts_with(whole) || whole.starts_with(now) => 1, - _ => 2, + let kin = match bytes.start - line.start == offset { + true if now == whole => Some(0), + true if now.starts_with(whole) || whole.starts_with(now) => Some(1), + _ => None, }; let start = self.hits[index].start(); let away = (start.line.0 + scrolled - place.line.0).abs(); - let key = (rank, away, start.column.0.abs_diff(place.column.0)); - if best.is_none_or(|(least, _)| key < least) { - best = Some((key, index)); + let column = start.column.0.abs_diff(place.column.0); + let held = kin.is_some() && ((away, column) == (0, 0) || self.above(&line) == above); + let rank = match kin { + Some(kin) if held => kin, + Some(kin) => 2 + kin, + None => 4, + }; + if best.is_none_or(|(least, _)| (rank, away, column) < least) { + best = Some(((rank, away, column), index)); } } best.map(|(_, index)| index) diff --git a/src/term/find/tests.rs b/src/term/find/tests.rs index df37d1f..4b8634a 100644 --- a/src/term/find/tests.rs +++ b/src/term/find/tests.rs @@ -194,6 +194,26 @@ fn a_line_that_grows_or_changes_keeps_the_match_at_its_place() { "needle 6 itself before needle 60 in its place" ); + let mut twins = term("needle\r\nneedle", 20, 3, 10); + let then = found(&twins, "needle", PLAIN); + feed(&mut twins, "!"); + let now = found(&twins, "needle", PLAIN); + assert_eq!( + now.follow(&then, 1), + Some(1), + "the grown line, not its twin above" + ); + + let mut rewritten = term("needle\r\nA\r\nneedle", 20, 4, 10); + let then = found(&rewritten, "needle", PLAIN); + feed(&mut rewritten, "\x1b[2;1HB\x1b[3;7H!"); + let now = found(&rewritten, "needle", PLAIN); + assert_eq!( + now.follow(&then, 1), + Some(1), + "grown in its place, though the line above changed" + ); + let mut changed = term("needle A needle B", 30, 3, 10); let then = found(&changed, "needle", PLAIN); feed(&mut changed, "\rneedle X needle Y");