Improve tracking for agent edits (#27857)

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Antonio Scandurra 2025-04-02 00:13:28 +02:00 committed by GitHub
parent d26c477d86
commit 4a252515b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 757 additions and 795 deletions

8
Cargo.lock generated
View file

@ -693,17 +693,22 @@ name = "assistant_tool"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-watch",
"buffer_diff", "buffer_diff",
"clock", "clock",
"collections", "collections",
"ctor",
"derive_more", "derive_more",
"env_logger 0.11.7",
"futures 0.3.31",
"gpui", "gpui",
"icons", "icons",
"language", "language",
"language_model", "language_model",
"log",
"parking_lot", "parking_lot",
"pretty_assertions",
"project", "project",
"rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
@ -718,7 +723,6 @@ dependencies = [
"anyhow", "anyhow",
"assistant_tool", "assistant_tool",
"chrono", "chrono",
"clock",
"collections", "collections",
"feature_flags", "feature_flags",
"futures 0.3.31", "futures 0.3.31",

View file

@ -149,7 +149,7 @@
{ {
"context": "AssistantDiff", "context": "AssistantDiff",
"bindings": { "bindings": {
"ctrl-y": "agent::ToggleKeep", "ctrl-y": "agent::Keep",
"ctrl-k ctrl-r": "agent::Reject" "ctrl-k ctrl-r": "agent::Reject"
} }
}, },

View file

@ -241,7 +241,7 @@
"context": "AssistantDiff", "context": "AssistantDiff",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-y": "agent::ToggleKeep", "cmd-y": "agent::Keep",
"cmd-alt-z": "agent::Reject" "cmd-alt-z": "agent::Reject"
} }
}, },

View file

