fix: skip omitted diff lines instead of expanding excerpts during navigation

This commit is contained in:
2026-10-11 03:02:47 +02:00
parent 54bb196192
commit 86d22d4fa2
5 changed files with 302 additions and 79 deletions
+129 -43
View File
@@ -1420,16 +1420,33 @@ impl Editor {
}
pub(crate) fn refresh_live_diff(&mut self) {
let row = self.cursor().row;
let Some(diff) = &mut self.diff else { return };
if diff.live_buffer != Some(self.active.id) || !diff.after_editable() {
return;
}
if diff.live_revision != Some(self.active.text_revision) {
diff.refresh_after(&self.active.lines, self.active.text_revision, row);
diff.refresh_after(&self.active.lines, self.active.text_revision);
diff.follow_caret = true;
}
diff.reveal_after(row);
let mut moved = false;
for index in 0..self.active.targets.len() {
let cursor = self.active.targets[index].active;
let Some(row) = self.diff.as_ref().unwrap().clamp_after_row(cursor.row) else {
continue;
};
if row != cursor.row {
self.active.targets[index].active = Cursor {
row,
column: cursor.column.min(self.max_column(row)),
};
self.active.targets[index].preferred_columns = None;
moved = true;
}
}
if moved {
self.active.search_match = None;
self.normalize_targets();
}
}
pub(crate) fn scroll_diff(&mut self, focus: Focus, down: bool) {
@@ -3805,11 +3822,9 @@ impl Editor {
let query = self.active.search_query.as_str();
// ponytail: O(buffer graphemes × query bytes) scan; index matches if large buffers need it.
let matches: Vec<_> = self
.active
.lines
.iter()
.enumerate()
.flat_map(|(row, line)| {
.navigable_rows(0, self.active.lines.len())
.flat_map(|row| {
let line = &self.active.lines[row];
line.grapheme_indices(true).filter_map(move |(byte, _)| {
(line[byte..].starts_with(query)
&& GraphemeCursor::new(byte + query.len(), line.len(), true)
@@ -4749,10 +4764,10 @@ impl Editor {
let target = self.active.targets[index];
let shift = |row: usize| {
if down {
row.checked_add(1)
.filter(|row| *row < self.active.lines.len())
self.navigable_rows(row.saturating_add(1), self.active.lines.len())
.next()
} else {
row.checked_sub(1)
self.navigable_rows(0, row).next_back()
}
};
let Some(anchor_row) = shift(target.anchor.row) else {
@@ -4831,14 +4846,28 @@ impl Editor {
})
.collect();
// ponytail: literal scan is O(buffer graphemes × phrase bytes); index only for large buffers.
let candidates: Vec<_> = text
.grapheme_indices(true)
.filter_map(|(byte, _)| {
let diff = self.diff.as_ref().filter(|_| self.diff_editable());
let candidates: Vec<_> = self
.navigable_rows(0, self.active.lines.len())
.flat_map(|row| {
let offset = offsets[row];
let line = &self.active.lines[row];
line.grapheme_indices(true)
.map(move |(byte, _)| offset + byte)
.chain(std::iter::once(offset + line.len()))
})
.filter_map(|byte| {
let end = byte + phrase.len();
(text[byte..].starts_with(phrase)
&& GraphemeCursor::new(end, text.len(), true)
.is_boundary(&text, 0)
.expect("complete buffer supplies grapheme context"))
.expect("complete buffer supplies grapheme context")
&& diff.is_none_or(|diff| {
let last = offsets
.partition_point(|offset| *offset < end)
.saturating_sub(1);
diff.after_display_row(last).is_some()
}))
.then_some((byte, end))
})
.collect();
@@ -5784,6 +5813,19 @@ impl Editor {
}
}
fn navigable_rows(
&self,
start: usize,
end: usize,
) -> impl DoubleEndedIterator<Item = usize> + '_ {
let diff = self.diff.as_ref().filter(|_| self.diff_editable());
(start..if diff.is_none() { end } else { start }).chain(
diff.into_iter()
.flat_map(DiffState::after_rows)
.filter(move |row| start <= *row && *row < end),
)
}
fn move_targets(&mut self, action: Action, count: usize) {
self.reveal_caret();
for index in 0..self.active.targets.len() {
@@ -5797,30 +5839,54 @@ impl Editor {
.min(self.max_column(cursor.row))
}
Action::Down | Action::Up => {
let row = if matches!(action, Action::Down) {
cursor
.row
.saturating_add(count)
.min(self.active.lines.len() - 1)
if self.diff_editable() {
let step = |cursor: Cursor, row| Cursor {
row,
column: cursor.column.min(self.max_column(row)),
};
cursor = if action == Action::Down {
self.navigable_rows(
cursor.row.saturating_add(1),
self.active.lines.len(),
)
.take(count)
.fold(cursor, step)
} else {
self.navigable_rows(0, cursor.row)
.rev()
.take(count)
.fold(cursor, step)
};
} else {
cursor.row.saturating_sub(count)
};
let first = cursor.row.min(row);
let last = cursor.row.max(row);
cursor.column = (first..=last)
.map(|row| self.max_column(row))
.min()
.unwrap_or(0)
.min(cursor.column);
cursor.row = row;
let row = if matches!(action, Action::Down) {
cursor
.row
.saturating_add(count)
.min(self.active.lines.len() - 1)
} else {
cursor.row.saturating_sub(count)
};
let first = cursor.row.min(row);
let last = cursor.row.max(row);
cursor.column = (first..=last)
.map(|row| self.max_column(row))
.min()
.unwrap_or(0)
.min(cursor.column);
cursor.row = row;
}
}
Action::LineStart => cursor.column = 0,
Action::LineEnd => cursor.column = self.max_column(cursor.row),
Action::DocumentStart | Action::DocumentEnd => {
cursor.row = if action == Action::DocumentStart {
0
self.navigable_rows(0, self.active.lines.len())
.next()
.unwrap_or(cursor.row)
} else {
self.active.lines.len() - 1
self.navigable_rows(0, self.active.lines.len())
.next_back()
.unwrap_or(cursor.row)
};
cursor.column = first_nonblank(&self.active.lines[cursor.row]);
}
@@ -5838,17 +5904,34 @@ impl Editor {
action,
Action::BigWordNext | Action::BigWordPrevious | Action::BigWordEnd
);
let backward = matches!(action, Action::WordPrevious | Action::BigWordPrevious);
let start = if backward { 0 } else { row };
let end = if backward {
row + 1
} else {
self.active.lines.len()
};
let tokens = self.navigable_rows(start, end).flat_map(move |source_row| {
word_tokens(std::slice::from_ref(&self.active.lines[source_row]), big)
.map(move |(_, byte, kind)| (source_row, byte, kind))
});
let destination = match action {
Action::WordPrevious | Action::BigWordPrevious => {
let mut tokens = word_tokens(&self.active.lines[..=row], big)
let mut tokens = tokens
.rev()
.skip_while(|(r, b, _)| (*r, *b) >= (row, byte))
.peekable();
let mut destination = (0, 0);
let fallback = (
self.navigable_rows(0, self.active.lines.len())
.next()
.unwrap_or(row),
0,
);
let mut destination = fallback;
for _ in 0..count {
let Some(mut first) = tokens.find(|(_, _, kind)| *kind != WordKind::Whitespace)
else {
destination = (0, 0);
destination = fallback;
break;
};
if first.2 != WordKind::EmptyLine {
@@ -5861,14 +5944,17 @@ impl Editor {
destination
}
_ => {
let mut tokens = word_tokens(&self.active.lines[row..], big)
.skip_while(|(r, b, _)| (*r, *b) < (0, byte))
let mut tokens = tokens
.skip_while(|(r, b, _)| (*r, *b) < (row, byte))
.peekable();
let mut current = tokens
.next()
.expect("cursor has a grapheme or line-end token")
.2;
let last = self.active.lines.len() - 1;
let Some(first) = tokens.next() else {
return cursor;
};
let mut current = first.2;
let last = self
.navigable_rows(0, self.active.lines.len())
.next_back()
.unwrap_or(row);
let mut destination = (last, self.active.lines[last].len());
for _ in 0..count {
let next_start = matches!(action, Action::WordNext | Action::BigWordNext);
@@ -5890,7 +5976,7 @@ impl Editor {
first = tokens.next().expect("peeked token");
}
}
destination = (row + first.0, first.1);
destination = (first.0, first.1);
}
destination
}