//! Text diff computation for incremental buffer updates.//!//! This module provides functionality for computing minimal diffs between two text strings,//! which is used for auto-reloading files without wiping undo history or disrupting anchors.use std::ops::Range;use imara_diff::{Algorithm, Diff, InternedInput, Token};use string_offset::{ByteOffset, CharOffset};use super::buffer::{Buffer, ToBufferCharOffset};/// A computed diff between two strings.////// The edits are represented as byte ranges in the old text and their replacement strings./// These can be applied to transform the old text into the new text.#[derive(Debug, Clone, PartialEq, Eq)]pub struct TextDiff { /// List of edits: (old_byte_range, new_text) pub edits: Vec<(Range<usize>, String)>,}impl TextDiff { /// Returns true if this diff represents no changes. pub fn is_empty(&self) -> bool { self.edits.is_empty() } /// Convert byte-range edits to CharOffset-range edits. /// /// The buffer uses 1-indexed coordinates (first editable character is at CharOffset(1)). /// The diff byte offsets are 0-indexed relative to the plain text, so we add 1 to /// convert to the buffer's 1-indexed system before using to_buffer_char_offset. pub fn to_char_offset_edits(&self, buffer: &Buffer) -> Vec<(Range<CharOffset>, String)> { self.edits .iter() .map(|(byte_range, new_text)| { // Add 1 to convert from 0-indexed plain text byte offset to 1-indexed buffer byte offset let start = ByteOffset::from(byte_range.start + 1).to_buffer_char_offset(buffer); let end = ByteOffset::from(byte_range.end + 1).to_buffer_char_offset(buffer); (start..end, new_text.clone()) }) .collect() }}/// Compute a diff between two strings.////// This uses a line-based diff algorithm (Histogram) for efficiency./// Returns a list of edits as (byte_range_in_old, replacement_text) pairs.pub async fn text_diff(old_text: &str, new_text: &str) -> TextDiff { let input = InternedInput::new(old_text, new_text); // Yield here to prevent doing more work if the task is aborted. futures_lite::future::yield_now().await; // Only compute line-based diff for now. Zed does more fine-grained word-level diffing for smaller hunks // but I don't think it's worth it for our use case. let edits = diff_internal(&input, new_text).await; TextDiff { edits }}async fn diff_internal(input: &InternedInput<&str>, new_text: &str) -> Vec<(Range<usize>, String)> { let mut old_offset = 0; let mut new_offset = 0; let mut old_token_ix = 0; let mut new_token_ix = 0; let mut edits = Vec::new(); let diff = Diff::compute(Algorithm::Histogram, input); // Yield here to prevent doing more work if the task is aborted. futures_lite::future::yield_now().await; for hunk in diff.hunks() { // Calculate byte offsets for unchanged tokens before this hunk old_offset += token_len( input, &input.before[old_token_ix as usize..hunk.before.start as usize], ); new_offset += token_len( input, &input.after[new_token_ix as usize..hunk.after.start as usize], ); // Calculate byte lengths of the changed tokens let old_len = token_len( input, &input.before[hunk.before.start as usize..hunk.before.end as usize], ); let new_len = token_len( input, &input.after[hunk.after.start as usize..hunk.after.end as usize], ); let old_byte_range = old_offset..old_offset + old_len; let new_byte_range = new_offset..new_offset + new_len; old_token_ix = hunk.before.end; new_token_ix = hunk.after.end; old_offset = old_byte_range.end; new_offset = new_byte_range.end; let replacement = if new_byte_range.is_empty() { String::new() } else { new_text[new_byte_range].to_string() }; edits.push((old_byte_range, replacement)); } edits}/// Calculate total byte length of a sequence of tokens.fn token_len(input: &InternedInput<&str>, tokens: &[Token]) -> usize { tokens .iter() .map(|token| input.interner[*token].len()) .sum()}
//! Text diff computation for incremental buffer updates.
//!
//! This module provides functionality for computing minimal diffs between two text strings,
//! which is used for auto-reloading files without wiping undo history or disrupting anchors.
use std::ops::Range;
use imara_diff::{Algorithm, Diff, InternedInput, Token};
use string_offset::{ByteOffset, CharOffset};
use super::buffer::{Buffer, ToBufferCharOffset};
/// A computed diff between two strings.
///
/// The edits are represented as byte ranges in the old text and their replacement strings.
/// These can be applied to transform the old text into the new text.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextDiff {
/// List of edits: (old_byte_range, new_text)
pub edits: Vec<(Range<usize>, String)>,
}
impl TextDiff {
/// Returns true if this diff represents no changes.
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
/// Convert byte-range edits to CharOffset-range edits.
///
/// The buffer uses 1-indexed coordinates (first editable character is at CharOffset(1)).
/// The diff byte offsets are 0-indexed relative to the plain text, so we add 1 to
/// convert to the buffer's 1-indexed system before using to_buffer_char_offset.
pub fn to_char_offset_edits(&self, buffer: &Buffer) -> Vec<(Range<CharOffset>, String)> {
self.edits
.iter()
.map(|(byte_range, new_text)| {
// Add 1 to convert from 0-indexed plain text byte offset to 1-indexed buffer byte offset
let start = ByteOffset::from(byte_range.start + 1).to_buffer_char_offset(buffer);
let end = ByteOffset::from(byte_range.end + 1).to_buffer_char_offset(buffer);
(start..end, new_text.clone())
})
.collect()
}
}
/// Compute a diff between two strings.
///
/// This uses a line-based diff algorithm (Histogram) for efficiency.
/// Returns a list of edits as (byte_range_in_old, replacement_text) pairs.
pub async fn text_diff(old_text: &str, new_text: &str) -> TextDiff {
let input = InternedInput::new(old_text, new_text);
// Yield here to prevent doing more work if the task is aborted.
futures_lite::future::yield_now().await;
// Only compute line-based diff for now. Zed does more fine-grained word-level diffing for smaller hunks
// but I don't think it's worth it for our use case.
let edits = diff_internal(&input, new_text).await;
TextDiff { edits }
}
async fn diff_internal(input: &InternedInput<&str>, new_text: &str) -> Vec<(Range<usize>, String)> {
let mut old_offset = 0;
let mut new_offset = 0;
let mut old_token_ix = 0;
let mut new_token_ix = 0;
let mut edits = Vec::new();
let diff = Diff::compute(Algorithm::Histogram, input);
// Yield here to prevent doing more work if the task is aborted.
futures_lite::future::yield_now().await;
for hunk in diff.hunks() {
// Calculate byte offsets for unchanged tokens before this hunk
old_offset += token_len(
input,
&input.before[old_token_ix as usize..hunk.before.start as usize],
);
new_offset += token_len(
input,
&input.after[new_token_ix as usize..hunk.after.start as usize],
);
// Calculate byte lengths of the changed tokens
let old_len = token_len(
input,
&input.before[hunk.before.start as usize..hunk.before.end as usize],
);
let new_len = token_len(
input,
&input.after[hunk.after.start as usize..hunk.after.end as usize],
);
let old_byte_range = old_offset..old_offset + old_len;
let new_byte_range = new_offset..new_offset + new_len;
old_token_ix = hunk.before.end;
new_token_ix = hunk.after.end;
old_offset = old_byte_range.end;
new_offset = new_byte_range.end;
let replacement = if new_byte_range.is_empty() {
String::new()
} else {
new_text[new_byte_range].to_string()
};
edits.push((old_byte_range, replacement));
}
edits
}
/// Calculate total byte length of a sequence of tokens.
fn token_len(input: &InternedInput<&str>, tokens: &[Token]) -> usize {
tokens
.iter()
.map(|token| input.interner[*token].len())
.sum()
}