@ -66,7 +66,7 @@ actions!(
AcceptSuggestedContext, AcceptSuggestedContext,
OpenActiveThreadAsMarkdown, OpenActiveThreadAsMarkdown,
OpenAssistantDiff, OpenAssistantDiff,
ToggleKeep, Keep,
Reject, Reject,
RejectAll, RejectAll,
KeepAll KeepAll

View file

@ -1,4 +1,4 @@
use crate::{Thread, ThreadEvent, ToggleKeep}; use crate::{Thread, ThreadEvent};
use anyhow::Result; use anyhow::Result;
use buffer_diff::DiffHunkStatus; use buffer_diff::DiffHunkStatus;
use collections::HashSet; use collections::HashSet;
@ -78,7 +78,7 @@ impl AssistantDiff {
hunk_range, hunk_range,
is_created_file, is_created_file,
line_height, line_height,
_editor: &Entity<Editor>, editor: &Entity<Editor>,
window: &mut Window, window: &mut Window,
cx: &mut App| { cx: &mut App| {
render_diff_hunk_controls( render_diff_hunk_controls(
@ -88,6 +88,7 @@ impl AssistantDiff {
is_created_file, is_created_file,
line_height, line_height,
&assistant_diff, &assistant_diff,
editor,
window, window,
cx, cx,
) )
@ -130,7 +131,7 @@ impl AssistantDiff {
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx); let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>(); let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
for (buffer, changed) in changed_buffers { for (buffer, diff_handle) in changed_buffers {
let Some(file) = buffer.read(cx).file().cloned() else { let Some(file) = buffer.read(cx).file().cloned() else {
continue; continue;
}; };
@ -139,7 +140,7 @@ impl AssistantDiff {
paths_to_delete.remove(&path_key); paths_to_delete.remove(&path_key);
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
let diff = changed.diff.read(cx); let diff = diff_handle.read(cx);
let diff_hunk_ranges = diff let diff_hunk_ranges = diff
.hunks_intersecting_range( .hunks_intersecting_range(
language::Anchor::MIN..language::Anchor::MAX, language::Anchor::MIN..language::Anchor::MAX,
@ -159,7 +160,7 @@ impl AssistantDiff {
editor::DEFAULT_MULTIBUFFER_CONTEXT, editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx, cx,
); );
multibuffer.add_diff(changed.diff.clone(), cx); multibuffer.add_diff(diff_handle, cx);
(was_empty, is_excerpt_newly_added) (was_empty, is_excerpt_newly_added)
}); });
@ -221,7 +222,7 @@ impl AssistantDiff {
} }
} }
fn toggle_keep(&mut self, _: &crate::ToggleKeep, _window: &mut Window, cx: &mut Context<Self>) { fn keep(&mut self, _: &crate::Keep, _window: &mut Window, cx: &mut Context<Self>) {
let ranges = self let ranges = self
.editor .editor
.read(cx) .read(cx)
@ -240,8 +241,7 @@ impl AssistantDiff {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id); let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer { if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| { self.thread.update(cx, |thread, cx| {
let accept = hunk.status().has_secondary_hunk(); thread.keep_edits_in_range(buffer, hunk.buffer_range, cx)
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
}); });
} }
} }
@ -268,10 +268,9 @@ impl AssistantDiff {
.update(cx, |thread, cx| thread.keep_all_edits(cx)); .update(cx, |thread, cx| thread.keep_all_edits(cx));
} }
fn review_diff_hunks( fn keep_edits_in_ranges(
&mut self, &mut self,
hunk_ranges: Vec<Range<editor::Anchor>>, hunk_ranges: Vec<Range<editor::Anchor>>,
accept: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let snapshot = self.multibuffer.read(cx).snapshot(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx);
@ -285,7 +284,7 @@ impl AssistantDiff {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id); let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer { if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| { self.thread.update(cx, |thread, cx| {
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx) thread.keep_edits_in_range(buffer, hunk.buffer_range, cx)
}); });
} }
} }
@ -479,7 +478,7 @@ impl Render for AssistantDiff {
} else { } else {
"AssistantDiff" "AssistantDiff"
}) })
.on_action(cx.listener(Self::toggle_keep)) .on_action(cx.listener(Self::keep))
.on_action(cx.listener(Self::reject)) .on_action(cx.listener(Self::reject))
.on_action(cx.listener(Self::reject_all)) .on_action(cx.listener(Self::reject_all))
.on_action(cx.listener(Self::keep_all)) .on_action(cx.listener(Self::keep_all))
@ -495,16 +494,16 @@ impl Render for AssistantDiff {
fn render_diff_hunk_controls( fn render_diff_hunk_controls(
row: u32, row: u32,
status: &DiffHunkStatus, _status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>, hunk_range: Range<editor::Anchor>,
is_created_file: bool, is_created_file: bool,
line_height: Pixels, line_height: Pixels,
assistant_diff: &Entity<AssistantDiff>, assistant_diff: &Entity<AssistantDiff>,
editor: &Entity<Editor>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> AnyElement { ) -> AnyElement {
let editor = assistant_diff.read(cx).editor.clone(); let editor = editor.clone();
h_flex() h_flex()
.h(line_height) .h(line_height)
.mr_0p5() .mr_0p5()
@ -519,75 +518,47 @@ fn render_diff_hunk_controls(
.gap_1() .gap_1()
.occlude() .occlude()
.shadow_md() .shadow_md()
.children(if status.has_secondary_hunk() { .children(vec![
vec![ Button::new("reject", "Reject")
Button::new("reject", "Reject") .disabled(is_created_file)
.disabled(is_created_file) .key_binding(
.key_binding( KeyBinding::for_action_in(
KeyBinding::for_action_in( &crate::Reject,
&crate::Reject,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.restore_hunks_in_ranges(vec![point..point], window, cx);
});
}
}),
Button::new(("keep", row as u64), "Keep")
.key_binding(
KeyBinding::for_action_in(
&crate::ToggleKeep,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let assistant_diff = assistant_diff.clone();
move |_event, _window, cx| {
assistant_diff.update(cx, |diff, cx| {
diff.review_diff_hunks(
vec![hunk_range.start..hunk_range.start],
true,
cx,
);
});
}
}),
]
} else {
vec![
Button::new(("review", row as u64), "Review")
.key_binding(KeyBinding::for_action_in(
&ToggleKeep,
&editor.read(cx).focus_handle(cx), &editor.read(cx).focus_handle(cx),
window, window,
cx, cx,
)) )
.on_click({ .map(|kb| kb.size(rems_from_px(12.))),
let assistant_diff = assistant_diff.clone(); )
move |_event, _window, cx| { .on_click({
assistant_diff.update(cx, |diff, cx| { let editor = editor.clone();
diff.review_diff_hunks( move |_event, window, cx| {
vec![hunk_range.start..hunk_range.start], editor.update(cx, |editor, cx| {
false, let snapshot = editor.snapshot(window, cx);
cx, let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
); editor.restore_hunks_in_ranges(vec![point..point], window, cx);
}); });
} }
}), }),
] Button::new(("keep", row as u64), "Keep")
}) .key_binding(
KeyBinding::for_action_in(
&crate::Keep,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let assistant_diff = assistant_diff.clone();
move |_event, _window, cx| {
assistant_diff.update(cx, |diff, cx| {
diff.keep_edits_in_ranges(vec![hunk_range.start..hunk_range.start], cx);
});
}
}),
])
.when( .when(
!editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
|el| { |el| {

View file

@ -245,9 +245,6 @@ impl MessageEditor {
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
let context = context_store.read(cx).context().clone(); let context = context_store.read(cx).context().clone();
thread.action_log().update(cx, |action_log, cx| {
action_log.clear_reviewed_changes(cx);
});
thread.insert_user_message(user_message, context, checkpoint, cx); thread.insert_user_message(user_message, context, checkpoint, cx);
}) })
.ok(); .ok();
@ -546,7 +543,7 @@ impl Render for MessageEditor {
parent.child( parent.child(
v_flex().bg(cx.theme().colors().editor_background).children( v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.into_iter().enumerate().flat_map( changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, changed))| { |(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?; let file = buffer.read(cx).file()?;
let path = file.path(); let path = file.path();
@ -619,25 +616,13 @@ impl Render for MessageEditor {
.color(Color::Deleted), .color(Color::Deleted),
), ),
) )
.when(!changed.needs_review, |parent| {
parent.child(
Icon::new(IconName::Check)
.color(Color::Success),
)
})
.child( .child(
div() div()
.h_full() .h_full()
.absolute() .absolute()
.w_8() .w_8()
.bottom_0() .bottom_0()
.map(|this| { .right_0()
if !changed.needs_review {
this.right_4()
} else {
this.right_0()
}
})
.bg(linear_gradient( .bg(linear_gradient(
90., 90.,
linear_color_stop( linear_color_stop(

View file

@ -1679,23 +1679,20 @@ impl Thread {
Ok(String::from_utf8_lossy(&markdown).to_string()) Ok(String::from_utf8_lossy(&markdown).to_string())
} }
pub fn review_edits_in_range( pub fn keep_edits_in_range(
&mut self, &mut self,
buffer: Entity<language::Buffer>, buffer: Entity<language::Buffer>,
buffer_range: Range<language::Anchor>, buffer_range: Range<language::Anchor>,
accept: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.action_log.update(cx, |action_log, cx| { self.action_log.update(cx, |action_log, cx| {
action_log.review_edits_in_range(buffer, buffer_range, accept, cx) action_log.keep_edits_in_range(buffer, buffer_range, cx)
}); });
} }
/// Keeps all edits across all buffers at once.
/// This provides a more performant alternative to calling review_edits_in_range for each buffer.
pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) { pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
self.action_log self.action_log
.update(cx, |action_log, _cx| action_log.keep_all_edits()); .update(cx, |action_log, cx| action_log.keep_all_edits(cx));
} }
pub fn action_log(&self) -> &Entity<ActionLog> { pub fn action_log(&self) -> &Entity<ActionLog> {

View file

@ -13,11 +13,11 @@ path = "src/assistant_tool.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true buffer_diff.workspace = true
clock.workspace = true clock.workspace = true
collections.workspace = true collections.workspace = true
derive_more.workspace = true derive_more.workspace = true
futures.workspace = true
gpui.workspace = true gpui.workspace = true
icons.workspace = true icons.workspace = true
language.workspace = true language.workspace = true
@ -27,15 +27,21 @@ project.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
text.workspace = true text.workspace = true
util.workspace = true
[dev-dependencies] [dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] }
log.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] }
rand.workspace = true
settings = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] }

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,6 @@ path = "src/assistant_tools.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
clock.workspace = true
chrono.workspace = true chrono.workspace = true
collections.workspace = true collections.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
@ -42,7 +41,6 @@ worktree.workspace = true
open = { workspace = true } open = { workspace = true }
[dev-dependencies] [dev-dependencies]
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] }

View file

@ -92,10 +92,11 @@ impl Tool for CreateFileTool {
})? })?
.await .await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?; .map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
let edit_id = buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx))?; cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| { action_log.update(cx, |action_log, cx| {
action_log.will_create_buffer(buffer.clone(), edit_id, cx) action_log.will_create_buffer(buffer.clone(), cx)
});
})?; })?;
project project

