872 lines · 25.5 KB · Rust
RawDownload
use language::{BufferSnapshot, Diff, Point, ToOffset};use project::search::SearchQuery;use std::iter;use util::{ResultExt as _, paths::PathMatcher}; /// Performs an exact string replacement in a buffer, requiring precise character-for-character matching./// Uses the search functionality to locate the first occurrence of the exact string./// Returns None if no exact match is found in the buffer.pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> Option<Diff> {    let query = SearchQuery::text(        old,        false,        true,        true,        PathMatcher::new(iter::empty::<&str>()).ok()?,        PathMatcher::new(iter::empty::<&str>()).ok()?,        false,        None,    )    .log_err()?;     let matches = query.search(&snapshot, None).await;     if matches.is_empty() {        return None;    }     let edit_range = matches[0].clone();    let diff = language::text_diff(&old, &new);     let edits = diff        .into_iter()        .map(|(old_range, text)| {            let start = edit_range.start + old_range.start;            let end = edit_range.start + old_range.end;            (start..end, text)        })        .collect::<Vec<_>>();     let diff = language::Diff {        base_version: snapshot.version().clone(),        line_ending: snapshot.line_ending(),        edits,    };     Some(diff)} /// Performs a replacement that's indentation-aware - matches text content ignoring leading whitespace differences./// When replacing, preserves the indentation level found in the buffer at each matching line./// Returns None if no match found or if indentation is offset inconsistently across matched lines.pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapshot) -> Option<Diff> {    let (old_lines, old_min_indent) = lines_with_min_indent(old);    let (new_lines, new_min_indent) = lines_with_min_indent(new);    let min_indent = old_min_indent.min(new_min_indent);     let old_lines = drop_lines_prefix(&old_lines, min_indent);    let new_lines = drop_lines_prefix(&new_lines, min_indent);     let max_row = buffer.max_point().row;     'windows: for start_row in 0..max_row + 1 {        let end_row = start_row + old_lines.len().saturating_sub(1) as u32;         if end_row > max_row {            // The buffer ends before fully matching the pattern            return None;        }         let start_point = Point::new(start_row, 0);        let end_point = Point::new(end_row, buffer.line_len(end_row));        let range = start_point.to_offset(buffer)..end_point.to_offset(buffer);         let window_text = buffer.text_for_range(range.clone());        let mut window_lines = window_text.lines();        let mut old_lines_iter = old_lines.iter();         let mut common_mismatch = None;         #[derive(Eq, PartialEq)]        enum Mismatch {            OverIndented(String),            UnderIndented(String),        }         while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())        {            let line_trimmed = window_line.trim_start();             if line_trimmed != old_line.trim_start() {                continue 'windows;            }             if line_trimmed.is_empty() {                continue;            }             let line_mismatch = if window_line.len() > old_line.len() {                let prefix = window_line[..window_line.len() - old_line.len()].to_string();                Mismatch::UnderIndented(prefix)            } else {                let prefix = old_line[..old_line.len() - window_line.len()].to_string();                Mismatch::OverIndented(prefix)            };             match &common_mismatch {                Some(common_mismatch) if common_mismatch != &line_mismatch => {                    continue 'windows;                }                Some(_) => (),                None => common_mismatch = Some(line_mismatch),            }        }         if let Some(common_mismatch) = &common_mismatch {            let line_ending = buffer.line_ending();            let replacement = new_lines                .iter()                .map(|new_line| {                    if new_line.trim().is_empty() {                        new_line.to_string()                    } else {                        match common_mismatch {                            Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,                            Mismatch::OverIndented(prefix) => new_line                                .strip_prefix(prefix)                                .unwrap_or(new_line)                                .to_string(),                        }                    }                })                .collect::<Vec<_>>()                .join(line_ending.as_str());             let diff = Diff {                base_version: buffer.version().clone(),                line_ending,                edits: vec![(range, replacement.into())],            };             return Some(diff);        }    }     None} fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> {    lines        .iter()        .map(|line| line.get(prefix_len..).unwrap_or(""))        .collect()} fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {    let mut lines = Vec::new();    let mut min_indent: Option<usize> = None;     for line in input.lines() {        lines.push(line);        if !line.trim().is_empty() {            let indent = line.len() - line.trim_start().len();            min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));        }    }     (lines, min_indent.unwrap_or(0))} #[cfg(test)]mod replace_exact_tests {    use super::*;    use gpui::TestAppContext;    use gpui::prelude::*;     #[gpui::test]    async fn basic(cx: &mut TestAppContext) {        let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;        assert_eq!(result, Some("let x = 42;".to_string()));    }     #[gpui::test]    async fn no_match(cx: &mut TestAppContext) {        let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;        assert_eq!(result, None);    }     #[gpui::test]    async fn multi_line(cx: &mut TestAppContext) {        let whole = "fn example() {\n    let x = 41;\n    println!(\"x = {}\", x);\n}";        let old_text = "    let x = 41;\n    println!(\"x = {}\", x);";        let new_text = "    let x = 42;\n    println!(\"x = {}\", x);";        let result = test_replace_exact(cx, whole, old_text, new_text).await;        assert_eq!(            result,            Some("fn example() {\n    let x = 42;\n    println!(\"x = {}\", x);\n}".to_string())        );    }     #[gpui::test]    async fn multiple_occurrences(cx: &mut TestAppContext) {        let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";        let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;        assert_eq!(            result,            Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())        );    }     #[gpui::test]    async fn empty_buffer(cx: &mut TestAppContext) {        let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;        assert_eq!(result, None);    }     #[gpui::test]    async fn partial_match(cx: &mut TestAppContext) {        let whole = "let x = 41; let y = 42;";        let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;        assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));    }     #[gpui::test]    async fn whitespace_sensitive(cx: &mut TestAppContext) {        let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;        assert_eq!(result, None);    }     #[gpui::test]    async fn entire_buffer(cx: &mut TestAppContext) {        let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;        assert_eq!(result, Some("let x = 42;".to_string()));    }     async fn test_replace_exact(        cx: &mut TestAppContext,        whole: &str,        old: &str,        new: &str,    ) -> Option<String> {        let buffer = cx.new(|cx| language::Buffer::local(whole, cx));         let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());         let diff = replace_exact(old, new, &buffer_snapshot).await;        diff.map(|diff| {            buffer.update(cx, |buffer, cx| {                let _ = buffer.apply_diff(diff, cx);                buffer.text()            })        })    }} #[cfg(test)]mod flexible_indent_tests {    use super::*;    use gpui::TestAppContext;    use gpui::prelude::*;    use unindent::Unindent;     #[gpui::test]    fn test_underindented_single_line(cx: &mut TestAppContext) {        let cur = "        let a = 41;".to_string();        let old = "    let a = 41;".to_string();        let new = "    let a = 42;".to_string();        let exp = "        let a = 42;".to_string();         let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);         assert_eq!(result, Some(exp.to_string()))    }     #[gpui::test]    fn test_overindented_single_line(cx: &mut TestAppContext) {        let cur = "    let a = 41;".to_string();        let old = "        let a = 41;".to_string();        let new = "        let a = 42;".to_string();        let exp = "    let a = 42;".to_string();         let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);         assert_eq!(result, Some(exp.to_string()))    }     #[gpui::test]    fn test_underindented_multi_line(cx: &mut TestAppContext) {        let whole = r#"            fn test() {                let x = 5;                println!("x = {}", x);                let y = 10;            }        "#        .unindent();         let old = r#"            let x = 5;            println!("x = {}", x);        "#        .unindent();         let new = r#"            let x = 42;            println!("New value: {}", x);        "#        .unindent();         let expected = r#"            fn test() {                let x = 42;                println!("New value: {}", x);                let y = 10;            }        "#        .unindent();         assert_eq!(            test_replace_with_flexible_indent(cx, &whole, &old, &new),            Some(expected.to_string())        );    }     #[gpui::test]    fn test_overindented_multi_line(cx: &mut TestAppContext) {        let cur = r#"            fn foo() {                let a = 41;                let b = 3.13;            }        "#        .unindent();         // 6 space indent instead of 4        let old = "      let a = 41;\n      let b = 3.13;";        let new = "      let a = 42;\n      let b = 3.14;";         let expected = r#"            fn foo() {                let a = 42;                let b = 3.14;            }        "#        .unindent();         let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);         assert_eq!(result, Some(expected.to_string()))    }     #[gpui::test]    fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {        let whole = r#"            fn test() {                if condition {                    println!("{}", 43);                }            }        "#        .unindent();         let old = r#"            if condition {            println!("{}", 43);        "#        .unindent();         let new = r#"            if condition {            println!("{}", 42);        "#        .unindent();         assert_eq!(            test_replace_with_flexible_indent(cx, &whole, &old, &new),            None        );    }     #[gpui::test]    fn test_replace_with_empty_lines(cx: &mut TestAppContext) {        // Test with empty lines        let whole = r#"            fn test() {                let x = 5;                 println!("x = {}", x);            }        "#        .unindent();         let old = r#"            let x = 5;             println!("x = {}", x);        "#        .unindent();         let new = r#"            let x = 10;             println!("New x: {}", x);        "#        .unindent();         let expected = r#"            fn test() {                let x = 10;                 println!("New x: {}", x);            }        "#        .unindent();         assert_eq!(            test_replace_with_flexible_indent(cx, &whole, &old, &new),            Some(expected.to_string())        );    }     #[gpui::test]    fn test_replace_no_match(cx: &mut TestAppContext) {        let whole = r#"            fn test() {                let x = 5;            }        "#        .unindent();         let old = r#"            let y = 10;        "#        .unindent();         let new = r#"            let y = 20;        "#        .unindent();         assert_eq!(            test_replace_with_flexible_indent(cx, &whole, &old, &new),            None        );    }     #[gpui::test]    fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) {        let whole = r#"            fn test() {                let x = 5;        "#        .unindent();         let old = r#"            let x = 5;            println!("x = {}", x);        "#        .unindent();         let new = r#"            let x = 10;            println!("x = {}", x);        "#        .unindent();         // Should return None because whole doesn't fully contain the old text        assert_eq!(            test_replace_with_flexible_indent(cx, &whole, &old, &new),            None        );    }     #[gpui::test]    fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {        let whole = r#"            let x = 5;        "#        .unindent();         let old = r#"            let x = 5;            let y = 10;        "#        .unindent();         let new = r#"            let x = 5;            let y = 20;        "#        .unindent();         assert_eq!(            test_replace_with_flexible_indent(cx, &whole, &old, &new),            None        );    }     #[gpui::test]    fn test_replace_old_is_empty(cx: &mut TestAppContext) {        let whole = r#"

Lines 1–500 of 872.