Detailed changes
@@ -693,17 +693,22 @@ name = "assistant_tool"
version = "0.1.0"
dependencies = [
"anyhow",
- "async-watch",
"buffer_diff",
"clock",
"collections",
+ "ctor",
"derive_more",
+ "env_logger 0.11.7",
+ "futures 0.3.31",
"gpui",
"icons",
"language",
"language_model",
+ "log",
"parking_lot",
+ "pretty_assertions",
"project",
+ "rand 0.8.5",
"serde",
"serde_json",
"settings",
@@ -718,7 +723,6 @@ dependencies = [
"anyhow",
"assistant_tool",
"chrono",
- "clock",
"collections",
"feature_flags",
"futures 0.3.31",
@@ -149,7 +149,7 @@
{
"context": "AssistantDiff",
"bindings": {
- "ctrl-y": "agent::ToggleKeep",
+ "ctrl-y": "agent::Keep",
"ctrl-k ctrl-r": "agent::Reject"
}
},
@@ -241,7 +241,7 @@
"context": "AssistantDiff",
"use_key_equivalents": true,
"bindings": {
- "cmd-y": "agent::ToggleKeep",
+ "cmd-y": "agent::Keep",
"cmd-alt-z": "agent::Reject"
}
},
@@ -66,7 +66,7 @@ actions!(
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown,
OpenAssistantDiff,
- ToggleKeep,
+ Keep,
Reject,
RejectAll,
KeepAll
@@ -1,4 +1,4 @@
-use crate::{Thread, ThreadEvent, ToggleKeep};
+use crate::{Thread, ThreadEvent};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::HashSet;
@@ -78,7 +78,7 @@ impl AssistantDiff {
hunk_range,
is_created_file,
line_height,
- _editor: &Entity<Editor>,
+ editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App| {
render_diff_hunk_controls(
@@ -88,6 +88,7 @@ impl AssistantDiff {
is_created_file,
line_height,
&assistant_diff,
+ editor,
window,
cx,
)
@@ -130,7 +131,7 @@ impl AssistantDiff {
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
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 {
continue;
};
@@ -139,7 +140,7 @@ impl AssistantDiff {
paths_to_delete.remove(&path_key);
let snapshot = buffer.read(cx).snapshot();
- let diff = changed.diff.read(cx);
+ let diff = diff_handle.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(
language::Anchor::MIN..language::Anchor::MAX,
@@ -159,7 +160,7 @@ impl AssistantDiff {
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
- multibuffer.add_diff(changed.diff.clone(), cx);
+ multibuffer.add_diff(diff_handle, cx);
(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
.editor
.read(cx)
@@ -240,8 +241,7 @@ impl AssistantDiff {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| {
- let accept = hunk.status().has_secondary_hunk();
- thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
+ thread.keep_edits_in_range(buffer, hunk.buffer_range, cx)
});
}
}
@@ -268,10 +268,9 @@ impl AssistantDiff {
.update(cx, |thread, cx| thread.keep_all_edits(cx));
}
- fn review_diff_hunks(
+ fn keep_edits_in_ranges(
&mut self,
hunk_ranges: Vec<Range<editor::Anchor>>,
- accept: bool,
cx: &mut Context<Self>,
) {
let snapshot = self.multibuffer.read(cx).snapshot(cx);
@@ -285,7 +284,7 @@ impl AssistantDiff {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
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 {
"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_all))
.on_action(cx.listener(Self::keep_all))
@@ -495,16 +494,16 @@ impl Render for AssistantDiff {
fn render_diff_hunk_controls(
row: u32,
- status: &DiffHunkStatus,
+ _status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
assistant_diff: &Entity<AssistantDiff>,
+ editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
- let editor = assistant_diff.read(cx).editor.clone();
-
+ let editor = editor.clone();
h_flex()
.h(line_height)
.mr_0p5()
@@ -519,75 +518,47 @@ fn render_diff_hunk_controls(
.gap_1()
.occlude()
.shadow_md()
- .children(if status.has_secondary_hunk() {
- vec![
- Button::new("reject", "Reject")
- .disabled(is_created_file)
- .key_binding(
- KeyBinding::for_action_in(
- &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.))),
+ .children(vec![
+ Button::new("reject", "Reject")
+ .disabled(is_created_file)
+ .key_binding(
+ KeyBinding::for_action_in(
+ &crate::Reject,
+ &editor.read(cx).focus_handle(cx),
+ window,
+ cx,
)
- .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,
+ .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::Keep,
&editor.read(cx).focus_handle(cx),
window,
cx,
- ))
- .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],
- false,
- 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(
!editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
|el| {
@@ -245,9 +245,6 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
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);
})
.ok();
@@ -546,7 +543,7 @@ impl Render for MessageEditor {
parent.child(
v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.into_iter().enumerate().flat_map(
- |(index, (buffer, changed))| {
+ |(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
@@ -619,25 +616,13 @@ impl Render for MessageEditor {
.color(Color::Deleted),
),
)
- .when(!changed.needs_review, |parent| {
- parent.child(
- Icon::new(IconName::Check)
- .color(Color::Success),
- )
- })
.child(
div()
.h_full()
.absolute()
.w_8()
.bottom_0()
- .map(|this| {
- if !changed.needs_review {
- this.right_4()
- } else {
- this.right_0()
- }
- })
+ .right_0()
.bg(linear_gradient(
90.,
linear_color_stop(
@@ -1679,23 +1679,20 @@ impl Thread {
Ok(String::from_utf8_lossy(&markdown).to_string())
}
- pub fn review_edits_in_range(
+ pub fn keep_edits_in_range(
&mut self,
buffer: Entity<language::Buffer>,
buffer_range: Range<language::Anchor>,
- accept: bool,
cx: &mut Context<Self>,
) {
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>) {
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> {
@@ -13,11 +13,11 @@ path = "src/assistant_tool.rs"
[dependencies]
anyhow.workspace = true
-async-watch.workspace = true
buffer_diff.workspace = true
clock.workspace = true
collections.workspace = true
derive_more.workspace = true
+futures.workspace = true
gpui.workspace = true
icons.workspace = true
language.workspace = true
@@ -27,15 +27,21 @@ project.workspace = true
serde.workspace = true
serde_json.workspace = true
text.workspace = true
+util.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
+ctor.workspace = true
+env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { 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"] }
+rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
@@ -1,11 +1,12 @@
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
-use collections::{BTreeMap, HashMap, HashSet};
+use collections::{BTreeMap, HashSet};
+use futures::{StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
-use language::{
- Buffer, BufferEvent, DiskState, OffsetRangeExt, Operation, TextBufferSnapshot, ToOffset,
-};
-use std::{ops::Range, sync::Arc};
+use language::{Buffer, BufferEvent, DiskState, Point};
+use std::{cmp, ops::Range, sync::Arc};
+use text::{Edit, Patch, Rope};
+use util::RangeExt;
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -28,21 +29,6 @@ impl ActionLog {
}
}
- pub fn clear_reviewed_changes(&mut self, cx: &mut Context<Self>) {
- self.tracked_buffers
- .retain(|_buffer, tracked_buffer| match &mut tracked_buffer.change {
- Change::Edited {
- accepted_edit_ids, ..
- } => {
- accepted_edit_ids.clear();
- tracked_buffer.schedule_diff_update();
- true
- }
- Change::Deleted { reviewed, .. } => !*reviewed,
- });
- cx.notify();
- }
-
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
@@ -64,27 +50,31 @@ impl ActionLog {
.entry(buffer.clone())
.or_insert_with(|| {
let text_snapshot = buffer.read(cx).text_snapshot();
- let unreviewed_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
- let diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&text_snapshot, cx);
- diff.set_secondary_diff(unreviewed_diff.clone());
- diff
- });
- let (diff_update_tx, diff_update_rx) = async_watch::channel(());
+ let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
+ let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
+ let base_text;
+ let status;
+ let unreviewed_changes;
+ if created {
+ base_text = Rope::default();
+ status = TrackedBufferStatus::Created;
+ unreviewed_changes = Patch::new(vec![Edit {
+ old: 0..1,
+ new: 0..text_snapshot.max_point().row + 1,
+ }])
+ } else {
+ base_text = buffer.read(cx).as_rope().clone();
+ status = TrackedBufferStatus::Modified;
+ unreviewed_changes = Patch::default();
+ }
TrackedBuffer {
buffer: buffer.clone(),
- change: Change::Edited {
- unreviewed_edit_ids: HashSet::default(),
- accepted_edit_ids: HashSet::default(),
- initial_content: if created {
- None
- } else {
- Some(text_snapshot.clone())
- },
- },
+ base_text,
+ unreviewed_changes,
+ snapshot: text_snapshot.clone(),
+ status,
version: buffer.read(cx).version(),
diff,
- secondary_diff: unreviewed_diff,
diff_update: diff_update_tx,
_maintain_diff: cx.spawn({
let buffer = buffer.clone();
@@ -108,9 +98,7 @@ impl ActionLog {
cx: &mut Context<Self>,
) {
match event {
- BufferEvent::Operation { operation, .. } => {
- self.handle_buffer_operation(buffer, operation, cx)
- }
+ BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
BufferEvent::FileHandleChanged => {
self.handle_buffer_file_changed(buffer, cx);
}
@@ -118,62 +106,11 @@ impl ActionLog {
};
}
- fn handle_buffer_operation(
- &mut self,
- buffer: Entity<Buffer>,
- operation: &Operation,
- cx: &mut Context<Self>,
- ) {
+ fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return;
};
- let Operation::Buffer(text::Operation::Edit(operation)) = operation else {
- return;
- };
- let Change::Edited {
- unreviewed_edit_ids,
- accepted_edit_ids,
- ..
- } = &mut tracked_buffer.change
- else {
- return;
- };
-
- if unreviewed_edit_ids.contains(&operation.timestamp)
- || accepted_edit_ids.contains(&operation.timestamp)
- {
- return;
- }
-
- let buffer = buffer.read(cx);
- let operation_edit_ranges = buffer
- .edited_ranges_for_edit_ids::<usize>([&operation.timestamp])
- .collect::<Vec<_>>();
- let intersects_unreviewed_edits = ranges_intersect(
- operation_edit_ranges.iter().cloned(),
- buffer.edited_ranges_for_edit_ids::<usize>(unreviewed_edit_ids.iter()),
- );
- let mut intersected_accepted_edits = HashSet::default();
- for accepted_edit_id in accepted_edit_ids.iter() {
- let intersects_accepted_edit = ranges_intersect(
- operation_edit_ranges.iter().cloned(),
- buffer.edited_ranges_for_edit_ids::<usize>([accepted_edit_id]),
- );
- if intersects_accepted_edit {
- intersected_accepted_edits.insert(*accepted_edit_id);
- }
- }
-
- // If the buffer operation overlaps with any tracked edits, mark it as unreviewed.
- // If it intersects an already-accepted id, mark that edit as unreviewed again.
- if intersects_unreviewed_edits || !intersected_accepted_edits.is_empty() {
- unreviewed_edit_ids.insert(operation.timestamp);
- for accepted_edit_id in intersected_accepted_edits {
- unreviewed_edit_ids.insert(accepted_edit_id);
- accepted_edit_ids.remove(&accepted_edit_id);
- }
- tracked_buffer.schedule_diff_update();
- }
+ tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
}
fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
@@ -181,36 +118,32 @@ impl ActionLog {
return;
};
- match tracked_buffer.change {
- Change::Deleted { .. } => {
+ match tracked_buffer.status {
+ TrackedBufferStatus::Created | TrackedBufferStatus::Modified => {
if buffer
.read(cx)
.file()
- .map_or(false, |file| file.disk_state() != DiskState::Deleted)
+ .map_or(false, |file| file.disk_state() == DiskState::Deleted)
{
- // If the buffer had been deleted by a tool, but it got
- // resurrected externally, we want to clear the changes we
- // were tracking and reset the buffer's state.
- tracked_buffer.change = Change::Edited {
- unreviewed_edit_ids: HashSet::default(),
- accepted_edit_ids: HashSet::default(),
- initial_content: Some(buffer.read(cx).text_snapshot()),
- };
+ // If the buffer had been edited by a tool, but it got
+ // deleted externally, we want to stop tracking it.
+ self.tracked_buffers.remove(&buffer);
}
- tracked_buffer.schedule_diff_update();
+ cx.notify();
}
- Change::Edited { .. } => {
+ TrackedBufferStatus::Deleted => {
if buffer
.read(cx)
.file()
- .map_or(false, |file| file.disk_state() == DiskState::Deleted)
+ .map_or(false, |file| file.disk_state() != DiskState::Deleted)
{
- // If the buffer had been edited by a tool, but it got
- // deleted externally, we want to stop tracking it.
+ // If the buffer had been deleted by a tool, but it got
+ // resurrected externally, we want to clear the changes we
+ // were tracking and reset the buffer's state.
self.tracked_buffers.remove(&buffer);
- } else {
- tracked_buffer.schedule_diff_update();
+ self.track_buffer(buffer, false, cx);
}
+ cx.notify();
}
}
}
@@ -218,19 +151,77 @@ impl ActionLog {
async fn maintain_diff(
this: WeakEntity<Self>,
buffer: Entity<Buffer>,
- mut diff_update: async_watch::Receiver<()>,
+ mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
cx: &mut AsyncApp,
) -> Result<()> {
- while let Some(_) = diff_update.recv().await.ok() {
- let update = this.update(cx, |this, cx| {
+ while let Some((author, buffer_snapshot)) = diff_update.next().await {
+ let (rebase, diff, language, language_registry) =
+ this.read_with(cx, |this, cx| {
+ let tracked_buffer = this
+ .tracked_buffers
+ .get(&buffer)
+ .context("buffer not tracked")?;
+
+ let rebase = cx.background_spawn({
+ let mut base_text = tracked_buffer.base_text.clone();
+ let old_snapshot = tracked_buffer.snapshot.clone();
+ let new_snapshot = buffer_snapshot.clone();
+ let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
+ async move {
+ let edits = diff_snapshots(&old_snapshot, &new_snapshot);
+ let unreviewed_changes = match author {
+ ChangeAuthor::User => rebase_patch(
+ &unreviewed_changes,
+ edits,
+ &mut base_text,
+ new_snapshot.as_rope(),
+ ),
+ ChangeAuthor::Agent => unreviewed_changes.compose(edits),
+ };
+ (
+ Arc::new(base_text.to_string()),
+ base_text,
+ unreviewed_changes,
+ )
+ }
+ });
+
+ anyhow::Ok((
+ rebase,
+ tracked_buffer.diff.clone(),
+ tracked_buffer.buffer.read(cx).language().cloned(),
+ tracked_buffer.buffer.read(cx).language_registry(),
+ ))
+ })??;
+
+ let (new_base_text, new_base_text_rope, unreviewed_changes) = rebase.await;
+ let diff_snapshot = BufferDiff::update_diff(
+ diff.clone(),
+ buffer_snapshot.clone(),
+ Some(new_base_text),
+ true,
+ false,
+ language,
+ language_registry,
+ cx,
+ )
+ .await;
+ if let Ok(diff_snapshot) = diff_snapshot {
+ diff.update(cx, |diff, cx| {
+ diff.set_snapshot(diff_snapshot, &buffer_snapshot, None, cx)
+ })?;
+ }
+ this.update(cx, |this, cx| {
let tracked_buffer = this
.tracked_buffers
.get_mut(&buffer)
.context("buffer not tracked")?;
- anyhow::Ok(tracked_buffer.update_diff(cx))
+ tracked_buffer.base_text = new_base_text_rope;
+ tracked_buffer.snapshot = buffer_snapshot;
+ tracked_buffer.unreviewed_changes = unreviewed_changes;
+ cx.notify();
+ anyhow::Ok(())
})??;
- update.await;
- this.update(cx, |_this, cx| cx.notify())?;
}
Ok(())
@@ -242,159 +233,126 @@ impl ActionLog {
}
/// Track a buffer as read, so we can notify the model about user edits.
- pub fn will_create_buffer(
- &mut self,
- buffer: Entity<Buffer>,
- edit_id: Option<clock::Lamport>,
- cx: &mut Context<Self>,
- ) {
+ pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer(buffer.clone(), true, cx);
- self.buffer_edited(buffer, edit_id.into_iter().collect(), cx)
+ self.buffer_edited(buffer, cx)
}
/// Mark a buffer as edited, so we can refresh it in the context
- pub fn buffer_edited(
- &mut self,
- buffer: Entity<Buffer>,
- mut edit_ids: Vec<clock::Lamport>,
- cx: &mut Context<Self>,
- ) {
+ pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.stale_buffers_in_context.insert(buffer.clone());
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
-
- match &mut tracked_buffer.change {
- Change::Edited {
- unreviewed_edit_ids,
- ..
- } => {
- unreviewed_edit_ids.extend(edit_ids.iter().copied());
- }
- Change::Deleted {
- deleted_content,
- deletion_id,
- ..
- } => {
- edit_ids.extend(*deletion_id);
- tracked_buffer.change = Change::Edited {
- unreviewed_edit_ids: edit_ids.into_iter().collect(),
- accepted_edit_ids: HashSet::default(),
- initial_content: Some(deleted_content.clone()),
- };
- }
+ if let TrackedBufferStatus::Deleted = tracked_buffer.status {
+ tracked_buffer.status = TrackedBufferStatus::Modified;
}
-
- tracked_buffer.schedule_diff_update();
+ tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
}
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
- if let Change::Edited {
- initial_content, ..
- } = &tracked_buffer.change
- {
- if let Some(initial_content) = initial_content {
- let deletion_id = buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
- tracked_buffer.change = Change::Deleted {
- reviewed: false,
- deleted_content: initial_content.clone(),
- deletion_id,
- };
- tracked_buffer.schedule_diff_update();
- } else {
+ match tracked_buffer.status {
+ TrackedBufferStatus::Created => {
self.tracked_buffers.remove(&buffer);
cx.notify();
}
+ TrackedBufferStatus::Modified => {
+ buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
+ tracked_buffer.status = TrackedBufferStatus::Deleted;
+ tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
+ }
+ TrackedBufferStatus::Deleted => {}
}
+ cx.notify();
}
- /// Accepts edits in a given range within a buffer.
- pub fn review_edits_in_range<T: ToOffset>(
+ pub fn keep_edits_in_range<T>(
&mut self,
buffer: Entity<Buffer>,
buffer_range: Range<T>,
- accept: bool,
cx: &mut Context<Self>,
- ) {
+ ) where
+ T: 'static + language::ToPoint, // + Clone
+ // + Copy
+ // + Ord
+ // + Sub<T, Output = T>
+ // + Add<T, Output = T>
+ // + AddAssign
+ // + Default
+ // + PartialEq,
+ {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return;
};
- let buffer = buffer.read(cx);
- let buffer_range = buffer_range.to_offset(buffer);
-
- match &mut tracked_buffer.change {
- Change::Deleted { reviewed, .. } => {
- *reviewed = accept;
+ match tracked_buffer.status {
+ TrackedBufferStatus::Deleted => {
+ self.tracked_buffers.remove(&buffer);
+ cx.notify();
}
- Change::Edited {
- unreviewed_edit_ids,
- accepted_edit_ids,
- ..
- } => {
- let (source, destination) = if accept {
- (unreviewed_edit_ids, accepted_edit_ids)
- } else {
- (accepted_edit_ids, unreviewed_edit_ids)
- };
- source.retain(|edit_id| {
- for range in buffer.edited_ranges_for_edit_ids::<usize>([edit_id]) {
- if buffer_range.end >= range.start && buffer_range.start <= range.end {
- destination.insert(*edit_id);
- return false;
- }
+ _ => {
+ let buffer = buffer.read(cx);
+ let buffer_range =
+ buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
+ let buffer_row_range = buffer_range.start.row..buffer_range.end.row + 1;
+ let mut delta = 0i32;
+ tracked_buffer.unreviewed_changes.retain_mut(|edit| {
+ edit.old.start = (edit.old.start as i32 + delta) as u32;
+ edit.old.end = (edit.old.end as i32 + delta) as u32;
+ if edit.new.overlaps(&buffer_row_range) {
+ let old_bytes = tracked_buffer
+ .base_text
+ .point_to_offset(Point::new(edit.old.start, 0))
+ ..tracked_buffer.base_text.point_to_offset(cmp::min(
+ Point::new(edit.old.end, 0),
+ tracked_buffer.base_text.max_point(),
+ ));
+ let new_bytes = tracked_buffer
+ .snapshot
+ .point_to_offset(Point::new(edit.new.start, 0))
+ ..tracked_buffer.snapshot.point_to_offset(cmp::min(
+ Point::new(edit.new.end, 0),
+ tracked_buffer.snapshot.max_point(),
+ ));
+ tracked_buffer.base_text.replace(
+ old_bytes,
+ &tracked_buffer
+ .snapshot
+ .text_for_range(new_bytes)
+ .collect::<String>(),
+ );
+ delta += edit.new_len() as i32 - edit.old_len() as i32;
+ false
+ } else {
+ true
}
- true
});
+ tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
}
}
-
- tracked_buffer.schedule_diff_update();
}
- /// Keep all edits across all buffers.
- /// This is a more performant alternative to calling review_edits_in_range for each buffer.
- pub fn keep_all_edits(&mut self) {
- // Process all tracked buffers
- for (_, tracked_buffer) in self.tracked_buffers.iter_mut() {
- match &mut tracked_buffer.change {
- Change::Deleted { reviewed, .. } => {
- *reviewed = true;
- }
- Change::Edited {
- unreviewed_edit_ids,
- accepted_edit_ids,
- ..
- } => {
- accepted_edit_ids.extend(unreviewed_edit_ids.drain());
+ pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
+ self.tracked_buffers
+ .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
+ TrackedBufferStatus::Deleted => false,
+ _ => {
+ tracked_buffer.unreviewed_changes.clear();
+ tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
+ tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
+ true
}
- }
-
- tracked_buffer.schedule_diff_update();
- }
+ });
+ cx.notify();
}
/// Returns the set of buffers that contain changes that haven't been reviewed by the user.
- pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, ChangedBuffer> {
+ pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
self.tracked_buffers
.iter()
.filter(|(_, tracked)| tracked.has_changes(cx))
- .map(|(buffer, tracked)| {
- (
- buffer.clone(),
- ChangedBuffer {
- diff: tracked.diff.clone(),
- needs_review: match &tracked.change {
- Change::Edited {
- unreviewed_edit_ids,
- ..
- } => !unreviewed_edit_ids.is_empty(),
- Change::Deleted { reviewed, .. } => !reviewed,
- },
- },
- )
- })
+ .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
.collect()
}
@@ -419,48 +377,169 @@ impl ActionLog {
}
}
-fn ranges_intersect(
- ranges_a: impl IntoIterator<Item = Range<usize>>,
- ranges_b: impl IntoIterator<Item = Range<usize>>,
-) -> bool {
- let mut ranges_a_iter = ranges_a.into_iter().peekable();
- let mut ranges_b_iter = ranges_b.into_iter().peekable();
- while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
- if range_a.end < range_b.start {
- ranges_a_iter.next();
- } else if range_b.end < range_a.start {
- ranges_b_iter.next();
+fn rebase_patch(
+ patch: &Patch<u32>,
+ edits: Vec<Edit<u32>>,
+ old_text: &mut Rope,
+ new_text: &Rope,
+) -> Patch<u32> {
+ let mut translated_unreviewed_edits = Patch::default();
+ let mut conflicting_edits = Vec::new();
+
+ let mut old_edits = patch.edits().iter().cloned().peekable();
+ let mut new_edits = edits.into_iter().peekable();
+ let mut applied_delta = 0i32;
+ let mut rebased_delta = 0i32;
+
+ while let Some(mut new_edit) = new_edits.next() {
+ let mut conflict = false;
+
+ // Push all the old edits that are before this new edit or that intersect with it.
+ while let Some(old_edit) = old_edits.peek() {
+ if new_edit.old.end <= old_edit.new.start {
+ break;
+ } else if new_edit.old.start >= old_edit.new.end {
+ let mut old_edit = old_edits.next().unwrap();
+ old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
+ old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
+ old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
+ old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
+ rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
+ translated_unreviewed_edits.push(old_edit);
+ } else {
+ conflict = true;
+ if new_edits
+ .peek()
+ .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
+ {
+ new_edit.old.start = (new_edit.old.start as i32 + applied_delta) as u32;
+ new_edit.old.end = (new_edit.old.end as i32 + applied_delta) as u32;
+ conflicting_edits.push(new_edit);
+ new_edit = new_edits.next().unwrap();
+ } else {
+ let mut old_edit = old_edits.next().unwrap();
+ old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
+ old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
+ old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
+ old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
+ rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
+ translated_unreviewed_edits.push(old_edit);
+ }
+ }
+ }
+
+ if conflict {
+ new_edit.old.start = (new_edit.old.start as i32 + applied_delta) as u32;
+ new_edit.old.end = (new_edit.old.end as i32 + applied_delta) as u32;
+ conflicting_edits.push(new_edit);
} else {
- return true;
+ // This edit doesn't intersect with any old edit, so we can apply it to the old text.
+ new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
+ new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
+ let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
+ ..old_text.point_to_offset(cmp::min(
+ Point::new(new_edit.old.end, 0),
+ old_text.max_point(),
+ ));
+ let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
+ ..new_text.point_to_offset(cmp::min(
+ Point::new(new_edit.new.end, 0),
+ new_text.max_point(),
+ ));
+
+ old_text.replace(
+ old_bytes,
+ &new_text.chunks_in_range(new_bytes).collect::<String>(),
+ );
+ applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
+ }
+ }
+
+ // Push all the outstanding old edits.
+ for mut old_edit in old_edits {
+ old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
+ old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
+ old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
+ old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
+ translated_unreviewed_edits.push(old_edit);
+ }
+
+ translated_unreviewed_edits.compose(conflicting_edits)
+}
+
+fn diff_snapshots(
+ old_snapshot: &text::BufferSnapshot,
+ new_snapshot: &text::BufferSnapshot,
+) -> Vec<Edit<u32>> {
+ let mut edits = new_snapshot
+ .edits_since::<Point>(&old_snapshot.version)
+ .map(|edit| {
+ if edit.old.start.column == old_snapshot.line_len(edit.old.start.row)
+ && new_snapshot.chars_at(edit.new.start).next() == Some('\n')
+ && edit.old.start != old_snapshot.max_point()
+ {
+ Edit {
+ old: edit.old.start.row + 1..edit.old.end.row + 1,
+ new: edit.new.start.row + 1..edit.new.end.row + 1,
+ }
+ } else if edit.old.start.column == 0
+ && edit.old.end.column == 0
+ && edit.new.end.column == 0
+ && edit.old.end != old_snapshot.max_point()
+ {
+ Edit {
+ old: edit.old.start.row..edit.old.end.row,
+ new: edit.new.start.row..edit.new.end.row,
+ }
+ } else {
+ Edit {
+ old: edit.old.start.row..edit.old.end.row + 1,
+ new: edit.new.start.row..edit.new.end.row + 1,
+ }
+ }
+ })
+ .peekable();
+ let mut row_edits = Vec::new();
+ while let Some(mut edit) = edits.next() {
+ while let Some(next_edit) = edits.peek() {
+ if edit.old.end >= next_edit.old.start {
+ edit.old.end = next_edit.old.end;
+ edit.new.end = next_edit.new.end;
+ edits.next();
+ } else {
+ break;
+ }
}
+ row_edits.push(edit);
}
- false
+ row_edits
+}
+
+enum ChangeAuthor {
+ User,
+ Agent,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+enum TrackedBufferStatus {
+ Created,
+ Modified,
+ Deleted,
}
struct TrackedBuffer {
buffer: Entity<Buffer>,
- change: Change,
+ base_text: Rope,
+ unreviewed_changes: Patch<u32>,
+ status: TrackedBufferStatus,
version: clock::Global,
diff: Entity<BufferDiff>,
- secondary_diff: Entity<BufferDiff>,
- diff_update: async_watch::Sender<()>,
+ snapshot: text::BufferSnapshot,
+ diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
_maintain_diff: Task<()>,
_subscription: Subscription,
}
-enum Change {
- Edited {
- unreviewed_edit_ids: HashSet<clock::Lamport>,
- accepted_edit_ids: HashSet<clock::Lamport>,
- initial_content: Option<TextBufferSnapshot>,
- },
- Deleted {
- reviewed: bool,
- deleted_content: TextBufferSnapshot,
- deletion_id: Option<clock::Lamport>,
- },
-}
-
impl TrackedBuffer {
fn has_changes(&self, cx: &App) -> bool {
self.diff
@@ -470,165 +549,62 @@ impl TrackedBuffer {
.is_some()
}
- fn schedule_diff_update(&self) {
- self.diff_update.send(()).ok();
- }
-
- fn update_diff(&mut self, cx: &mut App) -> Task<()> {
- match &self.change {
- Change::Edited {
- unreviewed_edit_ids,
- accepted_edit_ids,
- ..
- } => {
- let edits_to_undo = unreviewed_edit_ids
- .iter()
- .chain(accepted_edit_ids)
- .map(|edit_id| (*edit_id, u32::MAX))
- .collect::<HashMap<_, _>>();
- let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
- buffer_without_edits
- .update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
- let primary_diff_update = self.diff.update(cx, |diff, cx| {
- diff.set_base_text_buffer(
- buffer_without_edits,
- self.buffer.read(cx).text_snapshot(),
- cx,
- )
- });
-
- let unreviewed_edits_to_undo = unreviewed_edit_ids
- .iter()
- .map(|edit_id| (*edit_id, u32::MAX))
- .collect::<HashMap<_, _>>();
- let buffer_without_unreviewed_edits =
- self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
- buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
- buffer.undo_operations(unreviewed_edits_to_undo, cx)
- });
- let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
- diff.set_base_text_buffer(
- buffer_without_unreviewed_edits.clone(),
- self.buffer.read(cx).text_snapshot(),
- cx,
- )
- });
-
- cx.background_spawn(async move {
- _ = primary_diff_update.await;
- _ = secondary_diff_update.await;
- })
- }
- Change::Deleted {
- reviewed,
- deleted_content,
- ..
- } => {
- let reviewed = *reviewed;
- let deleted_content = deleted_content.clone();
-
- let primary_diff = self.diff.clone();
- let secondary_diff = self.secondary_diff.clone();
- let buffer_snapshot = self.buffer.read(cx).text_snapshot();
- let language = self.buffer.read(cx).language().cloned();
- let language_registry = self.buffer.read(cx).language_registry().clone();
-
- cx.spawn(async move |cx| {
- let base_text = Arc::new(deleted_content.text());
-
- let primary_diff_snapshot = BufferDiff::update_diff(
- primary_diff.clone(),
- buffer_snapshot.clone(),
- Some(base_text.clone()),
- true,
- false,
- language.clone(),
- language_registry.clone(),
- cx,
- )
- .await;
- let secondary_diff_snapshot = BufferDiff::update_diff(
- secondary_diff.clone(),
- buffer_snapshot.clone(),
- if reviewed {
- None
- } else {
- Some(base_text.clone())
- },
- true,
- false,
- language.clone(),
- language_registry.clone(),
- cx,
- )
- .await;
-
- if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
- primary_diff
- .update(cx, |diff, cx| {
- diff.set_snapshot(primary_diff_snapshot, &buffer_snapshot, None, cx)
- })
- .ok();
- }
-
- if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
- secondary_diff
- .update(cx, |diff, cx| {
- diff.set_snapshot(
- secondary_diff_snapshot,
- &buffer_snapshot,
- None,
- cx,
- )
- })
- .ok();
- }
- })
- }
- }
+ fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
+ self.diff_update
+ .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
+ .ok();
}
}
pub struct ChangedBuffer {
pub diff: Entity<BufferDiff>,
- pub needs_review: bool,
}
#[cfg(test)]
mod tests {
+ use std::env;
+
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use language::Point;
use project::{FakeFs, Fs, Project, RemoveOptions};
+ use rand::prelude::*;
use serde_json::json;
use settings::SettingsStore;
- use util::path;
+ use util::{RandomCharIter, path, post_inc};
+
+ #[ctor::ctor]
+ fn init_logger() {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::init();
+ }
+ }
#[gpui::test(iterations = 10)]
async fn test_edit_review(cx: &mut TestAppContext) {
let action_log = cx.new(|_| ActionLog::new());
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
- let edit1 = buffer.update(cx, |buffer, cx| {
- buffer
- .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
- .unwrap()
- });
- let edit2 = buffer.update(cx, |buffer, cx| {
- buffer
- .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
- .unwrap()
+ cx.update(|cx| {
+ action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
+ buffer.update(cx, |buffer, cx| {
+ buffer
+ .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
+ .unwrap()
+ });
+ buffer.update(cx, |buffer, cx| {
+ buffer
+ .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
+ .unwrap()
+ });
+ action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
+ cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abc\ndEf\nghi\njkl\nmnO"
);
-
- action_log.update(cx, |log, cx| {
- log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx)
- });
- cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
@@ -636,13 +612,11 @@ mod tests {
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(2, 0),
- review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(4, 0)..Point::new(4, 3),
- review_status: ReviewStatus::Unreviewed,
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
@@ -651,84 +625,26 @@ mod tests {
);
action_log.update(cx, |log, cx| {
- log.review_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), true, cx)
+ log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
});
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
- vec![
- HunkStatus {
- range: Point::new(1, 0)..Point::new(2, 0),
- review_status: ReviewStatus::Unreviewed,
- diff_status: DiffHunkStatusKind::Modified,
- old_text: "def\n".into(),
- },
- HunkStatus {
- range: Point::new(4, 0)..Point::new(4, 3),
- review_status: ReviewStatus::Reviewed,
- diff_status: DiffHunkStatusKind::Modified,
- old_text: "mno".into(),
- }
- ],
- )]
- );
-
- action_log.update(cx, |log, cx| {
- log.review_edits_in_range(
- buffer.clone(),
- Point::new(3, 0)..Point::new(4, 3),
- false,
- cx,
- )
- });
- cx.run_until_parked();
- assert_eq!(
- unreviewed_hunks(&action_log, cx),
- vec![(
- buffer.clone(),
- vec![
- HunkStatus {
- range: Point::new(1, 0)..Point::new(2, 0),
- review_status: ReviewStatus::Unreviewed,
- diff_status: DiffHunkStatusKind::Modified,
- old_text: "def\n".into(),
- },
- HunkStatus {
- range: Point::new(4, 0)..Point::new(4, 3),
- review_status: ReviewStatus::Unreviewed,
- diff_status: DiffHunkStatusKind::Modified,
- old_text: "mno".into(),
- }
- ],
+ vec![HunkStatus {
+ range: Point::new(1, 0)..Point::new(2, 0),
+ diff_status: DiffHunkStatusKind::Modified,
+ old_text: "def\n".into(),
+ }],
)]
);
action_log.update(cx, |log, cx| {
- log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), true, cx)
+ log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
});
cx.run_until_parked();
- assert_eq!(
- unreviewed_hunks(&action_log, cx),
- vec![(
- buffer.clone(),
- vec![
- HunkStatus {
- range: Point::new(1, 0)..Point::new(2, 0),
- review_status: ReviewStatus::Reviewed,
- diff_status: DiffHunkStatusKind::Modified,
- old_text: "def\n".into(),
- },
- HunkStatus {
- range: Point::new(4, 0)..Point::new(4, 3),
- review_status: ReviewStatus::Reviewed,
- diff_status: DiffHunkStatusKind::Modified,
- old_text: "mno".into(),
- }
- ],
- )]
- );
+ assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
@@ -14,7 +14,6 @@ path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
-clock.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
@@ -42,7 +41,6 @@ worktree.workspace = true
open = { workspace = true }
[dev-dependencies]
-clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
@@ -92,10 +92,11 @@ impl Tool for CreateFileTool {
})?
.await
.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))?;
-
- action_log.update(cx, |action_log, cx| {
- action_log.will_create_buffer(buffer.clone(), edit_id, cx)
+ cx.update(|cx| {
+ buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
+ action_log.update(cx, |action_log, cx| {
+ action_log.will_create_buffer(buffer.clone(), cx)
+ });
})?;
project
@@ -174,7 +174,6 @@ enum EditorResponse {
struct AppliedAction {
source: String,
buffer: Entity<language::Buffer>,
- edit_ids: Vec<clock::Lamport>,
}
#[derive(Debug)]
@@ -339,18 +338,17 @@ impl EditToolRequest {
self.push_search_error(error);
}
DiffResult::Diff(diff) => {
- let edit_ids = buffer.update(cx, |buffer, cx| {
- buffer.finalize_last_transaction();
- buffer.apply_diff(diff, false, cx);
- let transaction = buffer.finalize_last_transaction();
- transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
+ cx.update(|cx| {
+ buffer.update(cx, |buffer, cx| {
+ buffer.finalize_last_transaction();
+ buffer.apply_diff(diff, cx);
+ buffer.finalize_last_transaction();
+ });
+ self.action_log
+ .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
})?;
- self.push_applied_action(AppliedAction {
- source,
- buffer,
- edit_ids,
- });
+ self.push_applied_action(AppliedAction { source, buffer });
}
}
@@ -473,9 +471,6 @@ impl EditToolRequest {
for action in applied {
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)?;
}
@@ -1,7 +1,7 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
-use gpui::{App, AppContext, Entity, Task};
+use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -165,7 +165,7 @@ impl Tool for FindReplaceFileTool {
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| {
project
.find_project_path(&input.path, cx)
@@ -225,20 +225,18 @@ impl Tool for FindReplaceFileTool {
return Err(err)
};
- let (edit_ids, snapshot) = buffer.update(cx, |buffer, cx| {
+ let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
- buffer.apply_diff(diff, false, cx);
- let transaction = buffer.finalize_last_transaction();
- let edit_ids = transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone());
-
- (edit_ids, buffer.snapshot())
+ buffer.apply_diff(diff, cx);
+ buffer.finalize_last_transaction();
+ buffer.snapshot()
})?;
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)
})?.await?;
@@ -249,6 +247,7 @@ impl Tool for FindReplaceFileTool {
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
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
buffer.update(cx, |buffer, cx| {
- let _ = buffer.apply_diff(diff, false, cx);
+ let _ = buffer.apply_diff(diff, cx);
buffer.text()
})
})
@@ -377,7 +377,6 @@ impl RealGitRepository {
#[derive(Clone, Debug)]
pub struct GitRepositoryCheckpoint {
ref_name: String,
- head_sha: Option<Oid>,
commit_sha: Oid,
}
@@ -1213,11 +1212,6 @@ impl GitRepository for RealGitRepository {
Ok(GitRepositoryCheckpoint {
ref_name,
- head_sha: if let Some(head_sha) = head_sha {
- Some(head_sha.parse()?)
- } else {
- None
- },
commit_sha: checkpoint_sha.parse()?,
})
})
@@ -1252,13 +1246,6 @@ impl GitRepository for RealGitRepository {
})
.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(())
})
.boxed()
@@ -1269,10 +1256,6 @@ impl GitRepository for RealGitRepository {
left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint,
) -> BoxFuture<Result<bool>> {
- if left.head_sha != right.head_sha {
- return future::ready(Ok(false)).boxed();
- }
-
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1768,7 +1751,6 @@ fn checkpoint_author_envs() -> HashMap<String, String> {
#[cfg(test)]
mod tests {
use super::*;
- use crate::status::FileStatus;
use gpui::TestAppContext;
#[gpui::test]
@@ -1803,7 +1785,6 @@ mod tests {
smol::fs::write(repo_dir.path().join("new_file_before_checkpoint"), "1")
.await
.unwrap();
- let sha_before_checkpoint = repo.head_sha().unwrap();
let checkpoint = repo.checkpoint().await.unwrap();
// Ensure the user can't see any branches after creating a checkpoint.
@@ -1837,7 +1818,6 @@ mod tests {
repo.gc().await.unwrap();
repo.restore_checkpoint(checkpoint.clone()).await.unwrap();
- assert_eq!(repo.head_sha().unwrap(), sha_before_checkpoint);
assert_eq!(
smol::fs::read_to_string(&file_path).await.unwrap(),
"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]
async fn test_compare_checkpoints(cx: &mut TestAppContext) {
cx.executor().allow_parking();
@@ -1321,7 +1321,7 @@ impl Buffer {
this.update(cx, |this, cx| {
if this.version() == diff.base_version {
this.finalize_last_transaction();
- this.apply_diff(diff, true, cx);
+ this.apply_diff(diff, cx);
tx.send(this.finalize_last_transaction().cloned()).ok();
this.has_conflict = false;
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
/// calculated, then adjust the diff to account for those changes, and discard any
/// parts of the diff that conflict with those changes.
- ///
- /// 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> {
+ pub fn apply_diff(&mut self, diff: Diff, cx: &mut Context<Self>) -> Option<TransactionId> {
let snapshot = self.snapshot();
let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
let mut delta = 0;
@@ -1919,17 +1912,7 @@ impl Buffer {
self.start_transaction();
self.text.set_line_ending(diff.line_ending);
- if atomic {
- 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.edit(adjusted_edits, None, 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;
buffer.update(cx, |buffer, cx| {
- buffer.apply_diff(diff, true, cx).unwrap();
+ buffer.apply_diff(diff, cx).unwrap();
assert_eq!(buffer.text(), text);
let actual_offsets = anchors
.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;
buffer.update(cx, |buffer, cx| {
- buffer.apply_diff(diff, true, cx).unwrap();
+ buffer.apply_diff(diff, cx).unwrap();
assert_eq!(buffer.text(), text);
let actual_offsets = anchors
.iter()
@@ -435,7 +435,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
let format_diff = format.await;
buffer.update(cx, |buffer, cx| {
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.
//
@@ -1230,7 +1230,7 @@ impl LocalLspStore {
.await;
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
- buffer.apply_diff(diff, true, cx);
+ buffer.apply_diff(diff, cx);
transaction_id_format =
transaction_id_format.or(buffer.end_transaction(cx));
if let Some(transaction_id) = transaction_id_format {
@@ -1364,7 +1364,7 @@ impl LocalLspStore {
zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
- buffer.apply_diff(diff, true, cx);
+ buffer.apply_diff(diff, cx);
transaction_id_format =
transaction_id_format.or(buffer.end_transaction(cx));
if let Some(transaction_id) = transaction_id_format {
@@ -1407,7 +1407,7 @@ impl LocalLspStore {
zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
- buffer.apply_diff(diff, true, cx);
+ buffer.apply_diff(diff, cx);
transaction_id_format =
transaction_id_format.or(buffer.end_transaction(cx));
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> {
type Item = Edit<T>;
type IntoIter = std::vec::IntoIter<Edit<T>>;