View file

@ -174,7 +174,6 @@ enum EditorResponse {
struct AppliedAction { struct AppliedAction {
source: String, source: String,
buffer: Entity<language::Buffer>, buffer: Entity<language::Buffer>,
edit_ids: Vec<clock::Lamport>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -339,18 +338,17 @@ impl EditToolRequest {
self.push_search_error(error); self.push_search_error(error);
} }
DiffResult::Diff(diff) => { DiffResult::Diff(diff) => {
let edit_ids = buffer.update(cx, |buffer, cx| { cx.update(|cx| {
buffer.finalize_last_transaction(); buffer.update(cx, |buffer, cx| {
buffer.apply_diff(diff, false, cx); buffer.finalize_last_transaction();
let transaction = buffer.finalize_last_transaction(); buffer.apply_diff(diff, cx);
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone()) buffer.finalize_last_transaction();
});
self.action_log
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
})?; })?;
self.push_applied_action(AppliedAction { self.push_applied_action(AppliedAction { source, buffer });
source,
buffer,
edit_ids,
});
} }
} }
@ -473,9 +471,6 @@ impl EditToolRequest {
for action in applied { for action in applied {
changed_buffers.insert(action.buffer.clone()); changed_buffers.insert(action.buffer.clone());
self.action_log.update(cx, |log, cx| {
log.buffer_edited(action.buffer, action.edit_ids, cx)
})?;
write!(&mut output, "\n\n{}", action.source)?; write!(&mut output, "\n\n{}", action.source)?;
} }

View file

@ -1,7 +1,7 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for}; use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task}; use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project; use project::Project;
use schemars::JsonSchema; use schemars::JsonSchema;
@ -165,7 +165,7 @@ impl Tool for FindReplaceFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))), Err(err) => return Task::ready(Err(anyhow!(err))),
}; };
cx.spawn(async move |cx| { cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| { let project_path = project.read_with(cx, |project, cx| {
project project
.find_project_path(&input.path, cx) .find_project_path(&input.path, cx)
@ -225,20 +225,18 @@ impl Tool for FindReplaceFileTool {
return Err(err) return Err(err)
}; };
let (edit_ids, snapshot) = buffer.update(cx, |buffer, cx| { let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(); buffer.finalize_last_transaction();
buffer.apply_diff(diff, false, cx); buffer.apply_diff(diff, cx);
let transaction = buffer.finalize_last_transaction(); buffer.finalize_last_transaction();
let edit_ids = transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone()); buffer.snapshot()
(edit_ids, buffer.snapshot())
})?; })?;
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), edit_ids, cx) log.buffer_edited(buffer.clone(), cx)
})?; })?;
project.update(cx, |project, cx| { project.update( cx, |project, cx| {
project.save_buffer(buffer, cx) project.save_buffer(buffer, cx)
})?.await?; })?.await?;
@ -249,6 +247,7 @@ impl Tool for FindReplaceFileTool {
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str)) Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
}) })
} }
} }

