diff --git a/src/component/diff_view.rs b/src/component/diff_view.rs index 9c048f6..9ba48c4 100644 --- a/src/component/diff_view.rs +++ b/src/component/diff_view.rs @@ -28,7 +28,7 @@ struct DiffViewStateInner { #[derive(Clone)] pub(crate) struct DiffViewContent { - diff: Arc, + pub(crate) diff: Arc, } #[derive(Clone, gpui::IntoElement)] diff --git a/src/screen/dashboard/pull_request_diff_view.rs b/src/screen/dashboard/pull_request_diff_view.rs index 4bc328f..c485896 100644 --- a/src/screen/dashboard/pull_request_diff_view.rs +++ b/src/screen/dashboard/pull_request_diff_view.rs @@ -14,7 +14,7 @@ use crate::{ text_input::{self, TextInput, text_input}, }, query::{self, QueryStatus, read_query, use_query, watch_query}, - util, + util::{self, diff::DiffLineIndex}, }; pub(crate) struct PullRequestDiffView { @@ -33,15 +33,19 @@ pub(crate) struct PullRequestDiffView { search_input: gpui::Entity, } +struct DiffSearchHit { + diff_line_i: DiffLineIndex, + src_byte_range: std::ops::Range, +} + struct DiffSearchResult { cursor: DiffSearchResultCursor, - old_side: Vec<(usize, std::ops::Range)>, - new_side: Vec<(usize, std::ops::Range)>, + old_side: Vec, + new_side: Vec, } struct DiffSearchResultCursor { - is_old: bool, - line_index: usize, + side: util::diff::DiffSide, index: usize, } @@ -204,6 +208,10 @@ impl PullRequestDiffView { } fn search_in_diff(&mut self, search_str: &str, cx: &mut gpui::Context) { + let Some(diff_view_content) = &self.diff_view_content else { + return; + }; + let diff_view_state_in_search_mode = self .diff_view_state_in_search_mode .get_or_insert(DiffViewState::fork_from(&self.diff_view_state)); @@ -226,7 +234,13 @@ impl PullRequestDiffView { .diff_view_state .old_side_highlights()? .line_index_of_range(&range)?; - Some((line_idx, range)) + let diff_line_i = diff_view_content + .diff + .diff_line_index_for_line(util::diff::DiffSide::Old, line_idx); + Some(DiffSearchHit { + diff_line_i, + src_byte_range: range, + }) }) .collect::>(); let new_search_result = memchr::memmem::find_iter(&diff.new_content, search_str.as_bytes()) @@ -236,21 +250,27 @@ impl PullRequestDiffView { .diff_view_state .new_side_highlights()? .line_index_of_range(&range)?; - Some((line_idx, range)) + let diff_line_i = diff_view_content + .diff + .diff_line_index_for_line(util::diff::DiffSide::New, line_idx); + Some(DiffSearchHit { + diff_line_i, + src_byte_range: range, + }) }) .collect::>(); let old_side_highlights = old_search_result .iter() - .map(|(_, r)| -> util::syntax_highlight::HighlightedRange { - (r.clone(), symbol_highlight_style(theme)) + .map(|hit| -> util::syntax_highlight::HighlightedRange { + (hit.src_byte_range.clone(), symbol_highlight_style(theme)) }); let new_side_highlights = new_search_result .iter() - .map(|(_, r)| -> util::syntax_highlight::HighlightedRange { - (r.clone(), symbol_highlight_style(theme)) + .map(|hit| -> util::syntax_highlight::HighlightedRange { + (hit.src_byte_range.clone(), symbol_highlight_style(theme)) }); if let Some(h) = self @@ -276,14 +296,12 @@ impl PullRequestDiffView { } else { let cursor = if !old_search_result.is_empty() { DiffSearchResultCursor { - is_old: true, - line_index: old_search_result[0].0, + side: util::diff::DiffSide::Old, index: 0, } } else { DiffSearchResultCursor { - is_old: false, - line_index: new_search_result[0].0, + side: util::diff::DiffSide::New, index: 0, } }; @@ -305,21 +323,23 @@ impl PullRequestDiffView { return; }; - let (current_side, other_side) = if search_result.cursor.is_old { - (&search_result.old_side, &search_result.new_side) - } else { - (&search_result.new_side, &search_result.old_side) + let (current_side, other_side) = match search_result.cursor.side { + | util::diff::DiffSide::Old => (&search_result.old_side, &search_result.new_side), + | util::diff::DiffSide::New => (&search_result.new_side, &search_result.old_side), }; let theme = app::current_theme(cx); - let current_line_index = search_result.cursor.line_index; + let current_search_hit = ¤t_side[search_result.cursor.index]; let highlight_range = match current_side.get(search_result.cursor.index + 1) { - | Some((next_highlight_line, next_range)) if *next_highlight_line == current_line_index => { + | Some(DiffSearchHit { + diff_line_i, + src_byte_range, + }) if *diff_line_i == current_search_hit.diff_line_i => { // go to next search result on same side & same line search_result.cursor.index += 1; Some(( - next_range.clone(), + src_byte_range.clone(), gpui::HighlightStyle { background_color: Some(gpui::red()), ..symbol_highlight_style(theme) @@ -327,33 +347,42 @@ impl PullRequestDiffView { )) } | next => { - let next_highlight_line = next.map(|(line, _)| *line).unwrap_or(usize::MAX); + let next_highlight_line = next + .map(|hit| hit.diff_line_i) + .unwrap_or(DiffLineIndex::MAX); let mut i = 0; - let mut other_side_result: Option<(usize, usize, &std::ops::Range)> = None; - while let Some((other_side_line_i, r)) = &other_side.get(i) { - if *other_side_line_i == current_line_index { + let mut other_side_result: Option<(DiffLineIndex, usize, &std::ops::Range)> = + None; + while let Some(DiffSearchHit { + diff_line_i, + src_byte_range, + }) = &other_side.get(i) + { + if *diff_line_i == current_search_hit.diff_line_i + && matches!(search_result.cursor.side, util::diff::DiffSide::Old) + { // found other side highlight on the current line - other_side_result = Some((*other_side_line_i, i, r)); + // we are on old side, so jump to new side on same line + other_side_result = Some((*diff_line_i, i, src_byte_range)); break; } - if *other_side_line_i > current_line_index - && *other_side_line_i < next_highlight_line + if *diff_line_i > current_search_hit.diff_line_i + && *diff_line_i <= next_highlight_line { // found other side highlight in between current side highlight and next side highlight - other_side_result = Some((*other_side_line_i, i, r)); + other_side_result = Some((*diff_line_i, i, src_byte_range)); break; } - if *other_side_line_i > next_highlight_line { + if *diff_line_i > next_highlight_line { break; } i += 1; } - if let Some((other_side_line_i, other_side_i, r)) = other_side_result { + if let Some((_, other_side_i, r)) = other_side_result { // next cursor should be on other side with the found index - search_result.cursor.is_old = !search_result.cursor.is_old; - search_result.cursor.line_index = other_side_line_i; + search_result.cursor.side = search_result.cursor.side.flipped(); search_result.cursor.index = other_side_i; Some(( r.clone(), @@ -364,10 +393,9 @@ impl PullRequestDiffView { )) } else if let Some(next) = next { // stay on old side, point to next old side highlight - search_result.cursor.line_index = next_highlight_line; search_result.cursor.index = search_result.cursor.index + 1; Some(( - next.1.clone(), + next.src_byte_range.clone(), gpui::HighlightStyle { background_color: Some(gpui::red()), ..symbol_highlight_style(theme) @@ -380,13 +408,11 @@ impl PullRequestDiffView { }; if let Some(highlight_range) = highlight_range { - let content = if search_result.cursor.is_old { - diff_view_state.old_side_highlights_mut() - } else { - diff_view_state.new_side_highlights_mut() + let content = match search_result.cursor.side { + | util::diff::DiffSide::Old => diff_view_state.old_side_highlights_mut(), + | util::diff::DiffSide::New => diff_view_state.new_side_highlights_mut(), }; if let Some(mut content) = content { - println!("replacing highlight range {:?}", highlight_range); content.replace_highlight_range(&highlight_range); } } diff --git a/src/util/diff.rs b/src/util/diff.rs index ce87b55..741f0d0 100644 --- a/src/util/diff.rs +++ b/src/util/diff.rs @@ -4,6 +4,12 @@ use similar::DiffableStr; use crate::util; +#[derive(Copy, Clone)] +pub(crate) enum DiffSide { + Old, + New, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Op { Equal, @@ -12,6 +18,9 @@ pub(crate) enum Op { Replace, } +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct DiffLineIndex(usize); + #[derive(Clone)] pub(crate) struct DiffLine { pub(crate) op: Op, @@ -26,12 +35,31 @@ pub(crate) struct DiffLine { #[derive(Clone)] pub(crate) struct ContentDiff { pub(crate) diff_lines: Vec, + + // translates source line indices to diff_lines indices. + old_line_to_diff_line_indices: Vec, + new_line_to_diff_line_indices: Vec, + pub(crate) old_content: bytes::Bytes, pub(crate) old_line_count: usize, + pub(crate) new_content: bytes::Bytes, pub(crate) new_line_count: usize, } +impl DiffLineIndex { + pub(crate) const MAX: DiffLineIndex = DiffLineIndex(usize::MAX); +} + +impl DiffSide { + pub(crate) fn flipped(&self) -> Self { + match self { + | DiffSide::New => DiffSide::Old, + | DiffSide::Old => DiffSide::New, + } + } +} + pub(crate) fn diff_content( old_content: bytes::Bytes, new_content: bytes::Bytes, @@ -41,135 +69,181 @@ pub(crate) fn diff_content( let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content); let mut diff_lines: Vec = Vec::new(); + let mut old_line_to_diff_line_indices: Vec = + Vec::with_capacity(old_line_ranges.len()); + let mut new_line_to_diff_line_indices: Vec = + Vec::with_capacity(new_line_ranges.len()); + + fn push_diff_line_index( + line_i: usize, + diff_line_i: DiffLineIndex, + indices: &mut Vec, + ) { + if indices.get(line_i).is_none() { + indices.push(diff_line_i); + } + } for op in diff.ops() { match op { - | &similar::DiffOp::Equal { - old_index, - new_index, - len, - } => { - for i in 0..len { - let old_line = old_index + i; - let new_line = new_index + i; - let old_line_range = &old_line_ranges[old_line]; - let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); - diff_lines.push(DiffLine { - op: Op::Equal, - old_line: Some(old_line), - old_content: Some(Arc::clone(&content)), - old_byte_range: old_line_range.clone(), - new_line: Some(new_line), - new_content: Some(content), - new_byte_range: new_line_ranges[new_line].clone(), - }); - } - } + | &similar::DiffOp::Equal { + old_index, + new_index, + len, + } => { + for i in 0..len { + let old_line = old_index + i; + let new_line = new_index + i; + let old_line_range = &old_line_ranges[old_line]; + let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); - | &similar::DiffOp::Insert { - new_index, new_len, .. - } => { - for i in 0..new_len { - let new_line_range = &new_line_ranges[new_index + i]; - let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?); - diff_lines.push(DiffLine { - op: Op::Insert, + diff_lines.push(DiffLine { + op: Op::Equal, + old_line: Some(old_line), + old_content: Some(Arc::clone(&content)), + old_byte_range: old_line_range.clone(), + new_line: Some(new_line), + new_content: Some(content), + new_byte_range: new_line_ranges[new_line].clone(), + }); + + let diff_line_i = DiffLineIndex(diff_lines.len() - 1); + + push_diff_line_index(old_line, diff_line_i, &mut old_line_to_diff_line_indices); + push_diff_line_index(new_line, diff_line_i, &mut new_line_to_diff_line_indices); + } + } + + | &similar::DiffOp::Insert { + new_index, new_len, .. + } => { + for i in 0..new_len { + let new_line_range = &new_line_ranges[new_index + i]; + let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?); + let new_line = new_index + i; + diff_lines.push(DiffLine { + op: Op::Insert, + old_line: None, + old_content: None, + old_byte_range: 0..0, + new_line: Some(new_line), + new_content: Some(content), + new_byte_range: new_line_range.clone(), + }); + push_diff_line_index( + new_line, + DiffLineIndex(diff_lines.len() - 1), + &mut new_line_to_diff_line_indices, + ); + } + } + + | &similar::DiffOp::Replace { + old_index, + old_len, + new_index, + new_len, + } => { + for i in 0..new_len.max(old_len) { + let old_line = old_index + i; + let new_line = new_index + i; + let diff_line_i = DiffLineIndex(diff_lines.len()); + + let diff_line = match (old_line_ranges.get(old_line), new_line_ranges.get(new_line)) + { + | (Some(old_range), Some(new_range)) => { + push_diff_line_index(old_line, diff_line_i, &mut old_line_to_diff_line_indices); + push_diff_line_index(new_line, diff_line_i, &mut new_line_to_diff_line_indices); + + DiffLine { + op: Op::Replace, + old_line: Some(old_line), + old_content: Some(Arc::from( + old_content.slice(old_range.clone()).as_str()?, + )), + old_byte_range: old_range.clone(), + new_line: Some(new_line), + new_content: Some(Arc::from( + new_content.slice(new_range.clone()).as_str()?, + )), + new_byte_range: new_range.clone(), + } + } + + | (None, Some(new_range)) => { + push_diff_line_index(new_line, diff_line_i, &mut new_line_to_diff_line_indices); + + DiffLine { + op: Op::Replace, old_line: None, old_content: None, old_byte_range: 0..0, - new_line: Some(new_index + i), - new_content: Some(content), - new_byte_range: new_line_range.clone(), - }) + new_line: Some(new_line), + new_content: Some(Arc::from( + new_content.slice(new_range.clone()).as_str()?, + )), + new_byte_range: new_range.clone(), + } } - } - | &similar::DiffOp::Replace { - old_index, - old_len, - new_index, - new_len, - } => { - for i in 0..new_len.max(old_len) { - let old_line = old_index + i; - let new_line = new_index + i; + | (Some(old_range), None) => { + push_diff_line_index(old_line, diff_line_i, &mut old_line_to_diff_line_indices); - let diff_line = match ( - old_line_ranges.get(old_line), - new_line_ranges.get(new_line), - ) { - | (Some(old_range), Some(new_range)) => DiffLine { - op: Op::Replace, - old_line: Some(old_line), - old_content: Some(Arc::from( - old_content.slice(old_range.clone()).as_str()?, - )), - old_byte_range: old_range.clone(), - new_line: Some(new_line), - new_content: Some(Arc::from( - new_content.slice(new_range.clone()).as_str()?, - )), - new_byte_range: new_range.clone(), - }, - - | (None, Some(new_range)) => DiffLine { - op: Op::Replace, - old_line: None, - old_content: None, - old_byte_range: 0..0, - new_line: Some(new_index + i), - new_content: Some(Arc::from( - new_content.slice(new_range.clone()).as_str()?, - )), - new_byte_range: new_range.clone(), - }, - - | (Some(old_range), None) => DiffLine { - op: Op::Replace, - old_line: Some(old_index + i), - old_content: Some(Arc::from( - old_content.slice(old_range.clone()).as_str()?, - )), - old_byte_range: old_range.clone(), - new_line: None, - new_content: None, - new_byte_range: 0..0, - }, - - | (None, None) => { - // unlickly to happen, but if it does, idk - panic!( - "the unlikely happened: both old & new index of DiffOps::Replace don't point to any line in the parsed line ranges." - ) - } - }; - - diff_lines.push(diff_line); - } - } - - | &similar::DiffOp::Delete { - old_index, old_len, .. - } => { - for i in 0..old_len { - let old_line_range = &old_line_ranges[old_index]; - let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); - diff_lines.push(DiffLine { - op: Op::Delete, - old_line: Some(old_index + i), - old_content: Some(content), - old_byte_range: old_line_range.clone(), + DiffLine { + op: Op::Replace, + old_line: Some(old_line), + old_content: Some(Arc::from( + old_content.slice(old_range.clone()).as_str()?, + )), + old_byte_range: old_range.clone(), new_line: None, new_content: None, new_byte_range: 0..0, - }) + } } + + | (None, None) => { + // unlickly to happen, but if it does, idk + panic!( + "the unlikely happened: both old & new index of DiffOps::Replace don't point to any line in the parsed line ranges." + ) + } + }; + + diff_lines.push(diff_line); } } + + | &similar::DiffOp::Delete { + old_index, old_len, .. + } => { + for i in 0..old_len { + let old_line = old_index + i; + let old_line_range = &old_line_ranges[old_index]; + let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?); + diff_lines.push(DiffLine { + op: Op::Delete, + old_line: Some(old_index + i), + old_content: Some(content), + old_byte_range: old_line_range.clone(), + new_line: None, + new_content: None, + new_byte_range: 0..0, + }); + push_diff_line_index( + old_line, + DiffLineIndex(diff_lines.len() - 1), + &mut old_line_to_diff_line_indices, + ); + } + } + } } Some(ContentDiff { diff_lines, + old_line_to_diff_line_indices, + new_line_to_diff_line_indices, old_content, old_line_count: old_line_ranges.len(), new_content, @@ -189,4 +263,11 @@ impl ContentDiff { pub(crate) fn last(&self) -> Option<&DiffLine> { self.diff_lines.last() } + + pub(crate) fn diff_line_index_for_line(&self, side: DiffSide, line_i: usize) -> DiffLineIndex { + match side { + | DiffSide::New => self.new_line_to_diff_line_indices[line_i], + | DiffSide::Old => self.old_line_to_diff_line_indices[line_i], + } + } } diff --git a/src/util/file.rs b/src/util/file.rs index 9ef6191..d7667c1 100644 --- a/src/util/file.rs +++ b/src/util/file.rs @@ -43,17 +43,17 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType { ContentType::Text } else { match memchr(0, &content[..content.len().min(8192)]) { - | None => ContentType::Text, - | Some(_) => ContentType::Binary, + | None => ContentType::Text, + | Some(_) => ContentType::Binary, } } } pub(crate) fn file_type_from_path(path: &str) -> FileType { match Path::new(path).extension().map(|it| it.to_str()).flatten() { - | Some("rs") => FileType::Rust, - | Some("js") | Some("jsx") => FileType::JavaScript, - | _ => FileType::Unknown, + | Some("rs") => FileType::Rust, + | Some("js") | Some("jsx") => FileType::JavaScript, + | _ => FileType::Unknown, } } @@ -71,18 +71,18 @@ pub(crate) fn line_ranges(content: &[u8]) -> Vec> { let c = content[i]; match (c, content.get(i + 1)) { - | (b'\r', Some(b'\n')) => { - // if \r found, check if its \r\n or if its a lone \r - // if \r\n, then treat as one line break - ranges.push(line_start..i + 1); - // because we already counted the \n byte, the next iter into it needs to be skipped - skip_next = true; - line_start = i + 2; - } - | _ => { - ranges.push(line_start..i); - line_start = i + 1; - } + | (b'\r', Some(b'\n')) => { + // if \r found, check if its \r\n or if its a lone \r + // if \r\n, then treat as one line break + ranges.push(line_start..i + 1); + // because we already counted the \n byte, the next iter into it needs to be skipped + skip_next = true; + line_start = i + 2; + } + | _ => { + ranges.push(line_start..i); + line_start = i + 1; + } } } @@ -101,28 +101,28 @@ pub(crate) fn sort_by_path(mut items: Vec, key: impl Fn(&T) -> &str) -> So let b_is_root_file = !b_path.contains('/'); match (a_is_root_file, b_is_root_file) { - | (true, false) => return std::cmp::Ordering::Greater, - | (false, true) => return std::cmp::Ordering::Less, - | _ => {} + | (true, false) => return std::cmp::Ordering::Greater, + | (false, true) => return std::cmp::Ordering::Less, + | _ => {} } let mut a_parts = a_path.split('/').peekable(); let mut b_parts = b_path.split('/').peekable(); loop { match (a_parts.next(), b_parts.next()) { - | (Some(a), Some(b)) => { - if a != b { - match (a_parts.peek().is_some(), b_parts.peek().is_some()) { - | (true, false) => return std::cmp::Ordering::Less, - | (false, true) => return std::cmp::Ordering::Greater, - | _ => {} - } - return a.cmp(b); + | (Some(a), Some(b)) => { + if a != b { + match (a_parts.peek().is_some(), b_parts.peek().is_some()) { + | (true, false) => return std::cmp::Ordering::Less, + | (false, true) => return std::cmp::Ordering::Greater, + | _ => {} } + return a.cmp(b); } - | (Some(_), None) => return std::cmp::Ordering::Greater, - | (None, Some(_)) => return std::cmp::Ordering::Less, - | (None, None) => return std::cmp::Ordering::Equal, + } + | (Some(_), None) => return std::cmp::Ordering::Greater, + | (None, Some(_)) => return std::cmp::Ordering::Less, + | (None, None) => return std::cmp::Ordering::Equal, } } }); @@ -222,66 +222,66 @@ pub(crate) fn build_file_tree( for path in paths.0.iter() { let path = key(path); match path.rsplit_once('/') { - | None => { - flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth); - stack.clear(); - // top level file - items.push(FileTreeItem { - kind: FileTreeItemKind::File, - full_path: path.into(), - name: path.into(), - level: 0, - }); - } + | None => { + flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth); + stack.clear(); + // top level file + items.push(FileTreeItem { + kind: FileTreeItemKind::File, + full_path: path.into(), + name: path.into(), + level: 0, + }); + } - | Some((parent, _)) => { - let mut common_depth = 0; + | Some((parent, _)) => { + let mut common_depth = 0; - for (i, seg) in parent.split('/').enumerate() { - let stack_item = stack.get(i); - if stack_item.is_none() { - // segment is unseen, push to stack - stack.push(seg); - common_depth += 1; - } else if Some(&seg) == stack.get(i) { - // segment matches stack, continue comparison - common_depth += 1; - } else { - // segment differs from stack, stop comparison - break; - } - } - - if common_depth == stack.len() { - // current path is in same directory as stack, add to leafs - leafs.push(path); - base_depth = common_depth; + for (i, seg) in parent.split('/').enumerate() { + let stack_item = stack.get(i); + if stack_item.is_none() { + // segment is unseen, push to stack + stack.push(seg); + common_depth += 1; + } else if Some(&seg) == stack.get(i) { + // segment matches stack, continue comparison + common_depth += 1; } else { - // e.g. stack = ["a", "b", "c"], path = ["a", "c"] - // common dir path = "a/", stack dir path = "a/b/c", common count = 1 - // push common dir a to items - // also push stack dir a/b/c to items but strip a from name so that it becomes "b/c" with level equal to common_count - // finally push any leaf under a/b/c - - let base_dir_created = - flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth); - - // pop top of stack minus common dir - stack.truncate(common_depth); - - if base_dir_created { - emitted_depth = common_depth; - } else { - emitted_depth = 0; - } - - for seg in parent.split('/').skip(common_depth) { - stack.push(seg); - } - - leafs.push(path); + // segment differs from stack, stop comparison + break; } } + + if common_depth == stack.len() { + // current path is in same directory as stack, add to leafs + leafs.push(path); + base_depth = common_depth; + } else { + // e.g. stack = ["a", "b", "c"], path = ["a", "c"] + // common dir path = "a/", stack dir path = "a/b/c", common count = 1 + // push common dir a to items + // also push stack dir a/b/c to items but strip a from name so that it becomes "b/c" with level equal to common_count + // finally push any leaf under a/b/c + + let base_dir_created = + flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth); + + // pop top of stack minus common dir + stack.truncate(common_depth); + + if base_dir_created { + emitted_depth = common_depth; + } else { + emitted_depth = 0; + } + + for seg in parent.split('/').skip(common_depth) { + stack.push(seg); + } + + leafs.push(path); + } + } } }