WIP and merge
This commit is contained in:
parent
97f4406ef6
commit
1bdde8b2e4
584 changed files with 33536 additions and 17400 deletions
|
@ -8,6 +8,7 @@ use crate::{Template, Templates};
|
|||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
||||
pub use edit_parser::EditFormat;
|
||||
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
||||
use futures::{
|
||||
Stream, StreamExt,
|
||||
|
@ -41,13 +42,23 @@ impl Template for CreateFilePromptTemplate {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditFilePromptTemplate {
|
||||
struct EditFileXmlPromptTemplate {
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
impl Template for EditFilePromptTemplate {
|
||||
const TEMPLATE_NAME: &'static str = "edit_file_prompt.hbs";
|
||||
impl Template for EditFileXmlPromptTemplate {
|
||||
const TEMPLATE_NAME: &'static str = "edit_file_prompt_xml.hbs";
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditFileDiffFencedPromptTemplate {
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
impl Template for EditFileDiffFencedPromptTemplate {
|
||||
const TEMPLATE_NAME: &'static str = "edit_file_prompt_diff_fenced.hbs";
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -70,6 +81,7 @@ pub struct EditAgent {
|
|||
action_log: Entity<ActionLog>,
|
||||
project: Entity<Project>,
|
||||
templates: Arc<Templates>,
|
||||
edit_format: EditFormat,
|
||||
}
|
||||
|
||||
impl EditAgent {
|
||||
|
@ -78,12 +90,14 @@ impl EditAgent {
|
|||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
templates: Arc<Templates>,
|
||||
edit_format: EditFormat,
|
||||
) -> Self {
|
||||
EditAgent {
|
||||
model,
|
||||
project,
|
||||
action_log,
|
||||
templates,
|
||||
edit_format,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,14 +223,23 @@ impl EditAgent {
|
|||
let this = self.clone();
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let conversation = conversation.clone();
|
||||
let edit_format = self.edit_format;
|
||||
let output = cx.spawn(async move |cx| {
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
|
||||
let prompt = EditFilePromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?;
|
||||
let prompt = match edit_format {
|
||||
EditFormat::XmlTags => EditFileXmlPromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?,
|
||||
EditFormat::DiffFenced => EditFileDiffFencedPromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?,
|
||||
};
|
||||
|
||||
let edit_chunks = this
|
||||
.request(conversation, CompletionIntent::EditFile, prompt, cx)
|
||||
.await?;
|
||||
|
@ -236,7 +259,7 @@ impl EditAgent {
|
|||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))?;
|
||||
|
||||
let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, cx);
|
||||
let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, self.edit_format, cx);
|
||||
let mut edit_events = edit_events.peekable();
|
||||
while let Some(edit_event) = Pin::new(&mut edit_events).peek().await {
|
||||
// Skip events until we're at the start of a new edit.
|
||||
|
@ -286,7 +309,13 @@ impl EditAgent {
|
|||
_ => {
|
||||
let ranges = resolved_old_text
|
||||
.into_iter()
|
||||
.map(|text| text.range)
|
||||
.map(|text| {
|
||||
let start_line =
|
||||
(snapshot.offset_to_point(text.range.start).row + 1) as usize;
|
||||
let end_line =
|
||||
(snapshot.offset_to_point(text.range.end).row + 1) as usize;
|
||||
start_line..end_line
|
||||
})
|
||||
.collect();
|
||||
output_events
|
||||
.unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
|
||||
|
@ -344,6 +373,7 @@ impl EditAgent {
|
|||
|
||||
fn parse_edit_chunks(
|
||||
chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
|
||||
edit_format: EditFormat,
|
||||
cx: &mut AsyncApp,
|
||||
) -> (
|
||||
Task<Result<EditAgentOutput>>,
|
||||
|
@ -353,7 +383,7 @@ impl EditAgent {
|
|||
let output = cx.background_spawn(async move {
|
||||
pin_mut!(chunks);
|
||||
|
||||
let mut parser = EditParser::new();
|
||||
let mut parser = EditParser::new(edit_format);
|
||||
let mut raw_edits = String::new();
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
match chunk {
|
||||
|
@ -429,25 +459,25 @@ impl EditAgent {
|
|||
let task = cx.background_spawn(async move {
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
|
||||
while let Some(edit_event) = edit_events.next().await {
|
||||
let EditParserEvent::OldTextChunk { chunk, done } = edit_event? else {
|
||||
let EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done,
|
||||
line_hint,
|
||||
} = edit_event?
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
old_range_tx.send(matcher.push(&chunk))?;
|
||||
old_range_tx.send(matcher.push(&chunk, line_hint))?;
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let matches = matcher.finish();
|
||||
let best_match = matcher.select_best_match();
|
||||
|
||||
let old_range = if matches.len() == 1 {
|
||||
matches.first()
|
||||
} else {
|
||||
// No matches or multiple ambiguous matches
|
||||
None
|
||||
};
|
||||
old_range_tx.send(old_range.cloned())?;
|
||||
old_range_tx.send(best_match.clone())?;
|
||||
|
||||
let indent = LineIndent::from_iter(
|
||||
matcher
|
||||
|
@ -456,10 +486,18 @@ impl EditAgent {
|
|||
.unwrap_or(&String::new())
|
||||
.chars(),
|
||||
);
|
||||
let resolved_old_texts = matches
|
||||
.into_iter()
|
||||
.map(|range| ResolvedOldText { range, indent })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let resolved_old_texts = if let Some(best_match) = best_match {
|
||||
vec![ResolvedOldText {
|
||||
range: best_match,
|
||||
indent,
|
||||
}]
|
||||
} else {
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|range| ResolvedOldText { range, indent })
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok((edit_events, resolved_old_texts))
|
||||
});
|
||||
|
@ -1341,7 +1379,13 @@ mod tests {
|
|||
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
EditAgent::new(model, project, action_log, Templates::new())
|
||||
EditAgent::new(
|
||||
model,
|
||||
project,
|
||||
action_log,
|
||||
Templates::new(),
|
||||
EditFormat::XmlTags,
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
|
@ -1374,10 +1418,12 @@ mod tests {
|
|||
&agent,
|
||||
indoc! {"
|
||||
<old_text>
|
||||
return 42;
|
||||
return 42;
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
return 100;
|
||||
return 100;
|
||||
}
|
||||
</new_text>
|
||||
"},
|
||||
&mut rng,
|
||||
|
@ -1407,7 +1453,7 @@ mod tests {
|
|||
|
||||
// And AmbiguousEditRange even should be emitted
|
||||
let events = drain_events(&mut events);
|
||||
let ambiguous_ranges = vec![17..31, 52..66, 87..101];
|
||||
let ambiguous_ranges = vec![2..3, 6..7, 10..11];
|
||||
assert!(
|
||||
events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
|
||||
"Should emit AmbiguousEditRange for non-unique text"
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
use anyhow::bail;
|
||||
use derive_more::{Add, AddAssign};
|
||||
use language_model::LanguageModel;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{mem, ops::Range};
|
||||
use std::{mem, ops::Range, str::FromStr, sync::Arc};
|
||||
|
||||
const OLD_TEXT_END_TAG: &str = "</old_text>";
|
||||
const NEW_TEXT_END_TAG: &str = "</new_text>";
|
||||
const EDITS_END_TAG: &str = "</edits>";
|
||||
const SEARCH_MARKER: &str = "<<<<<<< SEARCH";
|
||||
const SEPARATOR_MARKER: &str = "=======";
|
||||
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
|
||||
const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditParserEvent {
|
||||
OldTextChunk { chunk: String, done: bool },
|
||||
NewTextChunk { chunk: String, done: bool },
|
||||
OldTextChunk {
|
||||
chunk: String,
|
||||
done: bool,
|
||||
line_hint: Option<u32>,
|
||||
},
|
||||
NewTextChunk {
|
||||
chunk: String,
|
||||
done: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -23,45 +36,164 @@ pub struct EditParserMetrics {
|
|||
pub mismatched_tags: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EditFormat {
|
||||
/// XML-like tags:
|
||||
/// <old_text>...</old_text>
|
||||
/// <new_text>...</new_text>
|
||||
XmlTags,
|
||||
/// Diff-fenced format, in which:
|
||||
/// - Text before the SEARCH marker is ignored
|
||||
/// - Fences are optional
|
||||
/// - Line hint is optional.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```diff
|
||||
/// <<<<<<< SEARCH line=42
|
||||
/// ...
|
||||
/// =======
|
||||
/// ...
|
||||
/// >>>>>>> REPLACE
|
||||
/// ```
|
||||
DiffFenced,
|
||||
}
|
||||
|
||||
impl FromStr for EditFormat {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"xml_tags" | "xml" => Ok(EditFormat::XmlTags),
|
||||
"diff_fenced" | "diff-fenced" | "diff" => Ok(EditFormat::DiffFenced),
|
||||
_ => bail!("Unknown EditFormat: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFormat {
|
||||
/// Return an optimal edit format for the language model
|
||||
pub fn from_model(model: Arc<dyn LanguageModel>) -> anyhow::Result<Self> {
|
||||
if model.provider_id().0 == "google" || model.id().0.to_lowercase().contains("gemini") {
|
||||
Ok(EditFormat::DiffFenced)
|
||||
} else {
|
||||
Ok(EditFormat::XmlTags)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an optimal edit format for the language model,
|
||||
/// with the ability to override it by setting the
|
||||
/// `ZED_EDIT_FORMAT` environment variable
|
||||
#[allow(dead_code)]
|
||||
pub fn from_env(model: Arc<dyn LanguageModel>) -> anyhow::Result<Self> {
|
||||
let default = EditFormat::from_model(model)?;
|
||||
std::env::var("ZED_EDIT_FORMAT").map_or(Ok(default), |s| EditFormat::from_str(&s))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EditFormatParser: Send + std::fmt::Debug {
|
||||
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]>;
|
||||
fn take_metrics(&mut self) -> EditParserMetrics;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EditParser {
|
||||
state: EditParserState,
|
||||
pub struct XmlEditParser {
|
||||
state: XmlParserState,
|
||||
buffer: String,
|
||||
metrics: EditParserMetrics,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum EditParserState {
|
||||
enum XmlParserState {
|
||||
Pending,
|
||||
WithinOldText { start: bool },
|
||||
WithinOldText { start: bool, line_hint: Option<u32> },
|
||||
AfterOldText,
|
||||
WithinNewText { start: bool },
|
||||
}
|
||||
|
||||
impl EditParser {
|
||||
#[derive(Debug)]
|
||||
pub struct DiffFencedEditParser {
|
||||
state: DiffParserState,
|
||||
buffer: String,
|
||||
metrics: EditParserMetrics,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DiffParserState {
|
||||
Pending,
|
||||
WithinSearch { start: bool, line_hint: Option<u32> },
|
||||
WithinReplace { start: bool },
|
||||
}
|
||||
|
||||
/// Main parser that delegates to format-specific parsers
|
||||
pub struct EditParser {
|
||||
parser: Box<dyn EditFormatParser>,
|
||||
}
|
||||
|
||||
impl XmlEditParser {
|
||||
pub fn new() -> Self {
|
||||
EditParser {
|
||||
state: EditParserState::Pending,
|
||||
XmlEditParser {
|
||||
state: XmlParserState::Pending,
|
||||
buffer: String::new(),
|
||||
metrics: EditParserMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
fn find_end_tag(&self) -> Option<Range<usize>> {
|
||||
let (tag, start_ix) = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
|
||||
.min_by_key(|(_, ix)| *ix)?;
|
||||
Some(start_ix..start_ix + tag.len())
|
||||
}
|
||||
|
||||
fn ends_with_tag_prefix(&self) -> bool {
|
||||
let mut end_prefixes = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
|
||||
.chain(["\n"]);
|
||||
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
}
|
||||
|
||||
fn parse_line_hint(&self, tag: &str) -> Option<u32> {
|
||||
use std::sync::LazyLock;
|
||||
static LINE_HINT_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
|
||||
|
||||
LINE_HINT_REGEX
|
||||
.captures(tag)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFormatParser for XmlEditParser {
|
||||
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
self.buffer.push_str(chunk);
|
||||
|
||||
let mut edit_events = SmallVec::new();
|
||||
loop {
|
||||
match &mut self.state {
|
||||
EditParserState::Pending => {
|
||||
if let Some(start) = self.buffer.find("<old_text>") {
|
||||
self.buffer.drain(..start + "<old_text>".len());
|
||||
self.state = EditParserState::WithinOldText { start: true };
|
||||
XmlParserState::Pending => {
|
||||
if let Some(start) = self.buffer.find("<old_text") {
|
||||
if let Some(tag_end) = self.buffer[start..].find('>') {
|
||||
let tag_end = start + tag_end + 1;
|
||||
let tag = &self.buffer[start..tag_end];
|
||||
let line_hint = self.parse_line_hint(tag);
|
||||
self.buffer.drain(..tag_end);
|
||||
self.state = XmlParserState::WithinOldText {
|
||||
start: true,
|
||||
line_hint,
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EditParserState::WithinOldText { start } => {
|
||||
XmlParserState::WithinOldText { start, line_hint } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
|
@ -69,6 +201,7 @@ impl EditParser {
|
|||
*start = false;
|
||||
}
|
||||
|
||||
let line_hint = *line_hint;
|
||||
if let Some(tag_range) = self.find_end_tag() {
|
||||
let mut chunk = self.buffer[..tag_range.start].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
|
@ -81,27 +214,32 @@ impl EditParser {
|
|||
}
|
||||
|
||||
self.buffer.drain(..tag_range.end);
|
||||
self.state = EditParserState::AfterOldText;
|
||||
edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true });
|
||||
self.state = XmlParserState::AfterOldText;
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done: true,
|
||||
line_hint,
|
||||
});
|
||||
} else {
|
||||
if !self.ends_with_tag_prefix() {
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
line_hint,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
EditParserState::AfterOldText => {
|
||||
XmlParserState::AfterOldText => {
|
||||
if let Some(start) = self.buffer.find("<new_text>") {
|
||||
self.buffer.drain(..start + "<new_text>".len());
|
||||
self.state = EditParserState::WithinNewText { start: true };
|
||||
self.state = XmlParserState::WithinNewText { start: true };
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EditParserState::WithinNewText { start } => {
|
||||
XmlParserState::WithinNewText { start } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
|
@ -121,7 +259,7 @@ impl EditParser {
|
|||
}
|
||||
|
||||
self.buffer.drain(..tag_range.end);
|
||||
self.state = EditParserState::Pending;
|
||||
self.state = XmlParserState::Pending;
|
||||
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
|
||||
} else {
|
||||
if !self.ends_with_tag_prefix() {
|
||||
|
@ -138,24 +276,163 @@ impl EditParser {
|
|||
edit_events
|
||||
}
|
||||
|
||||
fn find_end_tag(&self) -> Option<Range<usize>> {
|
||||
let (tag, start_ix) = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
|
||||
.min_by_key(|(_, ix)| *ix)?;
|
||||
Some(start_ix..start_ix + tag.len())
|
||||
fn take_metrics(&mut self) -> EditParserMetrics {
|
||||
std::mem::take(&mut self.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffFencedEditParser {
|
||||
pub fn new() -> Self {
|
||||
DiffFencedEditParser {
|
||||
state: DiffParserState::Pending,
|
||||
buffer: String::new(),
|
||||
metrics: EditParserMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ends_with_tag_prefix(&self) -> bool {
|
||||
let mut end_prefixes = END_TAGS
|
||||
fn ends_with_diff_marker_prefix(&self) -> bool {
|
||||
let diff_markers = [SEPARATOR_MARKER, REPLACE_MARKER];
|
||||
let mut diff_prefixes = diff_markers
|
||||
.iter()
|
||||
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
|
||||
.flat_map(|marker| (1..marker.len()).map(move |i| &marker[..i]))
|
||||
.chain(["\n"]);
|
||||
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
diff_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
}
|
||||
|
||||
pub fn finish(self) -> EditParserMetrics {
|
||||
self.metrics
|
||||
fn parse_line_hint(&self, search_line: &str) -> Option<u32> {
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
static LINE_HINT_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
|
||||
|
||||
LINE_HINT_REGEX
|
||||
.captures(search_line)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFormatParser for DiffFencedEditParser {
|
||||
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
self.buffer.push_str(chunk);
|
||||
|
||||
let mut edit_events = SmallVec::new();
|
||||
loop {
|
||||
match &mut self.state {
|
||||
DiffParserState::Pending => {
|
||||
if let Some(diff) = self.buffer.find(SEARCH_MARKER) {
|
||||
let search_end = diff + SEARCH_MARKER.len();
|
||||
if let Some(newline_pos) = self.buffer[search_end..].find('\n') {
|
||||
let search_line = &self.buffer[diff..search_end + newline_pos];
|
||||
let line_hint = self.parse_line_hint(search_line);
|
||||
self.buffer.drain(..search_end + newline_pos + 1);
|
||||
self.state = DiffParserState::WithinSearch {
|
||||
start: true,
|
||||
line_hint,
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
DiffParserState::WithinSearch { start, line_hint } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
}
|
||||
*start = false;
|
||||
}
|
||||
|
||||
let line_hint = *line_hint;
|
||||
if let Some(separator_pos) = self.buffer.find(SEPARATOR_MARKER) {
|
||||
let mut chunk = self.buffer[..separator_pos].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
chunk.pop();
|
||||
}
|
||||
|
||||
let separator_end = separator_pos + SEPARATOR_MARKER.len();
|
||||
if let Some(newline_pos) = self.buffer[separator_end..].find('\n') {
|
||||
self.buffer.drain(..separator_end + newline_pos + 1);
|
||||
self.state = DiffParserState::WithinReplace { start: true };
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done: true,
|
||||
line_hint,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if !self.ends_with_diff_marker_prefix() {
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
line_hint,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
DiffParserState::WithinReplace { start } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
}
|
||||
*start = false;
|
||||
}
|
||||
|
||||
if let Some(replace_pos) = self.buffer.find(REPLACE_MARKER) {
|
||||
let mut chunk = self.buffer[..replace_pos].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
chunk.pop();
|
||||
}
|
||||
|
||||
self.buffer.drain(..replace_pos + REPLACE_MARKER.len());
|
||||
if let Some(newline_pos) = self.buffer.find('\n') {
|
||||
self.buffer.drain(..newline_pos + 1);
|
||||
} else {
|
||||
self.buffer.clear();
|
||||
}
|
||||
|
||||
self.state = DiffParserState::Pending;
|
||||
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
|
||||
} else {
|
||||
if !self.ends_with_diff_marker_prefix() {
|
||||
edit_events.push(EditParserEvent::NewTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edit_events
|
||||
}
|
||||
|
||||
fn take_metrics(&mut self) -> EditParserMetrics {
|
||||
std::mem::take(&mut self.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
impl EditParser {
|
||||
pub fn new(format: EditFormat) -> Self {
|
||||
let parser: Box<dyn EditFormatParser> = match format {
|
||||
EditFormat::XmlTags => Box::new(XmlEditParser::new()),
|
||||
EditFormat::DiffFenced => Box::new(DiffFencedEditParser::new()),
|
||||
};
|
||||
EditParser { parser }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
self.parser.push(chunk)
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> EditParserMetrics {
|
||||
self.parser.take_metrics()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,8 +444,8 @@ mod tests {
|
|||
use std::cmp;
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_single_edit(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_single_edit(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text>original</old_text><new_text>updated</new_text>",
|
||||
|
@ -178,6 +455,7 @@ mod tests {
|
|||
vec![Edit {
|
||||
old_text: "original".to_string(),
|
||||
new_text: "updated".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -190,8 +468,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_multiple_edits(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_multiple_edits(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
|
@ -209,10 +487,12 @@ mod tests {
|
|||
Edit {
|
||||
old_text: "first old".to_string(),
|
||||
new_text: "first new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "second old".to_string(),
|
||||
new_text: "second new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
@ -226,8 +506,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_edits_with_extra_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_edits_with_extra_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
|
@ -244,14 +524,17 @@ mod tests {
|
|||
Edit {
|
||||
old_text: "content".to_string(),
|
||||
new_text: "updated content".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "second item".to_string(),
|
||||
new_text: "modified second item".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "third case".to_string(),
|
||||
new_text: "improved third case".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
@ -265,8 +548,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_nested_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_nested_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text>code with <tag>nested</tag> elements</old_text><new_text>new <code>content</code></new_text>",
|
||||
|
@ -276,6 +559,7 @@ mod tests {
|
|||
vec![Edit {
|
||||
old_text: "code with <tag>nested</tag> elements".to_string(),
|
||||
new_text: "new <code>content</code>".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -288,8 +572,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_empty_old_and_new_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_empty_old_and_new_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text></old_text><new_text></new_text>",
|
||||
|
@ -299,6 +583,7 @@ mod tests {
|
|||
vec![Edit {
|
||||
old_text: "".to_string(),
|
||||
new_text: "".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -311,8 +596,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_multiline_content(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_multiline_content(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text>line1\nline2\nline3</old_text><new_text>line1\nmodified line2\nline3</new_text>",
|
||||
|
@ -322,6 +607,7 @@ mod tests {
|
|||
vec![Edit {
|
||||
old_text: "line1\nline2\nline3".to_string(),
|
||||
new_text: "line1\nmodified line2\nline3".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -334,8 +620,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_mismatched_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
fn test_xml_mismatched_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
// Reduced from an actual Sonnet 3.7 output
|
||||
|
@ -368,10 +654,12 @@ mod tests {
|
|||
Edit {
|
||||
old_text: "a\nb\nc".to_string(),
|
||||
new_text: "a\nB\nc".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "d\ne\nf".to_string(),
|
||||
new_text: "D\ne\nF".to_string(),
|
||||
line_hint: None,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
@ -383,7 +671,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
|
||||
let mut parser = EditParser::new();
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
// Reduced from an actual Opus 4 output
|
||||
|
@ -402,6 +690,7 @@ mod tests {
|
|||
vec![Edit {
|
||||
old_text: "Lorem".to_string(),
|
||||
new_text: "LOREM".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -413,10 +702,297 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_diff_fenced_single_edit(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH
|
||||
original text
|
||||
=======
|
||||
updated text
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "original text".to_string(),
|
||||
new_text: "updated text".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_with_markdown_fences(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
```diff
|
||||
<<<<<<< SEARCH
|
||||
from flask import Flask
|
||||
=======
|
||||
import math
|
||||
from flask import Flask
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "from flask import Flask".to_string(),
|
||||
new_text: "import math\nfrom flask import Flask".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_multiple_edits(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH
|
||||
first old
|
||||
=======
|
||||
first new
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
second old
|
||||
=======
|
||||
second new
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![
|
||||
Edit {
|
||||
old_text: "first old".to_string(),
|
||||
new_text: "first new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "second old".to_string(),
|
||||
new_text: "second new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_mixed_formats(mut rng: StdRng) {
|
||||
// Test XML format parser only parses XML tags
|
||||
let mut xml_parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<old_text>xml style old</old_text><new_text>xml style new</new_text>
|
||||
|
||||
<<<<<<< SEARCH
|
||||
diff style old
|
||||
=======
|
||||
diff style new
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut xml_parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "xml style old".to_string(),
|
||||
new_text: "xml style new".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
xml_parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 2,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
|
||||
// Test diff-fenced format parser only parses diff markers
|
||||
let mut diff_parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<old_text>xml style old</old_text><new_text>xml style new</new_text>
|
||||
|
||||
<<<<<<< SEARCH
|
||||
diff style old
|
||||
=======
|
||||
diff style new
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut diff_parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "diff style old".to_string(),
|
||||
new_text: "diff style new".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
diff_parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_empty_sections(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "".to_string(),
|
||||
new_text: "".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_with_line_hint(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
let edits = parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH line=42
|
||||
original text
|
||||
=======
|
||||
updated text
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
assert_eq!(
|
||||
edits,
|
||||
vec![Edit {
|
||||
old_text: "original text".to_string(),
|
||||
line_hint: Some(42),
|
||||
new_text: "updated text".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_xml_line_hints(mut rng: StdRng) {
|
||||
// Line hint is a single quoted line number
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text line="23">original code</old_text>
|
||||
<new_text>updated code</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "original code");
|
||||
assert_eq!(edits[0].line_hint, Some(23));
|
||||
assert_eq!(edits[0].new_text, "updated code");
|
||||
|
||||
// Line hint is a single unquoted line number
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text line=45>original code</old_text>
|
||||
<new_text>updated code</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "original code");
|
||||
assert_eq!(edits[0].line_hint, Some(45));
|
||||
assert_eq!(edits[0].new_text, "updated code");
|
||||
|
||||
// Line hint is a range
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text line="23:50">original code</old_text>
|
||||
<new_text>updated code</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "original code");
|
||||
assert_eq!(edits[0].line_hint, Some(23));
|
||||
assert_eq!(edits[0].new_text, "updated code");
|
||||
|
||||
// No line hint
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text>old</old_text>
|
||||
<new_text>new</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "old");
|
||||
assert_eq!(edits[0].line_hint, None);
|
||||
assert_eq!(edits[0].new_text, "new");
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
struct Edit {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
line_hint: Option<u32>,
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
|
||||
|
@ -433,10 +1009,15 @@ mod tests {
|
|||
for chunk_ix in chunk_indices {
|
||||
for event in parser.push(&input[last_ix..chunk_ix]) {
|
||||
match event {
|
||||
EditParserEvent::OldTextChunk { chunk, done } => {
|
||||
EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done,
|
||||
line_hint,
|
||||
} => {
|
||||
old_text.as_mut().unwrap().push_str(&chunk);
|
||||
if done {
|
||||
pending_edit.old_text = old_text.take().unwrap();
|
||||
pending_edit.line_hint = line_hint;
|
||||
new_text = Some(String::new());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ use std::{
|
|||
cmp::Reverse,
|
||||
fmt::{self, Display},
|
||||
io::Write as _,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
};
|
||||
|
@ -38,10 +39,11 @@ fn eval_extract_handle_command_output() {
|
|||
//
|
||||
// Model | Pass rate
|
||||
// ----------------------------|----------
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// gemini-2.5-pro-06-05 | 0.77
|
||||
// gemini-2.5-flash | 0.11
|
||||
// gpt-4.1 | 1.00
|
||||
// claude-3.7-sonnet | 0.99 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.97 (2025-06-14)
|
||||
// gemini-2.5-pro-06-05 | 0.98 (2025-06-16)
|
||||
// gemini-2.5-flash | 0.11 (2025-05-22)
|
||||
// gpt-4.1 | 1.00 (2025-05-22)
|
||||
|
||||
let input_file_path = "root/blame.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");
|
||||
|
@ -57,7 +59,7 @@ fn eval_extract_handle_command_output() {
|
|||
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
|
||||
eval(
|
||||
100,
|
||||
0.7, // Taking the lower bar for Gemini
|
||||
0.95,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
|
@ -110,6 +112,13 @@ fn eval_extract_handle_command_output() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_delete_run_git_blame() {
|
||||
// Model | Pass rate
|
||||
// ----------------------------|----------
|
||||
// claude-3.7-sonnet | 1.0 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.96 (2025-06-14)
|
||||
// gemini-2.5-pro-06-05 | 1.0 (2025-06-16)
|
||||
// gemini-2.5-flash |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/blame.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
|
||||
let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
|
||||
|
@ -165,13 +174,12 @@ fn eval_delete_run_git_blame() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_translate_doc_comments() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// claude-3.7-sonnet | 1.0 (2025-06-14)
|
||||
// claude-sonnet-4 | 1.0 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/canvas.rs";
|
||||
|
@ -228,13 +236,12 @@ fn eval_translate_doc_comments() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// gemini-2.5-pro-preview-03-25 | 0.99
|
||||
// claude-3.7-sonnet | 0.96 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.11 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-latest | 0.99 (2025-06-16)
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/lib.rs";
|
||||
|
@ -354,13 +361,12 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_disable_cursor_blinking() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// claude-3.7-sonnet | 0.99 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.85 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-latest | 0.97 (2025-06-16)
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/editor.rs";
|
||||
|
@ -438,14 +444,20 @@ fn eval_disable_cursor_blinking() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_from_pixels_constructor() {
|
||||
// Results for 2025-05-22
|
||||
// Results for 2025-06-13
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
// The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE
|
||||
// value. Higher values improve the pass rate but may sometimes cause
|
||||
// edits to be misapplied. In the context of this eval, this means
|
||||
// the agent might add from_pixels tests in incorrect locations
|
||||
// (e.g., at the beginning of the file), yet the evaluation may still
|
||||
// rate it highly.
|
||||
//
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 0.94
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// Model | Date | Pass rate
|
||||
// =========================================================
|
||||
// claude-4.0-sonnet | 2025-06-14 | 0.99
|
||||
// claude-3.7-sonnet | 2025-06-14 | 0.88
|
||||
// gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/canvas.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
|
||||
|
@ -455,7 +467,7 @@ fn eval_from_pixels_constructor() {
|
|||
0.95,
|
||||
// For whatever reason, this eval produces more mismatched tags.
|
||||
// Increasing for now, let's see if we can bring this down.
|
||||
0.2,
|
||||
0.25,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
|
@ -641,15 +653,14 @@ fn eval_from_pixels_constructor() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_zode() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.0
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 | 1.0
|
||||
// gpt-4.1 | 1.0
|
||||
// claude-3.7-sonnet | 1.0 (2025-06-14)
|
||||
// claude-sonnet-4 | 1.0 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
|
||||
// gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22)
|
||||
// gpt-4.1 | 1.0 (2025-05-22)
|
||||
let input_file_path = "root/zode.py";
|
||||
let input_content = None;
|
||||
let edit_description = "Create the main Zode CLI script";
|
||||
|
@ -748,13 +759,12 @@ fn eval_zode() {
|
|||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_add_overwrite_test() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.16
|
||||
// gemini-2.5-pro-preview-03-25 | 0.35
|
||||
// claude-3.7-sonnet | 0.65 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.07 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22)
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/action_log.rs";
|
||||
|
@ -984,15 +994,14 @@ fn eval_create_empty_file() {
|
|||
// thoughts into it. This issue is not specific to empty files, but
|
||||
// it's easier to reproduce with them.
|
||||
//
|
||||
// Results for 2025-05-21:
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.00
|
||||
// gemini-2.5-pro-preview-03-25 | 1.00
|
||||
// gemini-2.5-flash-preview-04-17 | 1.00
|
||||
// gpt-4.1 | 1.00
|
||||
// claude-3.7-sonnet | 1.00 (2025-06-14)
|
||||
// claude-sonnet-4 | 1.00 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21)
|
||||
// gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21)
|
||||
// gpt-4.1 | 1.00 (2025-05-21)
|
||||
//
|
||||
//
|
||||
// TODO: gpt-4.1-mini errored 38 times:
|
||||
|
@ -1461,7 +1470,7 @@ impl EditAgentTest {
|
|||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
});
|
||||
|
||||
|
@ -1488,8 +1497,16 @@ impl EditAgentTest {
|
|||
.await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let edit_format = EditFormat::from_env(agent_model.clone()).unwrap();
|
||||
|
||||
Self {
|
||||
agent: EditAgent::new(agent_model, project.clone(), action_log, Templates::new()),
|
||||
agent: EditAgent::new(
|
||||
agent_model,
|
||||
project.clone(),
|
||||
action_log,
|
||||
Templates::new(),
|
||||
edit_format,
|
||||
),
|
||||
project,
|
||||
judge_model,
|
||||
}
|
||||
|
@ -1549,6 +1566,7 @@ impl EditAgentTest {
|
|||
.collect::<Vec<_>>();
|
||||
let worktrees = vec![WorktreeContext {
|
||||
root_name: "root".to_string(),
|
||||
abs_path: Path::new("/path/to/root").into(),
|
||||
rules_file: None,
|
||||
}];
|
||||
let prompt_builder = PromptBuilder::new(None)?;
|
||||
|
@ -1634,15 +1652,20 @@ impl EditAgentTest {
|
|||
}
|
||||
|
||||
async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
|
||||
let mut attempt = 0;
|
||||
loop {
|
||||
attempt += 1;
|
||||
match request().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimit(duration) => {
|
||||
// Wait until after we are allowed to try again
|
||||
eprintln!("Rate limit exceeded. Waiting for {duration:?}...",);
|
||||
Timer::after(duration).await;
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
|
||||
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
eprintln!(
|
||||
"Attempt #{attempt}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
);
|
||||
Timer::after(retry_after + jitter).await;
|
||||
continue;
|
||||
}
|
||||
_ => return Err(err.into()),
|
||||
|
|
|
@ -9132,7 +9132,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| lines.sort())
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
|
||||
}
|
||||
|
||||
pub fn sort_lines_case_insensitive(
|
||||
|
@ -9141,7 +9141,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
lines.sort_by_key(|line| line.to_lowercase())
|
||||
})
|
||||
}
|
||||
|
@ -9152,7 +9152,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
let mut seen = HashSet::default();
|
||||
lines.retain(|line| seen.insert(line.to_lowercase()));
|
||||
})
|
||||
|
@ -9164,7 +9164,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
let mut seen = HashSet::default();
|
||||
lines.retain(|line| seen.insert(*line));
|
||||
})
|
||||
|
@ -9606,20 +9606,20 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_lines(window, cx, |lines| lines.reverse())
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
|
||||
}
|
||||
|
||||
pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
|
||||
}
|
||||
|
||||
fn manipulate_lines<Fn>(
|
||||
fn manipulate_lines<M>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
mut manipulate: M,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<&str>),
|
||||
M: FnMut(&str) -> LineManipulationResult,
|
||||
{
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
|
||||
|
||||
|
@ -9652,18 +9652,14 @@ impl Editor {
|
|||
.text_for_range(start_point..end_point)
|
||||
.collect::<String>();
|
||||
|
||||
let mut lines = text.split('\n').collect_vec();
|
||||
let LineManipulationResult { new_text, line_count_before, line_count_after} = manipulate(&text);
|
||||
|
||||
let lines_before = lines.len();
|
||||
callback(&mut lines);
|
||||
let lines_after = lines.len();
|
||||
|
||||
edits.push((start_point..end_point, lines.join("\n")));
|
||||
edits.push((start_point..end_point, new_text));
|
||||
|
||||
// Selections must change based on added and removed line count
|
||||
let start_row =
|
||||
MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32);
|
||||
let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32);
|
||||
let end_row = MultiBufferRow(start_row.0 + line_count_after.saturating_sub(1) as u32);
|
||||
new_selections.push(Selection {
|
||||
id: selection.id,
|
||||
start: start_row,
|
||||
|
@ -9672,10 +9668,10 @@ impl Editor {
|
|||
reversed: selection.reversed,
|
||||
});
|
||||
|
||||
if lines_after > lines_before {
|
||||
added_lines += lines_after - lines_before;
|
||||
} else if lines_before > lines_after {
|
||||
removed_lines += lines_before - lines_after;
|
||||
if line_count_after > line_count_before {
|
||||
added_lines += line_count_after - line_count_before;
|
||||
} else if line_count_before > line_count_after {
|
||||
removed_lines += line_count_before - line_count_after;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9720,6 +9716,171 @@ impl Editor {
|
|||
})
|
||||
}
|
||||
|
||||
fn manipulate_immutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<&str>),
|
||||
{
|
||||
self.manipulate_lines(window, cx, |text| {
|
||||
let mut lines: Vec<&str> = text.split('\n').collect();
|
||||
let line_count_before = lines.len();
|
||||
|
||||
callback(&mut lines);
|
||||
|
||||
LineManipulationResult {
|
||||
new_text: lines.join("\n"),
|
||||
line_count_before,
|
||||
line_count_after: lines.len(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn manipulate_mutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<Cow<'_, str>>),
|
||||
{
|
||||
self.manipulate_lines(window, cx, |text| {
|
||||
let mut lines: Vec<Cow<str>> = text.split('\n').map(Cow::from).collect();
|
||||
let line_count_before = lines.len();
|
||||
|
||||
callback(&mut lines);
|
||||
|
||||
LineManipulationResult {
|
||||
new_text: lines.join("\n"),
|
||||
line_count_before,
|
||||
line_count_after: lines.len(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_indentation_to_spaces(
|
||||
&mut self,
|
||||
_: &ConvertIndentationToSpaces,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let tab_size = settings.tab_size.get() as usize;
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, |lines| {
|
||||
// Allocates a reasonably sized scratch buffer once for the whole loop
|
||||
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
|
||||
// Avoids recomputing spaces that could be inserted many times
|
||||
let space_cache: Vec<Vec<char>> = (1..=tab_size)
|
||||
.map(|n| IndentSize::spaces(n as u32).chars().collect())
|
||||
.collect();
|
||||
|
||||
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
|
||||
let mut chars = line.as_ref().chars();
|
||||
let mut col = 0;
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
' ' => {
|
||||
reindented_line.push(' ');
|
||||
col += 1;
|
||||
}
|
||||
'\t' => {
|
||||
// \t are converted to spaces depending on the current column
|
||||
let spaces_len = tab_size - (col % tab_size);
|
||||
reindented_line.extend(&space_cache[spaces_len - 1]);
|
||||
col += spaces_len;
|
||||
changed = true;
|
||||
}
|
||||
_ => {
|
||||
// If we dont append before break, the character is consumed
|
||||
reindented_line.push(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
reindented_line.clear();
|
||||
continue;
|
||||
}
|
||||
// Append the rest of the line and replace old reference with new one
|
||||
reindented_line.extend(chars);
|
||||
*line = Cow::Owned(reindented_line.clone());
|
||||
reindented_line.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_indentation_to_tabs(
|
||||
&mut self,
|
||||
_: &ConvertIndentationToTabs,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let tab_size = settings.tab_size.get() as usize;
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, |lines| {
|
||||
// Allocates a reasonably sized buffer once for the whole loop
|
||||
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
|
||||
// Avoids recomputing spaces that could be inserted many times
|
||||
let space_cache: Vec<Vec<char>> = (1..=tab_size)
|
||||
.map(|n| IndentSize::spaces(n as u32).chars().collect())
|
||||
.collect();
|
||||
|
||||
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
|
||||
let mut chars = line.chars();
|
||||
let mut spaces_count = 0;
|
||||
let mut first_non_indent_char = None;
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
' ' => {
|
||||
// Keep track of spaces. Append \t when we reach tab_size
|
||||
spaces_count += 1;
|
||||
changed = true;
|
||||
if spaces_count == tab_size {
|
||||
reindented_line.push('\t');
|
||||
spaces_count = 0;
|
||||
}
|
||||
}
|
||||
'\t' => {
|
||||
reindented_line.push('\t');
|
||||
spaces_count = 0;
|
||||
}
|
||||
_ => {
|
||||
// Dont append it yet, we might have remaining spaces
|
||||
first_non_indent_char = Some(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
reindented_line.clear();
|
||||
continue;
|
||||
}
|
||||
// Remaining spaces that didn't make a full tab stop
|
||||
if spaces_count > 0 {
|
||||
reindented_line.extend(&space_cache[spaces_count - 1]);
|
||||
}
|
||||
// If we consume an extra character that was not indentation, add it back
|
||||
if let Some(extra_char) = first_non_indent_char {
|
||||
reindented_line.push(extra_char);
|
||||
}
|
||||
// Append the rest of the line and replace old reference with new one
|
||||
reindented_line.extend(chars);
|
||||
*line = Cow::Owned(reindented_line.clone());
|
||||
reindented_line.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_to_upper_case(
|
||||
&mut self,
|
||||
_: &ConvertToUpperCase,
|
||||
|
@ -21157,6 +21318,13 @@ pub struct LineHighlight {
|
|||
pub type_id: Option<TypeId>,
|
||||
}
|
||||
|
||||
struct LineManipulationResult {
|
||||
pub new_text: String,
|
||||
pub line_count_before: usize,
|
||||
pub line_count_after: usize,
|
||||
}
|
||||
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
row: u32,
|
||||
status: &DiffHunkStatus,
|
||||
|
|
|
@ -10,8 +10,9 @@ const DELETION_COST: u32 = 10;
|
|||
pub struct StreamingFuzzyMatcher {
|
||||
snapshot: TextBufferSnapshot,
|
||||
query_lines: Vec<String>,
|
||||
line_hint: Option<u32>,
|
||||
incomplete_line: String,
|
||||
best_matches: Vec<Range<usize>>,
|
||||
matches: Vec<Range<usize>>,
|
||||
matrix: SearchMatrix,
|
||||
}
|
||||
|
||||
|
@ -21,8 +22,9 @@ impl StreamingFuzzyMatcher {
|
|||
Self {
|
||||
snapshot,
|
||||
query_lines: Vec::new(),
|
||||
line_hint: None,
|
||||
incomplete_line: String::new(),
|
||||
best_matches: Vec::new(),
|
||||
matches: Vec::new(),
|
||||
matrix: SearchMatrix::new(buffer_line_count + 1),
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +43,14 @@ impl StreamingFuzzyMatcher {
|
|||
///
|
||||
/// Returns `Some(range)` if a match has been found with the accumulated
|
||||
/// query so far, or `None` if no suitable match exists yet.
|
||||
pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> {
|
||||
pub fn push(&mut self, chunk: &str, line_hint: Option<u32>) -> Option<Range<usize>> {
|
||||
if line_hint.is_some() {
|
||||
self.line_hint = line_hint;
|
||||
}
|
||||
|
||||
// Add the chunk to our incomplete line buffer
|
||||
self.incomplete_line.push_str(chunk);
|
||||
self.line_hint = line_hint;
|
||||
|
||||
if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
|
||||
let complete_part = &self.incomplete_line[..=last_pos];
|
||||
|
@ -55,20 +62,11 @@ impl StreamingFuzzyMatcher {
|
|||
|
||||
self.incomplete_line.replace_range(..last_pos + 1, "");
|
||||
|
||||
self.best_matches = self.resolve_location_fuzzy();
|
||||
|
||||
if let Some(first_match) = self.best_matches.first() {
|
||||
Some(first_match.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
if let Some(first_match) = self.best_matches.first() {
|
||||
Some(first_match.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.matches = self.resolve_location_fuzzy();
|
||||
}
|
||||
|
||||
let best_match = self.select_best_match();
|
||||
best_match.or_else(|| self.matches.first().cloned())
|
||||
}
|
||||
|
||||
/// Finish processing and return the final best match(es).
|
||||
|
@ -80,9 +78,9 @@ impl StreamingFuzzyMatcher {
|
|||
if !self.incomplete_line.is_empty() {
|
||||
self.query_lines.push(self.incomplete_line.clone());
|
||||
self.incomplete_line.clear();
|
||||
self.best_matches = self.resolve_location_fuzzy();
|
||||
self.matches = self.resolve_location_fuzzy();
|
||||
}
|
||||
self.best_matches.clone()
|
||||
self.matches.clone()
|
||||
}
|
||||
|
||||
fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
|
||||
|
@ -198,6 +196,43 @@ impl StreamingFuzzyMatcher {
|
|||
|
||||
valid_matches.into_iter().map(|(_, range)| range).collect()
|
||||
}
|
||||
|
||||
/// Return the best match with starting position close enough to line_hint.
|
||||
pub fn select_best_match(&self) -> Option<Range<usize>> {
|
||||
// Allow line hint to be off by that many lines.
|
||||
// Higher values increase probability of applying edits to a wrong place,
|
||||
// Lower values increase edits failures and overall conversation length.
|
||||
const LINE_HINT_TOLERANCE: u32 = 200;
|
||||
|
||||
if self.matches.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.matches.len() == 1 {
|
||||
return self.matches.first().cloned();
|
||||
}
|
||||
|
||||
let Some(line_hint) = self.line_hint else {
|
||||
// Multiple ambiguous matches
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut best_match = None;
|
||||
let mut best_distance = u32::MAX;
|
||||
|
||||
for range in &self.matches {
|
||||
let start_point = self.snapshot.offset_to_point(range.start);
|
||||
let start_line = start_point.row;
|
||||
let distance = start_line.abs_diff(line_hint);
|
||||
|
||||
if distance <= LINE_HINT_TOLERANCE && distance < best_distance {
|
||||
best_distance = distance;
|
||||
best_match = Some(range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
best_match
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzzy_eq(left: &str, right: &str) -> bool {
|
||||
|
@ -640,6 +675,52 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_line_hint_selection() {
|
||||
let text = indoc! {r#"
|
||||
fn first_function() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
fn second_function() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
fn third_function() {
|
||||
return 42;
|
||||
}
|
||||
"#};
|
||||
|
||||
let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string());
|
||||
let snapshot = buffer.snapshot();
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// Given a query that matches all three functions
|
||||
let query = "return 42;\n";
|
||||
|
||||
// Test with line hint pointing to second function (around line 5)
|
||||
let best_match = matcher.push(query, Some(5)).expect("Failed to match query");
|
||||
|
||||
let matched_text = snapshot
|
||||
.text_for_range(best_match.clone())
|
||||
.collect::<String>();
|
||||
assert!(matched_text.contains("return 42;"));
|
||||
assert_eq!(
|
||||
best_match,
|
||||
63..77,
|
||||
"Expected to match `second_function` based on the line hint"
|
||||
);
|
||||
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
matcher.push(query, None);
|
||||
matcher.finish();
|
||||
let best_match = matcher.select_best_match();
|
||||
assert!(
|
||||
best_match.is_none(),
|
||||
"Best match should be None when query cannot be uniquely resolved"
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
|
||||
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
|
||||
|
@ -653,7 +734,7 @@ mod tests {
|
|||
|
||||
// Push chunks incrementally
|
||||
for chunk in &chunks {
|
||||
matcher.push(chunk);
|
||||
matcher.push(chunk, None);
|
||||
}
|
||||
|
||||
let actual_ranges = matcher.finish();
|
||||
|
@ -706,7 +787,7 @@ mod tests {
|
|||
|
||||
fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
|
||||
finder
|
||||
.push(chunk)
|
||||
.push(chunk, None)
|
||||
.map(|range| finder.snapshot.text_for_range(range).collect::<String>())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
Templates,
|
||||
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
|
||||
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
|
||||
schema::json_schema_for,
|
||||
ui::{COLLAPSED_LINES, ToolOutputPreview},
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ use assistant_tool::{
|
|||
ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
|
@ -69,13 +69,13 @@ pub struct EditFileToolInput {
|
|||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - backend
|
||||
/// - frontend
|
||||
/// - /a/b/backend
|
||||
/// - /c/d/frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// Notice how the file path starts with `backend`. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
|
@ -201,8 +201,14 @@ impl Tool for EditFileTool {
|
|||
let card_clone = card.clone();
|
||||
let action_log_clone = action_log.clone();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let edit_agent =
|
||||
EditAgent::new(model, project.clone(), action_log_clone, Templates::new());
|
||||
let edit_format = EditFormat::from_model(model.clone())?;
|
||||
let edit_agent = EditAgent::new(
|
||||
model,
|
||||
project.clone(),
|
||||
action_log_clone,
|
||||
Templates::new(),
|
||||
edit_format,
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
|
@ -333,14 +339,18 @@ impl Tool for EditFileTool {
|
|||
);
|
||||
anyhow::ensure!(
|
||||
ambiguous_ranges.is_empty(),
|
||||
// TODO: Include ambiguous_ranges, converted to line numbers.
|
||||
// This would work best if we add `line_hint` parameter
|
||||
// to edit_file_tool
|
||||
formatdoc! {"
|
||||
<old_text> matches more than one position in the file. Read the
|
||||
relevant sections of {input_path} again and extend <old_text> so
|
||||
that I can perform the requested edits.
|
||||
"}
|
||||
{
|
||||
let line_numbers = ambiguous_ranges
|
||||
.iter()
|
||||
.map(|range| range.start.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
formatdoc! {"
|
||||
<old_text> matches more than one position in the file (lines: {line_numbers}). Read the
|
||||
relevant sections of {input_path} again and extend <old_text> so
|
||||
that I can perform the requested edits.
|
||||
"}
|
||||
}
|
||||
);
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text("No edits were made.".into()),
|
||||
|
@ -800,11 +810,30 @@ impl ToolCard for EditFileToolCard {
|
|||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
language::Point::new(0, 0),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let snapshot =
|
||||
editor.buffer().read(cx).snapshot(cx);
|
||||
let first_hunk = editor
|
||||
.diff_hunks_in_ranges(
|
||||
&[editor::Anchor::min()
|
||||
..editor::Anchor::max()],
|
||||
&snapshot,
|
||||
)
|
||||
.next();
|
||||
if let Some(first_hunk) = first_hunk {
|
||||
let first_hunk_start =
|
||||
first_hunk.multi_buffer_range().start;
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::fit()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_anchor_ranges([
|
||||
first_hunk_start
|
||||
..first_hunk_start,
|
||||
]);
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ pub struct ReadFileToolInput {
|
|||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
/// - /a/b/directory1
|
||||
/// - /c/d/directory2
|
||||
///
|
||||
/// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
|
||||
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
You MUST respond with a series of edits to a file, using the following diff format:
|
||||
|
||||
```
|
||||
<<<<<<< SEARCH line=1
|
||||
from flask import Flask
|
||||
=======
|
||||
import math
|
||||
from flask import Flask
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH line=325
|
||||
return 0
|
||||
=======
|
||||
print("Done")
|
||||
|
||||
return 0
|
||||
>>>>>>> REPLACE
|
||||
|
||||
```
|
||||
|
||||
# File Editing Instructions
|
||||
|
||||
- Use the SEARCH/REPLACE diff format shown above
|
||||
- The SEARCH section must exactly match existing file content, including indentation
|
||||
- The SEARCH section must come from the actual file, not an outline
|
||||
- The SEARCH section cannot be empty
|
||||
- `line` should be a starting line number for the text to be replaced
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines
|
||||
- For non-unique lines, include enough context to identify them
|
||||
- Do not escape quotes, newlines, or other characters
|
||||
- For multiple occurrences, repeat the same diff block for each instance
|
||||
- Edits are sequential - each assumes previous edits are already applied
|
||||
- Only edit the specified file
|
||||
|
||||
# Example
|
||||
|
||||
```
|
||||
<<<<<<< SEARCH line=3
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
=======
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
active: bool,
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH line=25
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
};
|
||||
=======
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
active: true,
|
||||
};
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
|
||||
# Final instructions
|
||||
|
||||
Tool calls have been disabled. You MUST respond using the SEARCH/REPLACE diff format only.
|
||||
|
||||
<file_to_edit>
|
||||
{{path}}
|
||||
</file_to_edit>
|
||||
|
||||
<edit_description>
|
||||
{{edit_description}}
|
||||
</edit_description>
|
|
@ -3,21 +3,21 @@ You MUST respond with a series of edits to a file, using the following format:
|
|||
```
|
||||
<edits>
|
||||
|
||||
<old_text>
|
||||
<old_text line=10>
|
||||
OLD TEXT 1 HERE
|
||||
</old_text>
|
||||
<new_text>
|
||||
NEW TEXT 1 HERE
|
||||
</new_text>
|
||||
|
||||
<old_text>
|
||||
<old_text line=456>
|
||||
OLD TEXT 2 HERE
|
||||
</old_text>
|
||||
<new_text>
|
||||
NEW TEXT 2 HERE
|
||||
</new_text>
|
||||
|
||||
<old_text>
|
||||
<old_text line=42>
|
||||
OLD TEXT 3 HERE
|
||||
</old_text>
|
||||
<new_text>
|
||||
|
@ -33,6 +33,7 @@ NEW TEXT 3 HERE
|
|||
- `<old_text>` must exactly match existing file content, including indentation
|
||||
- `<old_text>` must come from the actual file, not an outline
|
||||
- `<old_text>` cannot be empty
|
||||
- `line` should be a starting line number for the text to be replaced
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines
|
||||
- For non-unique lines, include enough context to identify them
|
||||
|
@ -48,7 +49,7 @@ Claude and gpt-4.1 don't really need it. --}}
|
|||
<example>
|
||||
<edits>
|
||||
|
||||
<old_text>
|
||||
<old_text line=3>
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
|
@ -62,7 +63,7 @@ struct User {
|
|||
}
|
||||
</new_text>
|
||||
|
||||
<old_text>
|
||||
<old_text line=25>
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
Loading…
Add table
Add a link
Reference in a new issue