feat: preserve diff context during LSP navigation

This commit is contained in:
Valdemar Lorenzen
2026-10-12 14:03:19 +02:00
parent 6eb99a792a
commit 1b8b669ca0
10 changed files with 2559 additions and 126 deletions
+98 -8
View File
@@ -188,6 +188,7 @@ pub struct DiffState {
pub error: Option<String>,
pub comparison: Option<Comparison>,
pub comparison_id: Option<u64>,
pub(crate) snapshot_roots: [Option<PathBuf>; 2],
pub files: Option<FilePicker>,
pub files_visible: bool,
pub file: usize,
@@ -229,6 +230,7 @@ impl DiffState {
comparison: None,
comparison_id: None,
files: None,
snapshot_roots: [None, None],
files_visible: true,
file: 0,
focus: Focus::Files,
@@ -264,6 +266,10 @@ impl DiffState {
self.pending.take()
}
pub(crate) fn request_queued(&self) -> bool {
self.pending.is_some()
}
fn queue(&mut self, operation: Operation) {
self.request_kind = Some(match operation {
Operation::Compare { .. } => RequestKind::Comparison,
@@ -291,7 +297,6 @@ impl DiffState {
..
}) = &result
&& (self.comparison_id != Some(*comparison_id)
|| self.file != *file_index
|| self.request_kind
!= Some(RequestKind::File {
comparison_id: *comparison_id,
@@ -347,6 +352,7 @@ impl DiffState {
self.files = Some(files);
self.comparison = Some(comparison);
self.comparison_id = Some(id);
self.snapshot_roots = [None, None];
self.phase = Phase::View;
self.file = 0;
self.focus = if self.files_visible {
@@ -379,8 +385,10 @@ impl DiffState {
comparison.editable_paths.remove(&file.path);
}
*selected = file;
self.align_file();
self.normalize_focus();
if self.file == file_index {
self.align_file();
self.normalize_focus();
}
self.error = None;
} else {
self.error = Some("Unexpected changed-file response".into());
@@ -742,9 +750,13 @@ impl DiffState {
}
pub fn after_editable(&self) -> bool {
matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
&& self.working_after_available()
}
pub(crate) fn working_after_available(&self) -> bool {
self.phase == Phase::View
&& self.selected_file_loaded()
&& matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
&& self
.after
.as_ref()
@@ -752,6 +764,7 @@ impl DiffState {
&& self.comparison.as_ref().is_some_and(|comparison| {
comparison.files.get(self.file).is_some_and(|file| {
comparison.editable_paths.contains(&file.path)
&& file.status != FileStatus::Removed
&& file
.notice
.as_deref()
@@ -770,6 +783,20 @@ impl DiffState {
}) else {
return false;
};
self.select_file_index(index)
}
pub(crate) fn select_file_index(&mut self, index: usize) -> bool {
let Some(file) = self
.comparison
.as_ref()
.and_then(|comparison| comparison.files.get(index))
else {
return false;
};
if let Some(files) = &mut self.files {
files.reveal_path(&file.path);
}
self.file = index;
self.normalize_focus();
self.live_buffer = None;
@@ -809,6 +836,69 @@ impl DiffState {
}
}
pub(crate) fn load_file(&mut self, file_index: usize) {
if let Some(comparison_id) = self.comparison_id {
self.queue(Operation::LoadFile {
comparison_id,
file_index,
});
}
}
pub(crate) fn source_range_changed(
&self,
file_index: usize,
focus: Focus,
first: usize,
last: usize,
) -> bool {
if file_index == self.file {
return self.rows.iter().any(|aligned| {
aligned.kind != RowKind::Equal
&& match focus {
Focus::Before => aligned
.before
.is_some_and(|row| first <= row && row <= last),
Focus::After => {
aligned.after.is_some_and(|row| first <= row && row <= last)
}
Focus::Files => false,
}
});
}
self.comparison
.as_ref()
.and_then(|comparison| comparison.files.get(file_index))
.is_some_and(|file| {
file.hunks.iter().any(|hunk| {
let (start, len) = match focus {
Focus::Before => (hunk.before_start, hunk.before_len),
Focus::After => (hunk.after_start, hunk.after_len),
Focus::Files => return false,
};
len > 0 && start <= last && first < start.saturating_add(len)
})
})
}
pub(crate) fn reveal_source_row(&mut self, focus: Focus, row: usize) {
if self.source_display_row(focus, row).is_none()
&& let Some(index) = self.rows.iter().position(|aligned| match focus {
Focus::Before => aligned.before == Some(row),
Focus::After => aligned.after == Some(row),
Focus::Files => false,
})
{
self.excerpt_ranges.push((index, index + 1));
self.excerpt_ranges.sort_unstable();
self.rebuild_display_rows();
}
if let Some(display) = self.source_display_row(focus, row) {
self.top = display;
self.follow_caret = true;
}
}
pub(crate) fn source_display_row(&self, focus: Focus, source_row: usize) -> Option<usize> {
self.display_rows.iter().position(|display| {
matches!(*display, DisplayRow::Line(index) if self.source_row(focus, index) == Some(source_row))
@@ -833,14 +923,14 @@ impl DiffState {
})
}
pub(crate) fn snapshot_lines(&self, focus: Focus) -> Vec<String> {
let file = &self.comparison.as_ref().expect("snapshot comparison").files[self.file];
pub(crate) fn snapshot_lines_at(&self, file_index: usize, focus: Focus) -> Vec<String> {
let file = &self.comparison.as_ref().expect("snapshot comparison").files[file_index];
let (source, eof, live) = match focus {
Focus::Before => (&file.before, file.before_eof, false),
Focus::After => (
&file.after,
file.after_eof,
self.live_files.contains(&self.file),
self.live_files.contains(&file_index),
),
Focus::Files => unreachable!("snapshot belongs to a text pane"),
};
@@ -901,7 +991,7 @@ impl DiffState {
}
pub fn refresh_after(&mut self, lines: &[String], revision: u64) {
if !self.after_editable() {
if !self.working_after_available() {
return;
}
if self.live_revision == Some(revision) {