T10: find reads the grid as it shows, through Ely's find_all: combining marks, hidden cells blank, wrapped rows joined, Unicode words; new output, a resize and a settings change search again, and the current match follows its line text; a full reset replays the title through set_options
This commit is contained in:
10 files changed
+335
-103
No files matched your search
+144
-86
@@ -1,13 +1,16 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use alacritty_terminal::{
|
||||
grid::Dimensions,
|
||||
index::{Column, Direction, Point},
|
||||
index::{Column, Line, Point},
|
||||
term::{
|
||||
Term,
|
||||
search::{Match, RegexIter, RegexSearch},
|
||||
cell::{Cell, Flags},
|
||||
search::Match,
|
||||
},
|
||||
};
|
||||
use ely_gpui_component::{
|
||||
editor::{FindOptions, FindWidget},
|
||||
editor::{FindOptions, FindWidget, find_all},
|
||||
forms::{InputEvent, TextInput},
|
||||
};
|
||||
use gpui::{
|
||||
@@ -24,7 +27,7 @@ use super::{
|
||||
pub struct Finder {
|
||||
query: Entity<TextInput>,
|
||||
options: FindOptions,
|
||||
hits: Vec<Match>,
|
||||
found: Found,
|
||||
current: Option<usize>,
|
||||
error: Option<SharedString>,
|
||||
_input: Subscription,
|
||||
@@ -35,7 +38,8 @@ impl Finder {
|
||||
pub fn lit<T>(&self, term: &Term<T>, color: Hsla) -> Vec<(Match, Hsla)> {
|
||||
let offset = term.grid().display_offset() as i32;
|
||||
let (top, bottom) = (-offset, term.screen_lines() as i32 - 1 - offset);
|
||||
self.hits
|
||||
self.found
|
||||
.hits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, hit)| hit.end().line.0 >= top && hit.start().line.0 <= bottom)
|
||||
@@ -47,28 +51,122 @@ impl Finder {
|
||||
}
|
||||
}
|
||||
|
||||
/// `query` under `options` as the grid's search reads it; word edges are ASCII, which its lazy DFA needs.
|
||||
fn pattern(query: &str, options: FindOptions) -> String {
|
||||
let body = if options.regex {
|
||||
query.to_string()
|
||||
} else {
|
||||
regex::escape(query)
|
||||
};
|
||||
let body = if options.word {
|
||||
format!(r"(?-u:\b)(?:{body})(?-u:\b)")
|
||||
} else {
|
||||
body
|
||||
};
|
||||
let case = if options.case { "(?-i)" } else { "(?i)" };
|
||||
format!("{case}{body}")
|
||||
/// One search of the grid: its shown text, and each match as cells and as bytes of that text.
|
||||
#[derive(Default)]
|
||||
struct Found {
|
||||
text: String,
|
||||
hits: Vec<Match>,
|
||||
bytes: Vec<Range<usize>>,
|
||||
/// The grid's history then, to follow lines that scrolled since.
|
||||
history: usize,
|
||||
}
|
||||
|
||||
/// Every match of `pattern` in the scrollback and the screen, oldest first.
|
||||
fn search_all<T>(term: &Term<T>, pattern: &str) -> Result<Vec<Match>, String> {
|
||||
let mut regex = RegexSearch::new(pattern).map_err(|error| error.to_string())?;
|
||||
let start = Point::new(term.topmost_line(), Column(0));
|
||||
let end = Point::new(term.bottommost_line(), term.last_column());
|
||||
Ok(RegexIter::new(start, end, Direction::Right, term, &mut regex).collect())
|
||||
const SPACERS: Flags = Flags::WIDE_CHAR_SPACER.union(Flags::LEADING_WIDE_CHAR_SPACER);
|
||||
|
||||
/// What a cell shows: its character and marks; a hidden cell shows a space.
|
||||
fn shown(cell: &Cell) -> impl Iterator<Item = char> + '_ {
|
||||
let hidden = cell.flags.contains(Flags::HIDDEN);
|
||||
let marks = cell.zerowidth().filter(|_| !hidden);
|
||||
std::iter::once(if hidden { ' ' } else { cell.c }).chain(marks.into_iter().flatten().copied())
|
||||
}
|
||||
|
||||
/// The grid's shown text, oldest row first, lines ended by newlines; and each row's first byte.
|
||||
fn grid_text<T>(term: &Term<T>) -> (String, Vec<(usize, Line)>) {
|
||||
let (mut text, mut rows) = (String::new(), Vec::new());
|
||||
for line in term.topmost_line().0..=term.bottommost_line().0 {
|
||||
let row = &term.grid()[Line(line)];
|
||||
let cells = (0..term.columns()).map(|column| &row[Column(column)]);
|
||||
let wraps = row[term.last_column()].flags.contains(Flags::WRAPLINE);
|
||||
let end = if wraps {
|
||||
term.columns()
|
||||
} else {
|
||||
let blank = |cell: &Cell| shown(cell).eq([' ']);
|
||||
cells
|
||||
.clone()
|
||||
.rposition(|cell| !blank(cell))
|
||||
.map_or(0, |at| at + 1)
|
||||
};
|
||||
rows.push((text.len(), Line(line)));
|
||||
for cell in cells
|
||||
.take(end)
|
||||
.filter(|cell| !cell.flags.intersects(SPACERS))
|
||||
{
|
||||
text.extend(shown(cell));
|
||||
}
|
||||
if !wraps {
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
(text, rows)
|
||||
}
|
||||
|
||||
/// The cell showing byte `offset` of the grid's text; a line's end maps to the cell after its text.
|
||||
fn cell_at<T>(term: &Term<T>, rows: &[(usize, Line)], offset: usize) -> Point {
|
||||
let (mut end, line) = rows[rows.partition_point(|(start, _)| *start <= offset) - 1];
|
||||
let row = &term.grid()[line];
|
||||
for column in 0..term.columns() {
|
||||
let cell = &row[Column(column)];
|
||||
if !cell.flags.intersects(SPACERS) {
|
||||
end += shown(cell).map(char::len_utf8).sum::<usize>();
|
||||
}
|
||||
if offset < end {
|
||||
return Point::new(line, Column(column));
|
||||
}
|
||||
}
|
||||
Point::new(line, term.last_column())
|
||||
}
|
||||
|
||||
impl Found {
|
||||
/// Every match of `query`, oldest first; a pattern that does not compile is an error.
|
||||
fn search<T>(term: &Term<T>, query: &str, options: FindOptions) -> Result<Self, String> {
|
||||
let history = term.grid().history_size();
|
||||
if query.is_empty() {
|
||||
return Ok(Self {
|
||||
history,
|
||||
..Self::default()
|
||||
});
|
||||
}
|
||||
let (text, rows) = grid_text(term);
|
||||
let bytes = find_all(&text, query, options)?;
|
||||
let hits = bytes
|
||||
.iter()
|
||||
.map(|range| cell_at(term, &rows, range.start)..=cell_at(term, &rows, range.end - 1))
|
||||
.collect();
|
||||
Ok(Self {
|
||||
text,
|
||||
hits,
|
||||
bytes,
|
||||
history,
|
||||
})
|
||||
}
|
||||
|
||||
/// The bytes of the line holding byte `offset`.
|
||||
fn line(&self, offset: usize) -> Range<usize> {
|
||||
let start = self.text[..offset].rfind('\n').map_or(0, |at| at + 1);
|
||||
let end = self.text[offset..]
|
||||
.find('\n')
|
||||
.map_or(self.text.len(), |at| offset + at);
|
||||
start..end
|
||||
}
|
||||
|
||||
/// The match that was `then`'s `at`: same line and place, nearest its scrolled spot; else the nearest.
|
||||
fn follow(&self, then: &Found, at: usize) -> Option<usize> {
|
||||
let was = then.bytes[at].start;
|
||||
let line = then.line(was);
|
||||
let (text, offset) = (&then.text[line.clone()], was - line.start);
|
||||
let scrolled = self.history as i32 - then.history as i32;
|
||||
let expected = then.hits[at].start().line.0 - scrolled;
|
||||
let distance = |index: &usize| (self.hits[*index].start().line.0 - expected).abs();
|
||||
let all = 0..self.hits.len();
|
||||
all.clone()
|
||||
.filter(|&index| {
|
||||
let start = self.bytes[index].start;
|
||||
let line = self.line(start);
|
||||
start - line.start == offset && self.text[line] == *text
|
||||
})
|
||||
.min_by_key(distance)
|
||||
.or_else(|| all.min_by_key(distance))
|
||||
}
|
||||
}
|
||||
|
||||
/// The match one newer than `current`, or one older, round the ends.
|
||||
@@ -97,7 +195,7 @@ impl TerminalView {
|
||||
self.finder = Some(Finder {
|
||||
query: query.clone(),
|
||||
options: FindOptions::default(),
|
||||
hits: Vec::new(),
|
||||
found: Found::default(),
|
||||
current: None,
|
||||
error: None,
|
||||
_input: input,
|
||||
@@ -121,40 +219,42 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the query anew; a `fresh` query starts at the newest match, new output keeps the current one.
|
||||
/// Finds the query anew: fresh, from the newest match; else following the current one.
|
||||
pub(super) fn search(&mut self, fresh: bool, cx: &mut Context<Self>) {
|
||||
let Some(finder) = self.finder.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let query = finder.query.read(cx).text().to_string();
|
||||
let found = if query.is_empty() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
search_all(&self.pty.term.lock(), &pattern(&query, finder.options))
|
||||
};
|
||||
let (hits, error) = match found {
|
||||
Ok(hits) => (hits, None),
|
||||
let found = Found::search(&self.pty.term.lock(), &query, finder.options);
|
||||
let (found, error) = match found {
|
||||
Ok(found) => (found, None),
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"terminal {}: {query:?} is no pattern: {error}",
|
||||
self.pty.pid
|
||||
);
|
||||
(Vec::new(), Some(SharedString::from(error)))
|
||||
(Found::default(), Some(SharedString::from(error)))
|
||||
}
|
||||
};
|
||||
let newest = hits.len().checked_sub(1);
|
||||
let kept = finder.current.filter(|at| *at < hits.len());
|
||||
finder.current = if fresh { newest } else { kept.or(newest) };
|
||||
finder.current = match finder.current {
|
||||
Some(at) if !fresh => found.follow(&finder.found, at),
|
||||
_ => found.hits.len().checked_sub(1),
|
||||
};
|
||||
finder.error = error;
|
||||
finder.hits = hits;
|
||||
finder.found = found;
|
||||
if fresh {
|
||||
log::debug!(
|
||||
"terminal {}: {} matches of {query:?}",
|
||||
self.pty.pid,
|
||||
finder.hits.len()
|
||||
finder.found.hits.len()
|
||||
);
|
||||
self.reveal(cx);
|
||||
} else {
|
||||
let count = finder.found.hits.len();
|
||||
log::trace!(
|
||||
"terminal {}: {count} matches as the grid changed",
|
||||
self.pty.pid
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -164,7 +264,7 @@ impl TerminalView {
|
||||
let Some(finder) = self.finder.as_mut() else {
|
||||
return;
|
||||
};
|
||||
finder.current = stepped(finder.current, finder.hits.len(), newer);
|
||||
finder.current = stepped(finder.current, finder.found.hits.len(), newer);
|
||||
self.reveal(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -172,7 +272,7 @@ impl TerminalView {
|
||||
fn reveal(&mut self, cx: &mut Context<Self>) {
|
||||
let start = self.finder.as_ref().and_then(|finder| {
|
||||
let at = finder.current?;
|
||||
Some(*finder.hits[at].start())
|
||||
Some(*finder.found.hits[at].start())
|
||||
});
|
||||
if let Some(start) = start {
|
||||
log::trace!("terminal {}: match at {start:?}", self.pty.pid);
|
||||
@@ -192,7 +292,7 @@ impl TerminalView {
|
||||
pub(super) fn find_box(&self, cx: &mut Context<Self>) -> Option<impl IntoElement + use<>> {
|
||||
let finder = self.finder.as_ref()?;
|
||||
let (flip, step, close, back) = (cx.entity(), cx.entity(), cx.entity(), cx.entity());
|
||||
let total = finder.hits.len();
|
||||
let total = finder.found.hits.len();
|
||||
let widget = FindWidget::new(
|
||||
("find", cx.entity_id()),
|
||||
&finder.query,
|
||||
@@ -219,46 +319,4 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alacritty_terminal::term::test::mock_term;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_takes_case_words_and_patterns() {
|
||||
let term = mock_term("one two One\r\nthree phone one");
|
||||
let count = |query: &str, options: FindOptions| {
|
||||
search_all(&term, &pattern(query, options)).map(|all| all.len())
|
||||
};
|
||||
assert_eq!(count("one", FindOptions::default()), Ok(4));
|
||||
let case = FindOptions {
|
||||
case: true,
|
||||
..FindOptions::default()
|
||||
};
|
||||
assert_eq!(count("One", case), Ok(1));
|
||||
let word = FindOptions {
|
||||
word: true,
|
||||
..FindOptions::default()
|
||||
};
|
||||
assert_eq!(count("one", word), Ok(3), "phone holds no whole word");
|
||||
let regex = FindOptions {
|
||||
regex: true,
|
||||
..FindOptions::default()
|
||||
};
|
||||
assert_eq!(count("t[wh]", regex), Ok(2));
|
||||
assert_eq!(
|
||||
count("(", FindOptions::default()),
|
||||
Ok(0),
|
||||
"plain text is escaped"
|
||||
);
|
||||
assert!(count("(", regex).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steps_go_round_the_ends() {
|
||||
assert_eq!(stepped(Some(0), 3, false), Some(2));
|
||||
assert_eq!(stepped(Some(2), 3, true), Some(0));
|
||||
assert_eq!(stepped(Some(1), 3, false), Some(0));
|
||||
assert_eq!(stepped(None, 0, true), None);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
@@ -0,0 +1,155 @@
|
||||
use alacritty_terminal::{
|
||||
event::VoidListener,
|
||||
term::{Config, test::TermSize},
|
||||
vte::ansi::Processor,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const PLAIN: FindOptions = FindOptions {
|
||||
case: false,
|
||||
word: false,
|
||||
regex: false,
|
||||
};
|
||||
|
||||
fn term(bytes: &str, columns: usize, lines: usize, history: usize) -> Term<VoidListener> {
|
||||
let config = Config {
|
||||
scrolling_history: history,
|
||||
..Config::default()
|
||||
};
|
||||
let mut term = Term::new(config, &TermSize::new(columns, lines), VoidListener);
|
||||
feed(&mut term, bytes);
|
||||
term
|
||||
}
|
||||
|
||||
fn feed(term: &mut Term<VoidListener>, bytes: &str) {
|
||||
let mut parser: Processor = Processor::new();
|
||||
parser.advance(term, bytes.as_bytes());
|
||||
}
|
||||
|
||||
fn found(term: &Term<VoidListener>, query: &str, options: FindOptions) -> Found {
|
||||
Found::search(term, query, options).expect("a pattern")
|
||||
}
|
||||
|
||||
fn at(line: i32, column: usize) -> Point {
|
||||
Point::new(Line(line), Column(column))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_takes_case_words_and_patterns() {
|
||||
let term = term("one two One\r\nthree phone one", 20, 3, 10);
|
||||
let count =
|
||||
|query: &str, options| Found::search(&term, query, options).map(|found| found.hits.len());
|
||||
assert_eq!(count("one", PLAIN), Ok(4));
|
||||
assert_eq!(
|
||||
count(
|
||||
"One",
|
||||
FindOptions {
|
||||
case: true,
|
||||
..PLAIN
|
||||
}
|
||||
),
|
||||
Ok(1)
|
||||
);
|
||||
let word = FindOptions {
|
||||
word: true,
|
||||
..PLAIN
|
||||
};
|
||||
assert_eq!(count("one", word), Ok(3), "phone holds no whole word");
|
||||
let regex = FindOptions {
|
||||
regex: true,
|
||||
..PLAIN
|
||||
};
|
||||
assert_eq!(count("t[wh]", regex), Ok(2));
|
||||
assert_eq!(count("one$", regex), Ok(2), "a line ends after its text");
|
||||
assert_eq!(count("(", PLAIN), Ok(0), "plain text is escaped");
|
||||
assert!(count("(", regex).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_wide_cells_and_unicode_words_are_found_where_they_show() {
|
||||
let shown = "cafe\u{301} caf\u{e9} \u{4e2d}\u{6587} \x1b[8msecret\x1b[0m";
|
||||
let line = term(shown, 30, 2, 10);
|
||||
let hits = |query: &str, options| found(&line, query, options).hits;
|
||||
assert_eq!(
|
||||
hits("cafe\u{301}", PLAIN),
|
||||
[at(0, 0)..=at(0, 3)],
|
||||
"a mark rides its cell"
|
||||
);
|
||||
let word = FindOptions {
|
||||
word: true,
|
||||
..PLAIN
|
||||
};
|
||||
assert_eq!(hits("caf\u{e9}", word), [at(0, 5)..=at(0, 8)]);
|
||||
assert_eq!(hits("\u{4e2d}\u{6587}", word), [at(0, 10)..=at(0, 12)]);
|
||||
assert!(hits("secret", PLAIN).is_empty(), "hidden text stays hidden");
|
||||
let after = term("\u{4e2d}x", 10, 2, 10);
|
||||
let x = found(&after, "x", PLAIN).hits;
|
||||
assert_eq!(
|
||||
x,
|
||||
[at(0, 2)..=at(0, 2)],
|
||||
"a wide cell's spacer holds no text"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrapped_line_is_one_line_mapped_back_to_its_cells() {
|
||||
let term = term("12345678needle", 10, 3, 10);
|
||||
assert_eq!(found(&term, "needle", PLAIN).hits, [at(0, 8)..=at(1, 3)]);
|
||||
let regex = FindOptions {
|
||||
regex: true,
|
||||
..PLAIN
|
||||
};
|
||||
assert_eq!(found(&term, "needle$", regex).hits.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_current_match_follows_its_line() {
|
||||
let mut erased = term("needle OLD\r\nneedle KEEP\r\nneedle LAST", 20, 4, 10);
|
||||
let then = found(&erased, "needle", PLAIN);
|
||||
feed(&mut erased, "\x1b[1;1H\x1b[2K");
|
||||
let now = found(&erased, "needle", PLAIN);
|
||||
assert_eq!(now.follow(&then, 1), Some(0), "KEEP, though OLD is gone");
|
||||
|
||||
let mut ring = term("n 1\r\nn 2\r\nn 3\r\nn 4\r\nn 5", 10, 3, 2);
|
||||
let then = found(&ring, "n", PLAIN);
|
||||
feed(&mut ring, "\r\nn 6");
|
||||
let now = found(&ring, "n", PLAIN);
|
||||
assert_eq!(
|
||||
now.follow(&then, 2),
|
||||
Some(1),
|
||||
"n 3, a line up, though n 1 left"
|
||||
);
|
||||
|
||||
let mut same = term("n\r\nn\r\nn", 10, 3, 10);
|
||||
let then = found(&same, "n", PLAIN);
|
||||
feed(&mut same, "\r\nx\r\nx");
|
||||
let now = found(&same, "n", PLAIN);
|
||||
assert_eq!(
|
||||
now.follow(&then, 1),
|
||||
Some(1),
|
||||
"of like lines, the one that scrolled"
|
||||
);
|
||||
|
||||
let mut gone = term("needle A\r\nneedle B\r\n\r\nneedle C", 20, 5, 10);
|
||||
let then = found(&gone, "needle", PLAIN);
|
||||
feed(&mut gone, "\x1b[2;1H\x1b[2K");
|
||||
let now = found(&gone, "needle", PLAIN);
|
||||
assert_eq!(now.follow(&then, 1), Some(0), "B is gone: A is nearest");
|
||||
|
||||
let mut wide = term(&format!("{}needle", "x".repeat(15)), 12, 4, 10);
|
||||
let then = found(&wide, "needle", PLAIN);
|
||||
wide.resize(TermSize::new(30, 4));
|
||||
let now = found(&wide, "needle", PLAIN);
|
||||
assert_eq!(now.follow(&then, 0), Some(0));
|
||||
assert_eq!(now.hits[0].start().column, Column(15), "the reflowed cells");
|
||||
assert_ne!(now.hits[0].start(), then.hits[0].start());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steps_go_round_the_ends() {
|
||||
assert_eq!(stepped(Some(0), 3, false), Some(2));
|
||||
assert_eq!(stepped(Some(2), 3, true), Some(0));
|
||||
assert_eq!(stepped(Some(1), 3, false), Some(0));
|
||||
assert_eq!(stepped(None, 0, true), None);
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ pub fn grid(
|
||||
origin + point(cell.width * column as f32, cell.height * row as f32);
|
||||
view.caret = Bounds::new(corner, cell);
|
||||
}
|
||||
if view.fit(columns, lines) {
|
||||
if view.fit(columns, lines, cx) {
|
||||
cx.notify();
|
||||
window.request_animation_frame();
|
||||
}
|
||||
|
||||
+3
-1
@@ -20,6 +20,8 @@ pub enum PtyEvent {
|
||||
Term(Event),
|
||||
Folder(PathBuf),
|
||||
Mouse(MouseMode),
|
||||
/// A full reset, `ESC c`, which empties the title without a title event.
|
||||
Reset,
|
||||
}
|
||||
|
||||
/// Hands the grid's events to the session; `pid` names the program in logs.
|
||||
@@ -249,7 +251,7 @@ mod tests {
|
||||
PtyEvent::Folder(folder) => folders.push(folder),
|
||||
PtyEvent::Term(Event::ChildExit(status)) => success = status.success(),
|
||||
PtyEvent::Term(Event::Exit) => break,
|
||||
PtyEvent::Term(_) | PtyEvent::Mouse(_) => {}
|
||||
PtyEvent::Term(_) | PtyEvent::Mouse(_) | PtyEvent::Reset => {}
|
||||
}
|
||||
}
|
||||
(folders, success)
|
||||
|
||||
+10
-2
@@ -53,7 +53,12 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
log::debug!("pty {pid}: a folder report after its session closed");
|
||||
}
|
||||
});
|
||||
if let Some(mode) = self.mouse.feed(&buf[..count]) {
|
||||
let resets = self.mouse.resets;
|
||||
let changed = self.mouse.feed(&buf[..count]);
|
||||
if self.mouse.resets != resets && events.unbounded_send(PtyEvent::Reset).is_err() {
|
||||
log::debug!("pty {pid}: a reset after its session closed");
|
||||
}
|
||||
if let Some(mode) = changed {
|
||||
log::info!("pty {pid}: mouse {mode:?}");
|
||||
if events.unbounded_send(PtyEvent::Mouse(mode)).is_err() {
|
||||
log::debug!("pty {pid}: a mouse mode after its session closed");
|
||||
@@ -194,7 +199,7 @@ enum Csi {
|
||||
Private,
|
||||
}
|
||||
|
||||
/// The mouse modes as xterm holds them, read as vte reads; alacritty drops 9.
|
||||
/// The mouse modes as xterm holds them, and resets, read as vte reads; alacritty drops 9.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct MouseModes {
|
||||
state: Csi,
|
||||
@@ -204,6 +209,8 @@ pub(crate) struct MouseModes {
|
||||
/// Past a `:`, whose subparameters the parser's mode sets skip.
|
||||
sub: bool,
|
||||
mode: MouseMode,
|
||||
/// Full resets, `ESC c`, seen so far.
|
||||
pub resets: usize,
|
||||
}
|
||||
|
||||
impl MouseModes {
|
||||
@@ -230,6 +237,7 @@ impl MouseModes {
|
||||
(Csi::Escape, b'[') => Csi::Bracket,
|
||||
(Csi::Escape, b'c') => {
|
||||
self.mode = MouseMode::default();
|
||||
self.resets += 1;
|
||||
Csi::Ground
|
||||
}
|
||||
(Csi::Bracket, b'?') => {
|
||||
|
||||
@@ -50,6 +50,7 @@ fn mouse_modes_follow_private_modes_in_order_across_reads() {
|
||||
modes.feed(b"\x1b[?1002h");
|
||||
let reset = modes.feed(b"\x1bc");
|
||||
assert_eq!(reset, Some(MouseMode::default()), "a reset ends it");
|
||||
assert_eq!(modes.resets, 1);
|
||||
modes.feed(b"\x1b[?09;\x1b[?1003h");
|
||||
assert_eq!(modes.mode.tracking, Tracking::Any);
|
||||
}
|
||||
@@ -119,10 +120,10 @@ fn other_hosts_and_bad_uris_give_none() {
|
||||
assert_eq!(folder(b"file:///%FF"), None);
|
||||
}
|
||||
|
||||
/// vte's own reading of `bytes`, under the same mode rules.
|
||||
fn parsed(bytes: &[u8]) -> MouseMode {
|
||||
/// vte's own reading of `bytes`, under the same mode rules: the mode, and the resets.
|
||||
fn parsed(bytes: &[u8]) -> (MouseMode, usize) {
|
||||
use alacritty_terminal::vte::{Params, Parser, Perform};
|
||||
struct Modes(MouseMode);
|
||||
struct Modes(MouseMode, usize);
|
||||
impl Perform for Modes {
|
||||
fn csi_dispatch(&mut self, params: &Params, between: &[u8], ignore: bool, action: char) {
|
||||
if !ignore && between == b"?" && matches!(action, 'h' | 'l') {
|
||||
@@ -135,12 +136,13 @@ fn parsed(bytes: &[u8]) -> MouseMode {
|
||||
fn esc_dispatch(&mut self, between: &[u8], _: bool, byte: u8) {
|
||||
if between.is_empty() && byte == b'c' {
|
||||
self.0 = MouseMode::default();
|
||||
self.1 += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut modes = Modes(MouseMode::default());
|
||||
let mut modes = Modes(MouseMode::default(), 0);
|
||||
Parser::new().advance(&mut modes, bytes);
|
||||
modes.0
|
||||
(modes.0, modes.1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -174,7 +176,8 @@ fn the_tap_reads_mouse_modes_as_vte_does_byte_by_byte() {
|
||||
let mut modes = MouseModes::default();
|
||||
for end in 1..=case.len() {
|
||||
modes.feed(&case[end - 1..end]);
|
||||
assert_eq!(modes.mode, parsed(&case[..end]), "{case:?} to byte {end}");
|
||||
let seen = (modes.mode, modes.resets);
|
||||
assert_eq!(seen, parsed(&case[..end]), "{case:?} to byte {end}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -117,6 +117,7 @@ impl TerminalView {
|
||||
.term
|
||||
.lock()
|
||||
.set_options(config(SettingsStore::get(cx)));
|
||||
view.search(false, cx);
|
||||
cx.notify();
|
||||
}),
|
||||
}
|
||||
@@ -158,7 +159,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
/// Fits the grid to `columns` by `lines`; true when the size changed.
|
||||
pub(super) fn fit(&mut self, columns: usize, lines: usize) -> bool {
|
||||
pub(super) fn fit(&mut self, columns: usize, lines: usize, cx: &mut Context<Self>) -> bool {
|
||||
let fitted = fit(columns, lines, self.cell);
|
||||
let size = (usize::from(fitted.num_cols), usize::from(fitted.num_lines));
|
||||
if size == self.size {
|
||||
@@ -167,6 +168,7 @@ impl TerminalView {
|
||||
self.size = size;
|
||||
self.pty.resize(size, self.cell);
|
||||
log::debug!("terminal {}: {} by {}", self.pty.pid, size.0, size.1);
|
||||
self.search(false, cx);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -202,9 +204,15 @@ impl TerminalView {
|
||||
PtyEvent::Folder(folder) => cx.emit(TerminalEvent::Folder(folder)),
|
||||
PtyEvent::Term(event) => self.term_event(event, cx),
|
||||
PtyEvent::Mouse(mode) => self.mouse = mode,
|
||||
PtyEvent::Reset => {
|
||||
log::debug!("terminal {}: a reset; the title follows", self.pty.pid);
|
||||
// Setting the options sends the grid's title, empty after a reset.
|
||||
let settings = config(SettingsStore::get(cx));
|
||||
self.pty.term.lock().set_options(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ponytail: a full search per output batch while find is open; search new lines alone if scrollbacks lag.
|
||||
// ponytail: a full search per output batch while find is open; search only new lines if it lags.
|
||||
if drawn && self.finder.is_some() {
|
||||
self.search(false, cx);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user