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:
parent
d26c477d86
commit
4a252515b1
19 changed files with 757 additions and 795 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -66,7 +66,7 @@ actions!(
|
||||||
AcceptSuggestedContext,
|
AcceptSuggestedContext,
|
||||||
OpenActiveThreadAsMarkdown,
|
OpenActiveThreadAsMarkdown,
|
||||||
OpenAssistantDiff,
|
OpenAssistantDiff,
|
||||||
ToggleKeep,
|
Keep,
|
||||||
Reject,
|
Reject,
|
||||||
RejectAll,
|
RejectAll,
|
||||||
KeepAll
|
KeepAll
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
@ -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"] }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
//
|
//
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>>;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue