#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]// Adding this file level gate as some of the code around editability is not used in WASM yet. use std::collections::HashMap;use std::ops::Range;use std::rc::Rc;use std::sync::Arc; use futures::stream::AbortHandle;use itertools::Itertools;use pathfinder_color::ColorU;use rangemap::RangeMap;use similar::{ChangeTag, DiffOp, TextDiff};use string_offset::CharOffset;use warp_core::ui::theme::{AnsiColorIdentifier, Fill};use warp_editor::content::edit::TemporaryBlock;use warp_editor::content::version::BufferVersion;use warp_editor::multiline::{AnyMultilineString, LF, MultilineStr, MultilineString};use warp_editor::render::model::{Decoration, LineCount, LineDecoration};use warpui::{Entity, ModelContext}; use super::super::DiffResult;use crate::appearance::Appearance;use crate::code::editor::line::EditorLineLocation;use crate::code::editor::line_iterator::LineIterator; const OVERLAY_ALPHA: u8 = 56;const INLINE_OVERLAY_ALPHA: u8 = 71; /// Get the theme-appropriate add colorpub(crate) fn add_color(appearance: &Appearance) -> ColorU { AnsiColorIdentifier::Green .to_ansi_color(&appearance.theme().terminal_colors().normal) .into()} /// Get the theme-appropriate remove colorpub(crate) fn remove_color(appearance: &Appearance) -> ColorU { AnsiColorIdentifier::Red .to_ansi_color(&appearance.theme().terminal_colors().normal) .into()} /// Get the theme-appropriate replace colorpub(crate) fn replace_color(appearance: &Appearance) -> ColorU { AnsiColorIdentifier::Yellow .to_ansi_color(&appearance.theme().terminal_colors().normal) .into()} /// Get the theme-appropriate remove overlay colorpub(crate) fn remove_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Red.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = OVERLAY_ALPHA; color} /// Get the theme-appropriate add overlay colorpub(crate) fn add_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Green.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = OVERLAY_ALPHA; color} /// Get the theme-appropriate add inline overlay colorpub(crate) fn add_inline_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Green.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = INLINE_OVERLAY_ALPHA; color} /// Get the theme-appropriate remove inline overlay colorpub(crate) fn remove_inline_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Red.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = INLINE_OVERLAY_ALPHA; color} pub enum DiffModelEvent { DiffUpdated { version: BufferVersion }, UnifiedDiffComputed(Rc<DiffResult>),} /// Computes the unified diff (3 context lines, git style) and line stats/// between two contents.pub(crate) async fn compute_unified_diff( base: &MultilineStr<LF>, new: &MultilineStr<LF>, file_name: &str,) -> DiffResult { if base == new { return DiffResult { unified_diff: String::new(), lines_added: 0, lines_removed: 0, }; } let text_diff = TextDiff::from_lines(base.as_str(), new.as_str()); // Calculate diff statistics. let mut lines_added = 0; let mut lines_removed = 0; for op in text_diff.ops() { match op { DiffOp::Equal { .. } => (), DiffOp::Delete { old_len, .. } => lines_removed += old_len, DiffOp::Insert { new_len, .. } => lines_added += new_len, DiffOp::Replace { old_len, new_len, .. } => { lines_added += new_len; lines_removed += old_len; } } } DiffResult { unified_diff: text_diff .unified_diff() .context_radius(3) .header(file_name, file_name) .missing_newline_hint(false) .to_string(), lines_added, lines_removed, }} #[derive(Debug, Clone, Eq, PartialEq)]pub enum ChangeType { Replacement { replaced_range: Range<usize>, insertion: Vec<Range<usize>>, deletion: Vec<Range<usize>>, }, Addition,} #[derive(Debug, Default, Clone)]pub struct DiffStatus { /// A non-deletion change that maps the current range of content to an old range in base. change_mapping: RangeMap<usize, ChangeType>, /// A deletion that maps a line index in current content to an old range in base. deletion_mapping: HashMap<usize, Range<usize>>,} impl DiffStatus { /// Returns the number of lines added and removed in the current diff. pub fn get_diff_lines(&self) -> (usize, usize) { let mut lines_added = 0; let mut lines_removed = 0; // Count changes/additions for (new_range, change_type) in self.change_mapping.iter() { let new_range_lines = (LineCount::from(new_range.end) - LineCount::from(new_range.start)).as_usize(); match change_type { ChangeType::Addition => lines_added += new_range_lines, ChangeType::Replacement { replaced_range, .. } => { let old_range_lines = (LineCount::from(replaced_range.end) - LineCount::from(replaced_range.start)) .as_usize(); lines_added += new_range_lines; lines_removed += old_range_lines; } } } // Count deletions for range in self.deletion_mapping.values() { lines_removed += (LineCount::from(range.end) - LineCount::from(range.start)).as_usize(); } (lines_added, lines_removed) } /// Retrieve the hunk to render for a given line number. pub fn diff_hunk( &self, line_num: LineCount, appearance: &Appearance, ) -> Option<DiffHunkDisplay> { let line_num = line_num.as_usize(); if self.deletion_mapping.contains_key(&line_num) { return Some(DiffHunkDisplay::Remove(remove_color(appearance))); } match self.change_mapping.get(&line_num) { Some(ChangeType::Replacement { .. }) => Some(DiffHunkDisplay::Replacement { collapsed_color: replace_color(appearance), add_color: add_color(appearance), remove_color: remove_color(appearance), }), Some(ChangeType::Addition) => Some(DiffHunkDisplay::Add(add_color(appearance))), None => None, } } /// Return the range of diff hunk lines (if any) containing the given line. /// line_count is the line count assigned in the EditorWrapper element. /// This is 0-indexed. /// For deleted diff hunks, all lines will have the line_count of the line directly after /// the deleted section in the new current state of the file. /// For changed diff hunks, all lines in the old version (removed section of the hunk) /// will have the line number of the first line in the new version of the hunk. pub fn removed_diff_range(&self, line_count: LineCount) -> Option<Range<LineCount>> { let line_num = line_count.as_usize(); if self.deletion_mapping.contains_key(&(line_num)) { return Some(LineCount::from(line_num)..LineCount::from(line_num)); } // Check if this is a replacement (change with removed lines) self.added_diff_range(line_count) } /// Return the range of diff hunk lines (if any) containing the given line. /// line_count is the line count assigned in the EditorWrapper element. /// This is 0-indexed. pub fn added_diff_range(&self, line_num: LineCount) -> Option<Range<LineCount>> { let line_num = line_num.as_usize(); self.change_mapping .get_key_value(&line_num) .map(|(key, _)| LineCount::from(key.start)..LineCount::from(key.end)) }} /// The colors used to represent the diff hunks in the editor.#[derive(Debug, Clone, Copy)]pub enum DiffHunkDisplay { Add(ColorU), Replacement { collapsed_color: ColorU, add_color: ColorU, remove_color: ColorU, }, Remove(ColorU),} /// A single renderable diff hunk. It contains the information needed to decorate the render model.#[derive(Debug, Clone)]pub enum RenderableDiffHunk { Add { line_decoration: LineDecoration, }, Replace { line_decoration: LineDecoration, inline_highlights: Vec<(usize, Range<usize>)>, removed_lines: Vec<TemporaryBlock>, }, Deletion { removed_lines: Vec<TemporaryBlock>, },} /// Model that tracks the line-by-line diff status of the editor content.pub struct DiffModel { /// Store base in an Arc to avoid cloning the underlying data on every content change. base: Option<Arc<MultilineString<LF>>>, status: DiffStatus, abort_handle: Option<(AbortHandle, BufferVersion)>,} impl DiffModel { pub fn new() -> Self { Self { base: None, status: DiffStatus::default(), abort_handle: None, } } /// Total number of diff hunks in the current diff model. pub fn diff_hunk_count(&self) -> usize { self.status.change_mapping.len() + self.status.deletion_mapping.len() } /// Given an index of a diff hunk, expand it to the range of lines the hunk describes /// in the current buffer. pub fn line_range_by_diff_hunk_index(&self, index: usize) -> Option<Range<usize>> { self.added_or_changed_lines() .chain( self.status .deletion_mapping .keys() .map(|index| *index..*index), ) .sorted_by(|a, b| Ord::cmp(&a.start, &b.start)) .nth(index) } /// Get a single renderable diff hunk by its index. pub fn renderable_diff_hunk_by_index<'a>( &self, index: usize, lines: &mut LineIterator<'a, impl Iterator<Item = &'a str>>, appearance: &Appearance, ) -> Option<RenderableDiffHunk> { let (range, is_addition) = self.diff_by_index(index)?; if !is_addition { let deleted_range = self.status.deletion_mapping.get(&range.start)?; let mut removed_lines = Vec::with_capacity(deleted_range.len()); if let Ok(lines_in_range) = lines.lines_in_range(deleted_range) { for line in lines_in_range { let mut content = line.to_string(); if !content.ends_with('\n') { content.push('\n'); } removed_lines.push(TemporaryBlock { content, insert_before: LineCount::from(range.start), line_decoration: Some(remove_overlay_color(appearance).into()), inline_text_decorations: Vec::new(), }); } } Some(RenderableDiffHunk::Deletion { removed_lines }) } else { match self.status.change_mapping.get(&range.start)? { ChangeType::Replacement { replaced_range, insertion, deletion, } => { let mut removed_lines = Vec::new(); // Inline highlight indices are given over the entire multiline range. // We need to split them into per-line decorations. let mut start_char = 0; if let Ok(lines_in_range) = lines.lines_in_range(replaced_range) { for line in lines_in_range { let mut content = line.to_string(); if !content.ends_with('\n') { content.push('\n'); } let inline_text_decorations = deletion .iter() .filter_map(|inline| { let line_start = start_char; let line_end = start_char + line.chars().count(); let overlap_start = inline.start.max(line_start); let overlap_end = inline.end.min(line_end); if overlap_start < overlap_end { Some( Decoration::new( CharOffset::from(overlap_start - line_start), CharOffset::from(overlap_end - line_start), ) .with_background(Fill::Solid( remove_inline_overlay_color(appearance), )), ) } else { None } }) .collect_vec(); removed_lines.push(TemporaryBlock { content, insert_before: LineCount::from(range.start), line_decoration: Some(remove_overlay_color(appearance).into()), inline_text_decorations, }); start_char += line.chars().count() + 1; } } Some(RenderableDiffHunk::Replace { line_decoration: LineDecoration { start: LineCount::from(range.start), end: LineCount::from(range.end), overlay: add_overlay_color(appearance).into(), }, inline_highlights: insertion .iter() .map(|inline_change| (range.start, inline_change.clone())) .collect(), removed_lines, }) } ChangeType::Addition => Some(RenderableDiffHunk::Add { line_decoration: LineDecoration { start: LineCount::from(range.start), end: LineCount::from(range.end), overlay: add_overlay_color(appearance).into(), }, }), } } } /// Returns the number of diff hunks before the given line number. pub fn diff_hunk_count_before_line(&self, line: usize) -> usize { self.added_or_changed_lines() .chain( self.status .deletion_mapping .keys() .map(|index| *index..*index + 1), ) .filter(|range| range.start < line) .count() } /// Given a diff hunk index, calculate what is the reverse action that undo this diff. pub fn reverse_action_by_diff_hunk_index( &self, index: usize, ) -> Option<(Range<usize>, String)> { let line_range = self.line_range_by_diff_hunk_index(index)?; if let Some(replaced_range) = self.status.deletion_mapping.get(&line_range.start) { let text = self.base_text_by_line_range(replaced_range)?; return Some((line_range, text)); } if let Some(change) = self.status.change_mapping.get(&line_range.start) { return match change { ChangeType::Addition => Some((line_range, "".to_string())), ChangeType::Replacement { replaced_range, .. } => { let text = self.base_text_by_line_range(replaced_range)?; return Some((line_range, text)); } }; } None } /// Given a range of lines, return the corresponding substring in the base text. /// Note that all lines will end with a trailing newline. fn base_text_by_line_range(&self, replaced_range: &Range<usize>) -> Option<String> { let base_text = self.base.as_ref()?; let mut text = base_text .lines() .skip(replaced_range.start) .take(replaced_range.len()) .join("\n"); text.push('\n'); Some(text) } /// Returns the content of a deleted line given the location of a removed line. /// Returns None if the info is not for a removed line or if any lookup fails. pub fn deleted_line_content(&self, info: &EditorLineLocation) -> Option<String> { let index = self.deleted_line_to_base_line_index(info)?; self.base_line(index) } /// Convert a deleted line location to a line index in the base version of the text. pub fn deleted_line_to_base_line_index(&self, info: &EditorLineLocation) -> Option<usize> { // Extract line_number and index from Removed variant let (line_number, index) = match info { EditorLineLocation::Removed { line_number, index, .. } => (line_number.as_usize(), *index), _ => return None, }; // First check if this is a pure deletion. Note that deletion maps to line AFTER the removed line. // CODE-1638: Use line_number + 1 to align with deletion_mapping's off-by-one convention. if let Some(removed_range) = self.status.deletion_mapping.get(&line_number) { return Some(removed_range.start + index); } // Check if this is a replacement (change with removed lines) if let Some(ChangeType::Replacement { replaced_range, .. }) = self.status.change_mapping.get(&line_number) { return Some(replaced_range.start + index); } None } /// Convert a line index in the base version of the text to an editor line location. pub fn base_line_index_to_line_location(&self, index: usize) -> Option<EditorLineLocation> { for (line_range, change) in self.status.change_mapping.iter() { if let ChangeType::Replacement { replaced_range, .. } = change && replaced_range.contains(&index) { return Some(EditorLineLocation::Removed {Lines 1–500 of 812. Load more
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))] // Adding this file level gate as some of the code around editability is not used in WASM yet. use std::collections::HashMap; use std::ops::Range; use std::rc::Rc; use std::sync::Arc; use futures::stream::AbortHandle; use itertools::Itertools; use pathfinder_color::ColorU; use rangemap::RangeMap; use similar::{ChangeTag, DiffOp, TextDiff}; use string_offset::CharOffset; use warp_core::ui::theme::{AnsiColorIdentifier, Fill}; use warp_editor::content::edit::TemporaryBlock; use warp_editor::content::version::BufferVersion; use warp_editor::multiline::{AnyMultilineString, LF, MultilineStr, MultilineString}; use warp_editor::render::model::{Decoration, LineCount, LineDecoration}; use warpui::{Entity, ModelContext}; use super::super::DiffResult; use crate::appearance::Appearance; use crate::code::editor::line::EditorLineLocation; use crate::code::editor::line_iterator::LineIterator; const OVERLAY_ALPHA: u8 = 56; const INLINE_OVERLAY_ALPHA: u8 = 71; /// Get the theme-appropriate add color pub(crate) fn add_color(appearance: &Appearance) -> ColorU { AnsiColorIdentifier::Green .to_ansi_color(&appearance.theme().terminal_colors().normal) .into() } /// Get the theme-appropriate remove color pub(crate) fn remove_color(appearance: &Appearance) -> ColorU { AnsiColorIdentifier::Red .to_ansi_color(&appearance.theme().terminal_colors().normal) .into() } /// Get the theme-appropriate replace color pub(crate) fn replace_color(appearance: &Appearance) -> ColorU { AnsiColorIdentifier::Yellow .to_ansi_color(&appearance.theme().terminal_colors().normal) .into() } /// Get the theme-appropriate remove overlay color pub(crate) fn remove_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Red.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = OVERLAY_ALPHA; color } /// Get the theme-appropriate add overlay color pub(crate) fn add_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Green.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = OVERLAY_ALPHA; color } /// Get the theme-appropriate add inline overlay color pub(crate) fn add_inline_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Green.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = INLINE_OVERLAY_ALPHA; color } /// Get the theme-appropriate remove inline overlay color pub(crate) fn remove_inline_overlay_color(appearance: &Appearance) -> ColorU { let ansi_color = AnsiColorIdentifier::Red.to_ansi_color(&appearance.theme().terminal_colors().normal); let mut color: ColorU = ansi_color.into(); color.a = INLINE_OVERLAY_ALPHA; color } pub enum DiffModelEvent { DiffUpdated { version: BufferVersion }, UnifiedDiffComputed(Rc<DiffResult>), } /// Computes the unified diff (3 context lines, git style) and line stats /// between two contents. pub(crate) async fn compute_unified_diff( base: &MultilineStr<LF>, new: &MultilineStr<LF>, file_name: &str, ) -> DiffResult { if base == new { return DiffResult { unified_diff: String::new(), lines_added: 0, lines_removed: 0, }; } let text_diff = TextDiff::from_lines(base.as_str(), new.as_str()); // Calculate diff statistics. let mut lines_added = 0; let mut lines_removed = 0; for op in text_diff.ops() { match op { DiffOp::Equal { .. } => (), DiffOp::Delete { old_len, .. } => lines_removed += old_len, DiffOp::Insert { new_len, .. } => lines_added += new_len, DiffOp::Replace { old_len, new_len, .. } => { lines_added += new_len; lines_removed += old_len; } } } DiffResult { unified_diff: text_diff .unified_diff() .context_radius(3) .header(file_name, file_name) .missing_newline_hint(false) .to_string(), lines_added, lines_removed, } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum ChangeType { Replacement { replaced_range: Range<usize>, insertion: Vec<Range<usize>>, deletion: Vec<Range<usize>>, }, Addition, } #[derive(Debug, Default, Clone)] pub struct DiffStatus { /// A non-deletion change that maps the current range of content to an old range in base. change_mapping: RangeMap<usize, ChangeType>, /// A deletion that maps a line index in current content to an old range in base. deletion_mapping: HashMap<usize, Range<usize>>, } impl DiffStatus { /// Returns the number of lines added and removed in the current diff. pub fn get_diff_lines(&self) -> (usize, usize) { let mut lines_added = 0; let mut lines_removed = 0; // Count changes/additions for (new_range, change_type) in self.change_mapping.iter() { let new_range_lines = (LineCount::from(new_range.end) - LineCount::from(new_range.start)).as_usize(); match change_type { ChangeType::Addition => lines_added += new_range_lines, ChangeType::Replacement { replaced_range, .. } => { let old_range_lines = (LineCount::from(replaced_range.end) - LineCount::from(replaced_range.start)) .as_usize(); lines_added += new_range_lines; lines_removed += old_range_lines; } } } // Count deletions for range in self.deletion_mapping.values() { lines_removed += (LineCount::from(range.end) - LineCount::from(range.start)).as_usize(); } (lines_added, lines_removed) } /// Retrieve the hunk to render for a given line number. pub fn diff_hunk( &self, line_num: LineCount, appearance: &Appearance, ) -> Option<DiffHunkDisplay> { let line_num = line_num.as_usize(); if self.deletion_mapping.contains_key(&line_num) { return Some(DiffHunkDisplay::Remove(remove_color(appearance))); } match self.change_mapping.get(&line_num) { Some(ChangeType::Replacement { .. }) => Some(DiffHunkDisplay::Replacement { collapsed_color: replace_color(appearance), add_color: add_color(appearance), remove_color: remove_color(appearance), }), Some(ChangeType::Addition) => Some(DiffHunkDisplay::Add(add_color(appearance))), None => None, } } /// Return the range of diff hunk lines (if any) containing the given line. /// line_count is the line count assigned in the EditorWrapper element. /// This is 0-indexed. /// For deleted diff hunks, all lines will have the line_count of the line directly after /// the deleted section in the new current state of the file. /// For changed diff hunks, all lines in the old version (removed section of the hunk) /// will have the line number of the first line in the new version of the hunk. pub fn removed_diff_range(&self, line_count: LineCount) -> Option<Range<LineCount>> { let line_num = line_count.as_usize(); if self.deletion_mapping.contains_key(&(line_num)) { return Some(LineCount::from(line_num)..LineCount::from(line_num)); } // Check if this is a replacement (change with removed lines) self.added_diff_range(line_count) } /// Return the range of diff hunk lines (if any) containing the given line. /// line_count is the line count assigned in the EditorWrapper element. /// This is 0-indexed. pub fn added_diff_range(&self, line_num: LineCount) -> Option<Range<LineCount>> { let line_num = line_num.as_usize(); self.change_mapping .get_key_value(&line_num) .map(|(key, _)| LineCount::from(key.start)..LineCount::from(key.end)) } } /// The colors used to represent the diff hunks in the editor. #[derive(Debug, Clone, Copy)] pub enum DiffHunkDisplay { Add(ColorU), Replacement { collapsed_color: ColorU, add_color: ColorU, remove_color: ColorU, }, Remove(ColorU), } /// A single renderable diff hunk. It contains the information needed to decorate the render model. #[derive(Debug, Clone)] pub enum RenderableDiffHunk { Add { line_decoration: LineDecoration, }, Replace { line_decoration: LineDecoration, inline_highlights: Vec<(usize, Range<usize>)>, removed_lines: Vec<TemporaryBlock>, }, Deletion { removed_lines: Vec<TemporaryBlock>, }, } /// Model that tracks the line-by-line diff status of the editor content. pub struct DiffModel { /// Store base in an Arc to avoid cloning the underlying data on every content change. base: Option<Arc<MultilineString<LF>>>, status: DiffStatus, abort_handle: Option<(AbortHandle, BufferVersion)>, } impl DiffModel { pub fn new() -> Self { Self { base: None, status: DiffStatus::default(), abort_handle: None, } } /// Total number of diff hunks in the current diff model. pub fn diff_hunk_count(&self) -> usize { self.status.change_mapping.len() + self.status.deletion_mapping.len() } /// Given an index of a diff hunk, expand it to the range of lines the hunk describes /// in the current buffer. pub fn line_range_by_diff_hunk_index(&self, index: usize) -> Option<Range<usize>> { self.added_or_changed_lines() .chain( self.status .deletion_mapping .keys() .map(|index| *index..*index), ) .sorted_by(|a, b| Ord::cmp(&a.start, &b.start)) .nth(index) } /// Get a single renderable diff hunk by its index. pub fn renderable_diff_hunk_by_index<'a>( &self, index: usize, lines: &mut LineIterator<'a, impl Iterator<Item = &'a str>>, appearance: &Appearance, ) -> Option<RenderableDiffHunk> { let (range, is_addition) = self.diff_by_index(index)?; if !is_addition { let deleted_range = self.status.deletion_mapping.get(&range.start)?; let mut removed_lines = Vec::with_capacity(deleted_range.len()); if let Ok(lines_in_range) = lines.lines_in_range(deleted_range) { for line in lines_in_range { let mut content = line.to_string(); if !content.ends_with('\n') { content.push('\n'); } removed_lines.push(TemporaryBlock { content, insert_before: LineCount::from(range.start), line_decoration: Some(remove_overlay_color(appearance).into()), inline_text_decorations: Vec::new(), }); } } Some(RenderableDiffHunk::Deletion { removed_lines }) } else { match self.status.change_mapping.get(&range.start)? { ChangeType::Replacement { replaced_range, insertion, deletion, } => { let mut removed_lines = Vec::new(); // Inline highlight indices are given over the entire multiline range. // We need to split them into per-line decorations. let mut start_char = 0; if let Ok(lines_in_range) = lines.lines_in_range(replaced_range) { for line in lines_in_range { let mut content = line.to_string(); if !content.ends_with('\n') { content.push('\n'); } let inline_text_decorations = deletion .iter() .filter_map(|inline| { let line_start = start_char; let line_end = start_char + line.chars().count(); let overlap_start = inline.start.max(line_start); let overlap_end = inline.end.min(line_end); if overlap_start < overlap_end { Some( Decoration::new( CharOffset::from(overlap_start - line_start), CharOffset::from(overlap_end - line_start), ) .with_background(Fill::Solid( remove_inline_overlay_color(appearance), )), ) } else { None } }) .collect_vec(); removed_lines.push(TemporaryBlock { content, insert_before: LineCount::from(range.start), line_decoration: Some(remove_overlay_color(appearance).into()), inline_text_decorations, }); start_char += line.chars().count() + 1; } } Some(RenderableDiffHunk::Replace { line_decoration: LineDecoration { start: LineCount::from(range.start), end: LineCount::from(range.end), overlay: add_overlay_color(appearance).into(), }, inline_highlights: insertion .iter() .map(|inline_change| (range.start, inline_change.clone())) .collect(), removed_lines, }) } ChangeType::Addition => Some(RenderableDiffHunk::Add { line_decoration: LineDecoration { start: LineCount::from(range.start), end: LineCount::from(range.end), overlay: add_overlay_color(appearance).into(), }, }), } } } /// Returns the number of diff hunks before the given line number. pub fn diff_hunk_count_before_line(&self, line: usize) -> usize { self.added_or_changed_lines() .chain( self.status .deletion_mapping .keys() .map(|index| *index..*index + 1), ) .filter(|range| range.start < line) .count() } /// Given a diff hunk index, calculate what is the reverse action that undo this diff. pub fn reverse_action_by_diff_hunk_index( &self, index: usize, ) -> Option<(Range<usize>, String)> { let line_range = self.line_range_by_diff_hunk_index(index)?; if let Some(replaced_range) = self.status.deletion_mapping.get(&line_range.start) { let text = self.base_text_by_line_range(replaced_range)?; return Some((line_range, text)); } if let Some(change) = self.status.change_mapping.get(&line_range.start) { return match change { ChangeType::Addition => Some((line_range, "".to_string())), ChangeType::Replacement { replaced_range, .. } => { let text = self.base_text_by_line_range(replaced_range)?; return Some((line_range, text)); } }; } None } /// Given a range of lines, return the corresponding substring in the base text. /// Note that all lines will end with a trailing newline. fn base_text_by_line_range(&self, replaced_range: &Range<usize>) -> Option<String> { let base_text = self.base.as_ref()?; let mut text = base_text .lines() .skip(replaced_range.start) .take(replaced_range.len()) .join("\n"); text.push('\n'); Some(text) } /// Returns the content of a deleted line given the location of a removed line. /// Returns None if the info is not for a removed line or if any lookup fails. pub fn deleted_line_content(&self, info: &EditorLineLocation) -> Option<String> { let index = self.deleted_line_to_base_line_index(info)?; self.base_line(index) } /// Convert a deleted line location to a line index in the base version of the text. pub fn deleted_line_to_base_line_index(&self, info: &EditorLineLocation) -> Option<usize> { // Extract line_number and index from Removed variant let (line_number, index) = match info { EditorLineLocation::Removed { line_number, index, .. } => (line_number.as_usize(), *index), _ => return None, }; // First check if this is a pure deletion. Note that deletion maps to line AFTER the removed line. // CODE-1638: Use line_number + 1 to align with deletion_mapping's off-by-one convention. if let Some(removed_range) = self.status.deletion_mapping.get(&line_number) { return Some(removed_range.start + index); } // Check if this is a replacement (change with removed lines) if let Some(ChangeType::Replacement { replaced_range, .. }) = self.status.change_mapping.get(&line_number) { return Some(replaced_range.start + index); } None } /// Convert a line index in the base version of the text to an editor line location. pub fn base_line_index_to_line_location(&self, index: usize) -> Option<EditorLineLocation> { for (line_range, change) in self.status.change_mapping.iter() { if let ChangeType::Replacement { replaced_range, .. } = change && replaced_range.contains(&index) { return Some(EditorLineLocation::Removed {