first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+77 -15
View File
@@ -1,14 +1,13 @@
use std::cmp::Ordering;
use std::fmt::{self, Display};
use std::ops::Range;
use std::path::PathBuf;
use std::sync::LazyLock;
use itertools::{EitherOrBoth, Itertools};
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
fmt::{self, Display},
ops::Range,
path::PathBuf,
sync::LazyLock,
};
use strsim::jaro_winkler;
lazy_static! {
/// Regex to parse a line number from a string in the format "{number}|{line}"
@@ -283,6 +282,35 @@ fn unmatched_line_suffix<'a>(search_line: &str, file_line: &'a str) -> Option<&'
}
}
/// Preserve the unmatched suffix of the final matched file line when the LLM emitted only a
/// partial final line.
///
/// When search and replace have the same line count, preserve the suffix to maintain the legacy
/// behavior for simple partial-line replacements. When line counts differ, only preserve it if the
/// replacement's final line still carries the same partial context as the search's final line.
fn append_unmatched_line_suffix(search: &str, file_line: &str, insertion: &mut String) {
let Some(search_last_line) = lines(search).last() else {
return;
};
let Some(suffix) = unmatched_line_suffix(search_last_line, file_line) else {
return;
};
let search_line_count = lines(search).count();
let insertion_line_count = lines(insertion).count();
let Some(insertion_last_line) = lines(insertion).last() else {
return;
};
if search_line_count != insertion_line_count
&& search_last_line.trim_start() != insertion_last_line.trim_start()
{
return;
}
let insertion_point = insertion.trim_end_matches('\n').len();
insertion.insert_str(insertion_point, suffix);
}
/// We told the model not to include line numbers for the replacement content. However, it can
/// still happen. Try to remove them here.
/// https://github.com/warpdotdev/warp-server/blob/d9c1b6d1443290f2355979ae552d41af01a63bde/logic/ai/prompt/tools/suggest_diff.yaml#L34-L34
@@ -402,6 +430,14 @@ pub fn fuzzy_match_v4a_diffs(
}
}
// Sort by start line and remove overlapping deltas. When the LLM produces
// multiple hunks targeting the same region (e.g. a large deletion whose
// matched range subsumes a nearby single-line edit), the overlapping delta
// must be dropped — applying both would produce an invalid edit range in
// the editor buffer (see WARP-CLIENT-DEV-NYY).
deltas.sort_by_key(|d| d.replacement_line_range.start);
deltas = deduplicate_overlapping_deltas(deltas);
let update_deltas_empty = deltas.is_empty();
let failures = if failures.fuzzy_match_failures > 0
|| failures.missing_line_numbers > 0
@@ -420,6 +456,33 @@ pub fn fuzzy_match_v4a_diffs(
}
}
/// Given a list of `DiffDelta`s sorted by `replacement_line_range.start`,
/// drop any delta whose range overlaps with the preceding accepted delta.
///
/// "Overlaps" means `B.start < A.end` (strictly inside or partial overlap).
/// Adjacent ranges (`A.end == B.start`) are kept.
fn deduplicate_overlapping_deltas(sorted_deltas: Vec<DiffDelta>) -> Vec<DiffDelta> {
let mut result: Vec<DiffDelta> = Vec::with_capacity(sorted_deltas.len());
for delta in sorted_deltas {
let dominated = result.last().is_some_and(|prev| {
delta.replacement_line_range.start < prev.replacement_line_range.end
});
if dominated {
log::warn!(
"Dropping V4A delta with overlapping range {:?} \
(subsumed by preceding delta with range {:?})",
delta.replacement_line_range,
result.last().unwrap().replacement_line_range,
);
continue;
}
result.push(delta);
}
result
}
fn fuzzy_match_file_diffs(
diffs: &[SearchAndReplace],
file_content: &str,
@@ -547,13 +610,12 @@ fn fuzzy_match_file_diffs(
// the delta would replace the entire line and drop the unmatched
// suffix. Detect this and preserve the suffix in the insertion.
let mut insertion = diff.replace.clone();
if range.end >= 2 && lines(&search).count() == lines(&insertion).count() {
if let Some(suffix) = lines(&search)
.last()
.and_then(|last| unmatched_line_suffix(last, target_lines[range.end - 2]))
{
insertion.push_str(suffix);
}
if range.end >= 2 {
append_unmatched_line_suffix(
&search,
target_lines[range.end - 2],
&mut insertion,
);
}
deltas.push(DiffDelta {
replacement_line_range: range.start..range.end,
@@ -1208,5 +1270,5 @@ fn find_change_context_start(change_context: &[String], file_lines: &[&str]) ->
}
#[cfg(test)]
#[path = "mod_test.rs"]
#[path = "mod_tests.rs"]
mod tests;
@@ -608,6 +608,66 @@ fn test_partial_last_line_in_search_preserves_suffix() {
assert_eq!(result, "func foo() {\nlet y = 1;\nlet x = 2;\n}\n");
}
#[test]
fn test_partial_last_line_in_multiline_replacement_preserves_suffix() {
// This mirrors a model edit that deletes middle lines while leaving the final line as partial
// trailing context. The final line should remain a no-op after suffix preservation.
let file_content = "\
mod proxy;
pub fn run_daemon() -> anyhow::Result<()> {
// Logging is now handled by init_common (log_destination: File).
// socket_path: ~/.warp[-channel]/remote-server/server.sock
// The Unix domain socket the daemon binds on.
}
";
let diffs = [SearchAndReplace {
search: "\
2|pub fn run_daemon() -> anyhow::Result<()> {
3| // Logging is now handled by init_common (log_destination: File).
4|
5| // socket_path:"
.to_string(),
replace: "\
pub fn run_daemon() -> anyhow::Result<()> {
// socket_path:"
.to_string(),
}];
let (deltas, _failures) = fuzzy_match_file_diffs(&diffs, file_content);
assert_eq!(deltas.len(), 1, "Expected one matched delta");
assert_eq!(deltas[0].replacement_line_range, 2..6);
assert_eq!(
deltas[0].insertion,
"pub fn run_daemon() -> anyhow::Result<()> {\n // socket_path: ~/.warp[-channel]/remote-server/server.sock"
);
let file_lines: Vec<&str> = file_content.lines().collect();
let range = &deltas[0].replacement_line_range;
let mut result = String::new();
for line in &file_lines[..range.start - 1] {
result.push_str(line);
result.push('\n');
}
result.push_str(&deltas[0].insertion);
result.push('\n');
for line in &file_lines[range.end - 1..] {
result.push_str(line);
result.push('\n');
}
assert_eq!(
result,
"\
mod proxy;
pub fn run_daemon() -> anyhow::Result<()> {
// socket_path: ~/.warp[-channel]/remote-server/server.sock
// The Unix domain socket the daemon binds on.
}
"
);
}
#[test]
fn test_search_and_replace_accommodates_none() {
let parsed_diff = ParsedDiff::StrReplaceEdit {
@@ -679,3 +739,88 @@ fn test_custom_lines() {
assert_eq!(lines("foo\nbar").collect_vec(), vec!["foo", "bar"]);
assert_eq!(lines("foo\nbar\n").collect_vec(), vec!["foo", "bar"]);
}
/// Regression test for WARP-CLIENT-DEV-NYY: panic "Invalid edit range 4042..3982".
///
/// Reproduces the crash from MAA conversation d71bf84b (request b621adb3).
/// Two V4A hunks target the same region: a large deletion whose matched range
/// subsumes a nearby single-line edit. Without `deduplicate_overlapping_deltas`,
/// both deltas survive and `Buffer::edit` panics on the overlapping ranges.
#[test]
fn test_v4a_maa_crash_d71bf84b_no_overlapping_deltas() {
// File content where hunk A (deletion) and hunk B (delegate tweak) both
// match, and hunk A's matched range fully contains hunk B's.
// The `ActiveMicButtonTheme.background` line that hunk B targets sits
// inside `DefaultWeightAgentInputButtonTheme`'s impl, so hunk A's
// deletion (which covers the whole impl) subsumes hunk B.
let file_content = "\
}\n\
}\n\
}\n\
\n\
struct DefaultWeightAgentInputButtonTheme;\n\
\n\
impl ActionButtonTheme for DefaultWeightAgentInputButtonTheme {\n\
fn background(&self, hovered: bool, appearance: &Appearance) -> Option<Fill> {\n\
AgentInputButtonTheme.background(hovered, appearance)\n\
}\n\
\n\
fn text_color(\n\
&self,\n\
hovered: bool,\n\
background: Option<Fill>,\n\
appearance: &Appearance,\n\
) -> ColorU {\n\
AgentInputButtonTheme.text_color(hovered, background, appearance)\n\
}\n\
\n\
fn border(&self, appearance: &Appearance) -> Option<ColorU> {\n\
AgentInputButtonTheme.border(appearance)\n\
}\n\
\n\
fn should_opt_out_of_contrast_adjustment(&self) -> bool {\n\
true\n\
}\n\
}";
let hunks = vec![
// Hunk A: delete the entire DefaultWeightAgentInputButtonTheme block.
V4AHunk {
change_context: vec![],
pre_context: " }\n }\n}".to_string(),
old: "\nstruct DefaultWeightAgentInputButtonTheme;\n\nimpl ActionButtonTheme for DefaultWeightAgentInputButtonTheme {\n fn background(&self, hovered: bool, appearance: &Appearance) -> Option<Fill> {\n AgentInputButtonTheme.background(hovered, appearance)\n }\n\n fn text_color(\n &self,\n hovered: bool,\n background: Option<Fill>,\n appearance: &Appearance,\n ) -> ColorU {\n AgentInputButtonTheme.text_color(hovered, background, appearance)\n }\n\n fn border(&self, appearance: &Appearance) -> Option<ColorU> {\n AgentInputButtonTheme.border(appearance)\n }\n\n fn should_opt_out_of_contrast_adjustment(&self) -> bool {\n true\n }\n}".to_string(),
new: String::new(),
post_context: String::new(),
},
// Hunk B: tweak a delegate call inside the same region hunk A deletes.
// Its preContext + old match a line inside hunk A's range, so it
// produces a delta whose range overlaps with hunk A's.
V4AHunk {
change_context: vec![],
pre_context: "impl ActionButtonTheme for DefaultWeightAgentInputButtonTheme {\n fn background(&self, hovered: bool, appearance: &Appearance) -> Option<Fill> {".to_string(),
old: " AgentInputButtonTheme.background(hovered, appearance)".to_string(),
new: " AgentInputButtonTheme::default().background(hovered, appearance)".to_string(),
post_context: " }".to_string(),
},
];
let diff = fuzzy_match_v4a_diffs("mod.rs", &hunks, None, file_content);
let deltas = deltas(&diff);
// Hunk B's matched range is inside hunk A's, so deduplication must drop it.
// Only hunk A's delta (the deletion) should survive.
assert_eq!(
deltas.len(),
1,
"Expected 1 delta (subsumed hunk should be dropped), got {}: {:?}",
deltas.len(),
deltas
.iter()
.map(|d| &d.replacement_line_range)
.collect::<Vec<_>>(),
);
assert!(
deltas[0].insertion.is_empty(),
"The surviving delta should be the deletion"
);
}