View file

@ -518,7 +518,7 @@ mod tests {
// Call replace_flexible and transform the result // Call replace_flexible and transform the result
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| { replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, false, cx); let _ = buffer.apply_diff(diff, cx);
buffer.text() buffer.text()
}) })
}) })

View file

@ -377,7 +377,6 @@ impl RealGitRepository {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GitRepositoryCheckpoint { pub struct GitRepositoryCheckpoint {
ref_name: String, ref_name: String,
head_sha: Option<Oid>,
commit_sha: Oid, commit_sha: Oid,
} }
@ -1213,11 +1212,6 @@ impl GitRepository for RealGitRepository {
Ok(GitRepositoryCheckpoint { Ok(GitRepositoryCheckpoint {
ref_name, ref_name,
head_sha: if let Some(head_sha) = head_sha {
Some(head_sha.parse()?)
} else {
None
},
commit_sha: checkpoint_sha.parse()?, commit_sha: checkpoint_sha.parse()?,
}) })
}) })
@ -1252,13 +1246,6 @@ impl GitRepository for RealGitRepository {
}) })
.await?; .await?;
if let Some(head_sha) = checkpoint.head_sha {
git.run(&["reset", "--mixed", &head_sha.to_string()])
.await?;
} else {
git.run(&["update-ref", "-d", "HEAD"]).await?;
}
Ok(()) Ok(())
}) })
.boxed() .boxed()
@ -1269,10 +1256,6 @@ impl GitRepository for RealGitRepository {
left: GitRepositoryCheckpoint, left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint,
) -> BoxFuture<Result<bool>> { ) -> BoxFuture<Result<bool>> {
if left.head_sha != right.head_sha {
return future::ready(Ok(false)).boxed();
}
let working_directory = self.working_directory(); let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone(); let git_binary_path = self.git_binary_path.clone();
@ -1768,7 +1751,6 @@ fn checkpoint_author_envs() -> HashMap<String, String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::status::FileStatus;
use gpui::TestAppContext; use gpui::TestAppContext;
#[gpui::test] #[gpui::test]
@ -1803,7 +1785,6 @@ mod tests {
smol::fs::write(repo_dir.path().join("new_file_before_checkpoint"), "1") smol::fs::write(repo_dir.path().join("new_file_before_checkpoint"), "1")
.await .await
.unwrap(); .unwrap();
let sha_before_checkpoint = repo.head_sha().unwrap();
let checkpoint = repo.checkpoint().await.unwrap(); let checkpoint = repo.checkpoint().await.unwrap();
// Ensure the user can't see any branches after creating a checkpoint. // Ensure the user can't see any branches after creating a checkpoint.
@ -1837,7 +1818,6 @@ mod tests {
repo.gc().await.unwrap(); repo.gc().await.unwrap();
repo.restore_checkpoint(checkpoint.clone()).await.unwrap(); repo.restore_checkpoint(checkpoint.clone()).await.unwrap();
assert_eq!(repo.head_sha().unwrap(), sha_before_checkpoint);
assert_eq!( assert_eq!(
smol::fs::read_to_string(&file_path).await.unwrap(), smol::fs::read_to_string(&file_path).await.unwrap(),
"modified before checkpoint" "modified before checkpoint"
@ -1901,83 +1881,6 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_undoing_commit_via_checkpoint(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let file_path = repo_dir.path().join("file");
smol::fs::write(&file_path, "initial").await.unwrap();
let repo =
RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
repo.stage_paths(
vec![RepoPath::from_str("file")],
Arc::new(HashMap::default()),
)
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
Arc::new(checkpoint_author_envs()),
)
.await
.unwrap();
let initial_commit_sha = repo.head_sha().unwrap();
smol::fs::write(repo_dir.path().join("new_file1"), "content1")
.await
.unwrap();
smol::fs::write(repo_dir.path().join("new_file2"), "content2")
.await
.unwrap();
let checkpoint = repo.checkpoint().await.unwrap();
repo.stage_paths(
vec![
RepoPath::from_str("new_file1"),
RepoPath::from_str("new_file2"),
],
Arc::new(HashMap::default()),
)
.await
.unwrap();
repo.commit(
"Commit new files".into(),
None,
Arc::new(checkpoint_author_envs()),
)
.await
.unwrap();
repo.restore_checkpoint(checkpoint).await.unwrap();
assert_eq!(repo.head_sha().unwrap(), initial_commit_sha);
assert_eq!(
smol::fs::read_to_string(repo_dir.path().join("new_file1"))
.await
.unwrap(),
"content1"
);
assert_eq!(
smol::fs::read_to_string(repo_dir.path().join("new_file2"))
.await
.unwrap(),
"content2"
);
assert_eq!(
repo.status(&[]).await.unwrap().entries.as_ref(),
&[
(RepoPath::from_str("new_file1"), FileStatus::Untracked),
(RepoPath::from_str("new_file2"), FileStatus::Untracked)
]
);
}
#[gpui::test] #[gpui::test]
async fn test_compare_checkpoints(cx: &mut TestAppContext) { async fn test_compare_checkpoints(cx: &mut TestAppContext) {
cx.executor().allow_parking(); cx.executor().allow_parking();

View file

@ -1321,7 +1321,7 @@ impl Buffer {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
if this.version() == diff.base_version { if this.version() == diff.base_version {
this.finalize_last_transaction(); this.finalize_last_transaction();
this.apply_diff(diff, true, cx); this.apply_diff(diff, cx);
tx.send(this.finalize_last_transaction().cloned()).ok(); tx.send(this.finalize_last_transaction().cloned()).ok();
this.has_conflict = false; this.has_conflict = false;
this.did_reload(this.version(), this.line_ending(), new_mtime, cx); this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
@ -1882,14 +1882,7 @@ impl Buffer {
/// Applies a diff to the buffer. If the buffer has changed since the given diff was /// Applies a diff to the buffer. If the buffer has changed since the given diff was
/// calculated, then adjust the diff to account for those changes, and discard any /// calculated, then adjust the diff to account for those changes, and discard any
/// parts of the diff that conflict with those changes. /// parts of the diff that conflict with those changes.
/// pub fn apply_diff(&mut self, diff: Diff, cx: &mut Context<Self>) -> Option<TransactionId> {
/// If `atomic` is true, the diff will be applied as a single edit.
pub fn apply_diff(
&mut self,
diff: Diff,
atomic: bool,
cx: &mut Context<Self>,
) -> Option<TransactionId> {
let snapshot = self.snapshot(); let snapshot = self.snapshot();
let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable(); let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
let mut delta = 0; let mut delta = 0;
@ -1919,17 +1912,7 @@ impl Buffer {
self.start_transaction(); self.start_transaction();
self.text.set_line_ending(diff.line_ending); self.text.set_line_ending(diff.line_ending);
if atomic { self.edit(adjusted_edits, None, cx);
self.edit(adjusted_edits, None, cx);
} else {
let mut delta = 0isize;
for (range, new_text) in adjusted_edits {
let adjusted_range =
(range.start as isize + delta) as usize..(range.end as isize + delta) as usize;
delta += new_text.len() as isize - range.len() as isize;
self.edit([(adjusted_range, new_text)], None, cx);
}
}
self.end_transaction(cx) self.end_transaction(cx)
} }

View file

@ -376,7 +376,7 @@ async fn test_apply_diff(cx: &mut TestAppContext) {
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await; let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.apply_diff(diff, true, cx).unwrap(); buffer.apply_diff(diff, cx).unwrap();
assert_eq!(buffer.text(), text); assert_eq!(buffer.text(), text);
let actual_offsets = anchors let actual_offsets = anchors
.iter() .iter()
@ -390,7 +390,7 @@ async fn test_apply_diff(cx: &mut TestAppContext) {
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await; let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.apply_diff(diff, true, cx).unwrap(); buffer.apply_diff(diff, cx).unwrap();
assert_eq!(buffer.text(), text); assert_eq!(buffer.text(), text);
let actual_offsets = anchors let actual_offsets = anchors
.iter() .iter()
@ -435,7 +435,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
let format_diff = format.await; let format_diff = format.await;
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
let version_before_format = format_diff.base_version.clone(); let version_before_format = format_diff.base_version.clone();
buffer.apply_diff(format_diff, true, cx); buffer.apply_diff(format_diff, cx);
// The outcome depends on the order of concurrent tasks. // The outcome depends on the order of concurrent tasks.
// //

View file

@ -1230,7 +1230,7 @@ impl LocalLspStore {
.await; .await;
buffer.handle.update(cx, |buffer, cx| { buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction(); buffer.start_transaction();
buffer.apply_diff(diff, true, cx); buffer.apply_diff(diff, cx);
transaction_id_format = transaction_id_format =
transaction_id_format.or(buffer.end_transaction(cx)); transaction_id_format.or(buffer.end_transaction(cx));
if let Some(transaction_id) = transaction_id_format { if let Some(transaction_id) = transaction_id_format {
@ -1364,7 +1364,7 @@ impl LocalLspStore {
zlog::trace!(logger => "Applying changes"); zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| { buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction(); buffer.start_transaction();
buffer.apply_diff(diff, true, cx); buffer.apply_diff(diff, cx);
transaction_id_format = transaction_id_format =
transaction_id_format.or(buffer.end_transaction(cx)); transaction_id_format.or(buffer.end_transaction(cx));
if let Some(transaction_id) = transaction_id_format { if let Some(transaction_id) = transaction_id_format {
@ -1407,7 +1407,7 @@ impl LocalLspStore {
zlog::trace!(logger => "Applying changes"); zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| { buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction(); buffer.start_transaction();
buffer.apply_diff(diff, true, cx); buffer.apply_diff(diff, cx);
transaction_id_format = transaction_id_format =
transaction_id_format.or(buffer.end_transaction(cx)); transaction_id_format.or(buffer.end_transaction(cx));
if let Some(transaction_id) = transaction_id_format { if let Some(transaction_id) = transaction_id_format {

View file

@ -224,6 +224,15 @@ where
} }
} }
impl<T> Patch<T> {
pub fn retain_mut<F>(&mut self, f: F)
where
F: FnMut(&mut Edit<T>) -> bool,
{
self.0.retain_mut(f);
}
}
impl<T: Clone> IntoIterator for Patch<T> { impl<T: Clone> IntoIterator for Patch<T> {
type Item = Edit<T>; type Item = Edit<T>;
type IntoIter = std::vec::IntoIter<Edit<T>>; type IntoIter = std::vec::IntoIter<Edit<T>>;