@@ -59,10 +59,8 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
let max_row = buffer.max_point().row;
- 'windows: for start_row in 0..max_row.saturating_sub(old_lines.len() as u32 - 1) {
- let mut common_leading = None;
-
- let end_row = start_row + old_lines.len() as u32 - 1;
+ '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
@@ -77,6 +75,14 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
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();
@@ -89,18 +95,24 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
continue;
}
- let line_leading = &window_line[..window_line.len() - old_line.len()];
+ 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_leading {
- Some(common_leading) if common_leading != line_leading => {
+ match &common_mismatch {
+ Some(common_mismatch) if common_mismatch != &line_mismatch => {
continue 'windows;
}
Some(_) => (),
- None => common_leading = Some(line_leading.to_string()),
+ None => common_mismatch = Some(line_mismatch),
}
}
- if let Some(common_leading) = common_leading {
+ if let Some(common_mismatch) = &common_mismatch {
let line_ending = buffer.line_ending();
let replacement = new_lines
.iter()
@@ -108,7 +120,13 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
if new_line.trim().is_empty() {
new_line.to_string()
} else {
- common_leading.to_string() + new_line
+ 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<_>>()
@@ -150,14 +168,123 @@ fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
}
#[cfg(test)]
-mod tests {
+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_replace_consistent_indentation(cx: &mut TestAppContext) {
+ 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;
@@ -194,6 +321,33 @@ mod tests {
);
}
+ #[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#"
@@ -266,7 +420,6 @@ mod tests {
#[gpui::test]
fn test_replace_no_match(cx: &mut TestAppContext) {
- // Test with no match
let whole = r#"
fn test() {
let x = 5;
@@ -317,6 +470,71 @@ mod tests {
);
}
+ #[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#"
+ fn test() {
+ let x = 5;
+ }
+ "#
+ .unindent();
+
+ let old = "";
+ let new = r#"
+ let y = 10;
+ "#
+ .unindent();
+
+ assert_eq!(
+ test_replace_with_flexible_indent(cx, &whole, &old, &new),
+ None
+ );
+ }
+
+ #[gpui::test]
+ fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
+ let whole = "";
+ let old = r#"
+ let x = 5;
+ "#
+ .unindent();
+
+ let new = r#"
+ let x = 10;
+ "#
+ .unindent();
+
+ assert_eq!(
+ test_replace_with_flexible_indent(cx, &whole, &old, &new),
+ None
+ );
+ }
+
#[test]
fn test_lines_with_min_indent() {
// Empty string
@@ -504,6 +722,133 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_replace_exact_basic(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
+ assert!(diff.is_some());
+
+ let diff = diff.unwrap();
+ assert_eq!(diff.edits.len(), 1);
+
+ let result = buffer.update(cx, |buffer, cx| {
+ let _ = buffer.apply_diff(diff, cx);
+ buffer.text()
+ });
+
+ assert_eq!(result, "let x = 42;");
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
+ assert!(diff.is_none());
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| {
+ language::Buffer::local(
+ "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}",
+ cx,
+ )
+ });
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ let old_text = " let x = 41;\n println!(\"x = {}\", x);";
+ let new_text = " let x = 42;\n println!(\"x = {}\", x);";
+ let diff = replace_exact(old_text, new_text, &snapshot).await;
+ assert!(diff.is_some());
+
+ let diff = diff.unwrap();
+ let result = buffer.update(cx, |buffer, cx| {
+ let _ = buffer.apply_diff(diff, cx);
+ buffer.text()
+ });
+
+ assert_eq!(
+ result,
+ "fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
+ let buffer =
+ cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ // Should replace only the first occurrence
+ let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
+ assert!(diff.is_some());
+
+ let diff = diff.unwrap();
+ let result = buffer.update(cx, |buffer, cx| {
+ let _ = buffer.apply_diff(diff, cx);
+ buffer.text()
+ });
+
+ assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| language::Buffer::local("", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
+ assert!(diff.is_none());
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ // Verify substring replacement actually works
+ let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
+ assert!(diff.is_some());
+
+ let diff = diff.unwrap();
+ let result = buffer.update(cx, |buffer, cx| {
+ let _ = buffer.apply_diff(diff, cx);
+ buffer.text()
+ });
+
+ assert_eq!(result, "let x = 42; let y = 42;");
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
+ assert!(diff.is_none());
+ }
+
+ #[gpui::test]
+ async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
+ let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+ let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
+ assert!(diff.is_some());
+
+ let diff = diff.unwrap();
+ let result = buffer.update(cx, |buffer, cx| {
+ let _ = buffer.apply_diff(diff, cx);
+ buffer.text()
+ });
+
+ assert_eq!(result, "let x = 42;");
+ }
+
fn test_replace_with_flexible_indent(
cx: &mut TestAppContext,
whole: &str,