use anyhow::{Context as _, Result};use buffer_diff::BufferDiff;use collections::BTreeMap;use futures::{FutureExt, StreamExt, channel::mpsc};use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};use std::{cmp, ops::Range, sync::Arc};use text::{Edit, Patch, Rope};use util::RangeExt; /// Tracks actions performed by tools in a threadpub struct ActionLog { /// Buffers that we want to notify the model about when they change. tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>, /// Has the model edited a file since it last checked diagnostics? edited_since_project_diagnostics_check: bool, /// The project this action log is associated with project: Entity<Project>,} impl ActionLog { /// Creates a new, empty action log associated with the given project. pub fn new(project: Entity<Project>) -> Self { Self { tracked_buffers: BTreeMap::default(), edited_since_project_diagnostics_check: false, project, } } pub fn project(&self) -> &Entity<Project> { &self.project } /// Notifies a diagnostics check pub fn checked_project_diagnostics(&mut self) { self.edited_since_project_diagnostics_check = false; } /// Returns true if any files have been edited since the last project diagnostics check pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool { self.edited_since_project_diagnostics_check } fn track_buffer_internal( &mut self, buffer: Entity<Buffer>, is_created: bool, cx: &mut Context<Self>, ) -> &mut TrackedBuffer { let status = if is_created { if let Some(tracked) = self.tracked_buffers.remove(&buffer) { match tracked.status { TrackedBufferStatus::Created { existing_file_content, } => TrackedBufferStatus::Created { existing_file_content, }, TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => { TrackedBufferStatus::Created { existing_file_content: Some(tracked.diff_base), } } } } else if buffer .read(cx) .file() .map_or(false, |file| file.disk_state().exists()) { TrackedBufferStatus::Created { existing_file_content: Some(buffer.read(cx).as_rope().clone()), } } else { TrackedBufferStatus::Created { existing_file_content: None, } } } else { TrackedBufferStatus::Modified }; let tracked_buffer = self .tracked_buffers .entry(buffer.clone()) .or_insert_with(|| { let open_lsp_handle = self.project.update(cx, |project, cx| { project.register_buffer_with_language_servers(&buffer, cx) }); let text_snapshot = buffer.read(cx).text_snapshot(); let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); let diff_base; let unreviewed_edits; if is_created { diff_base = Rope::default(); unreviewed_edits = Patch::new(vec![Edit { old: 0..1, new: 0..text_snapshot.max_point().row + 1, }]) } else { diff_base = buffer.read(cx).as_rope().clone(); unreviewed_edits = Patch::default(); } TrackedBuffer { buffer: buffer.clone(), diff_base, unreviewed_edits: unreviewed_edits, snapshot: text_snapshot.clone(), status, version: buffer.read(cx).version(), diff, diff_update: diff_update_tx, _open_lsp_handle: open_lsp_handle, _maintain_diff: cx.spawn({ let buffer = buffer.clone(); async move |this, cx| { Self::maintain_diff(this, buffer, diff_update_rx, cx) .await .ok(); } }), _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), } }); tracked_buffer.version = buffer.read(cx).version(); tracked_buffer } fn handle_buffer_event( &mut self, buffer: Entity<Buffer>, event: &BufferEvent, cx: &mut Context<Self>, ) { match event { BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), BufferEvent::FileHandleChanged => { self.handle_buffer_file_changed(buffer, cx); } _ => {} }; } 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; }; tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); } fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { return; }; match tracked_buffer.status { TrackedBufferStatus::Created { .. } | TrackedBufferStatus::Modified => { if buffer .read(cx) .file() .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. self.tracked_buffers.remove(&buffer); } cx.notify(); } TrackedBufferStatus::Deleted => { if buffer .read(cx) .file() .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 edits we // were tracking and reset the buffer's state. self.tracked_buffers.remove(&buffer); self.track_buffer_internal(buffer, false, cx); } cx.notify(); } } } async fn maintain_diff( this: WeakEntity<Self>, buffer: Entity<Buffer>, mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>, cx: &mut AsyncApp, ) -> Result<()> { let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?; let git_diff = this .update(cx, |this, cx| { this.project.update(cx, |project, cx| { project.open_uncommitted_diff(buffer.clone(), cx) }) })? .await .ok(); let buffer_repo = git_store.read_with(cx, |git_store, cx| { git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) })?; let (git_diff_updates_tx, mut git_diff_updates_rx) = async_watch::channel(()); let _repo_subscription = if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) { cx.update(|cx| { let mut old_head = buffer_repo.read(cx).head_commit.clone(); Some(cx.subscribe(git_diff, move |_, event, cx| match event { buffer_diff::BufferDiffEvent::DiffChanged { .. } => { let new_head = buffer_repo.read(cx).head_commit.clone(); if new_head != old_head { old_head = new_head; git_diff_updates_tx.send(()).ok(); } } _ => {} })) })? } else { None }; loop { futures::select_biased! { buffer_update = buffer_updates.next() => { if let Some((author, buffer_snapshot)) = buffer_update { Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?; } else { break; } } _ = git_diff_updates_rx.changed().fuse() => { if let Some(git_diff) = git_diff.as_ref() { Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?; } } } } Ok(()) } async fn track_edits( this: &WeakEntity<ActionLog>, buffer: &Entity<Buffer>, author: ChangeAuthor, buffer_snapshot: text::BufferSnapshot, cx: &mut AsyncApp, ) -> Result<()> { let rebase = 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.diff_base.clone(); let old_snapshot = tracked_buffer.snapshot.clone(); let new_snapshot = buffer_snapshot.clone(); let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); async move { let edits = diff_snapshots(&old_snapshot, &new_snapshot); if let ChangeAuthor::User = author { apply_non_conflicting_edits( &unreviewed_edits, edits, &mut base_text, new_snapshot.as_rope(), ); } (Arc::new(base_text.to_string()), base_text) } }); anyhow::Ok(rebase) })??; let (new_base_text, new_diff_base) = rebase.await; Self::update_diff( this, buffer, buffer_snapshot, new_base_text, new_diff_base, cx, ) .await } async fn keep_committed_edits( this: &WeakEntity<ActionLog>, buffer: &Entity<Buffer>, git_diff: &Entity<BufferDiff>, cx: &mut AsyncApp, ) -> Result<()> { let buffer_snapshot = this.read_with(cx, |this, _cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; anyhow::Ok(tracked_buffer.snapshot.clone()) })??; let (new_base_text, new_diff_base) = this .read_with(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); let agent_diff_base = tracked_buffer.diff_base.clone(); let git_diff_base = git_diff.read(cx).base_text().as_rope().clone(); let buffer_text = tracked_buffer.snapshot.as_rope().clone(); anyhow::Ok(cx.background_spawn(async move { let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable(); let committed_edits = language::line_diff( &agent_diff_base.to_string(), &git_diff_base.to_string(), ) .into_iter() .map(|(old, new)| Edit { old, new }); let mut new_agent_diff_base = agent_diff_base.clone(); let mut row_delta = 0i32; for committed in committed_edits { while let Some(unreviewed) = old_unreviewed_edits.peek() { // If the committed edit matches the unreviewed // edit, assume the user wants to keep it. if committed.old == unreviewed.old { let unreviewed_new = buffer_text.slice_rows(unreviewed.new.clone()).to_string(); let committed_new = git_diff_base.slice_rows(committed.new.clone()).to_string(); if unreviewed_new == committed_new { let old_byte_start = new_agent_diff_base.point_to_offset(Point::new( (unreviewed.old.start as i32 + row_delta) as u32, 0, )); let old_byte_end = new_agent_diff_base.point_to_offset(cmp::min( Point::new( (unreviewed.old.end as i32 + row_delta) as u32, 0, ), new_agent_diff_base.max_point(), )); new_agent_diff_base .replace(old_byte_start..old_byte_end, &unreviewed_new); row_delta += unreviewed.new_len() as i32 - unreviewed.old_len() as i32; } } else if unreviewed.old.start >= committed.old.end { break; } old_unreviewed_edits.next().unwrap(); } } ( Arc::new(new_agent_diff_base.to_string()), new_agent_diff_base, ) })) })?? .await; Self::update_diff( this, buffer, buffer_snapshot, new_base_text, new_diff_base, cx, ) .await } async fn update_diff( this: &WeakEntity<ActionLog>, buffer: &Entity<Buffer>, buffer_snapshot: text::BufferSnapshot, new_base_text: Arc<String>, new_diff_base: Rope, cx: &mut AsyncApp, ) -> Result<()> { let (diff, language, language_registry) = this.read_with(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), buffer.read(cx).language_registry().clone(), )) })??; let diff_snapshot = BufferDiff::update_diff( diff.clone(), buffer_snapshot.clone(), Some(new_base_text), true, false, language, language_registry, cx, ) .await; let mut unreviewed_edits = Patch::default(); if let Ok(diff_snapshot) = diff_snapshot { unreviewed_edits = cx .background_spawn({ let diff_snapshot = diff_snapshot.clone(); let buffer_snapshot = buffer_snapshot.clone(); let new_diff_base = new_diff_base.clone(); async move { let mut unreviewed_edits = Patch::default(); for hunk in diff_snapshot .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer_snapshot) { let old_range = new_diff_base .offset_to_point(hunk.diff_base_byte_range.start) ..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end); let new_range = hunk.range.start..hunk.range.end; unreviewed_edits.push(point_to_row_edit( Edit { old: old_range, new: new_range, }, &new_diff_base, &buffer_snapshot.as_rope(), )); } unreviewed_edits } }) .await; diff.update(cx, |diff, cx| { diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx); })?; } this.update(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get_mut(buffer) .context("buffer not tracked")?; tracked_buffer.diff_base = new_diff_base; tracked_buffer.snapshot = buffer_snapshot; tracked_buffer.unreviewed_edits = unreviewed_edits; cx.notify(); anyhow::Ok(()) })? } /// Track a buffer as read, so we can notify the model about user edits. pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { self.track_buffer_internal(buffer, false, cx); } /// Mark a buffer as edited, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { self.edited_since_project_diagnostics_check = true; self.track_buffer_internal(buffer.clone(), true, cx); } /// Mark a buffer as edited, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { self.edited_since_project_diagnostics_check = true; let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; } 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_internal(buffer.clone(), false, cx); 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(); } pub fn keep_edits_in_range( &mut self, buffer: Entity<Buffer>,Lines 1–500 of 2166. Load more
use anyhow::{Context as _, Result}; use buffer_diff::BufferDiff; use collections::BTreeMap; use futures::{FutureExt, StreamExt, channel::mpsc}; use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity}; use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; 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 { /// Buffers that we want to notify the model about when they change. tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>, /// Has the model edited a file since it last checked diagnostics? edited_since_project_diagnostics_check: bool, /// The project this action log is associated with project: Entity<Project>, } impl ActionLog { /// Creates a new, empty action log associated with the given project. pub fn new(project: Entity<Project>) -> Self { Self { tracked_buffers: BTreeMap::default(), edited_since_project_diagnostics_check: false, project, } } pub fn project(&self) -> &Entity<Project> { &self.project } /// Notifies a diagnostics check pub fn checked_project_diagnostics(&mut self) { self.edited_since_project_diagnostics_check = false; } /// Returns true if any files have been edited since the last project diagnostics check pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool { self.edited_since_project_diagnostics_check } fn track_buffer_internal( &mut self, buffer: Entity<Buffer>, is_created: bool, cx: &mut Context<Self>, ) -> &mut TrackedBuffer { let status = if is_created { if let Some(tracked) = self.tracked_buffers.remove(&buffer) { match tracked.status { TrackedBufferStatus::Created { existing_file_content, } => TrackedBufferStatus::Created { existing_file_content, }, TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => { TrackedBufferStatus::Created { existing_file_content: Some(tracked.diff_base), } } } } else if buffer .read(cx) .file() .map_or(false, |file| file.disk_state().exists()) { TrackedBufferStatus::Created { existing_file_content: Some(buffer.read(cx).as_rope().clone()), } } else { TrackedBufferStatus::Created { existing_file_content: None, } } } else { TrackedBufferStatus::Modified }; let tracked_buffer = self .tracked_buffers .entry(buffer.clone()) .or_insert_with(|| { let open_lsp_handle = self.project.update(cx, |project, cx| { project.register_buffer_with_language_servers(&buffer, cx) }); let text_snapshot = buffer.read(cx).text_snapshot(); let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); let diff_base; let unreviewed_edits; if is_created { diff_base = Rope::default(); unreviewed_edits = Patch::new(vec![Edit { old: 0..1, new: 0..text_snapshot.max_point().row + 1, }]) } else { diff_base = buffer.read(cx).as_rope().clone(); unreviewed_edits = Patch::default(); } TrackedBuffer { buffer: buffer.clone(), diff_base, unreviewed_edits: unreviewed_edits, snapshot: text_snapshot.clone(), status, version: buffer.read(cx).version(), diff, diff_update: diff_update_tx, _open_lsp_handle: open_lsp_handle, _maintain_diff: cx.spawn({ let buffer = buffer.clone(); async move |this, cx| { Self::maintain_diff(this, buffer, diff_update_rx, cx) .await .ok(); } }), _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), } }); tracked_buffer.version = buffer.read(cx).version(); tracked_buffer } fn handle_buffer_event( &mut self, buffer: Entity<Buffer>, event: &BufferEvent, cx: &mut Context<Self>, ) { match event { BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), BufferEvent::FileHandleChanged => { self.handle_buffer_file_changed(buffer, cx); } _ => {} }; } 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; }; tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); } fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { return; }; match tracked_buffer.status { TrackedBufferStatus::Created { .. } | TrackedBufferStatus::Modified => { if buffer .read(cx) .file() .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. self.tracked_buffers.remove(&buffer); } cx.notify(); } TrackedBufferStatus::Deleted => { if buffer .read(cx) .file() .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 edits we // were tracking and reset the buffer's state. self.tracked_buffers.remove(&buffer); self.track_buffer_internal(buffer, false, cx); } cx.notify(); } } } async fn maintain_diff( this: WeakEntity<Self>, buffer: Entity<Buffer>, mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>, cx: &mut AsyncApp, ) -> Result<()> { let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?; let git_diff = this .update(cx, |this, cx| { this.project.update(cx, |project, cx| { project.open_uncommitted_diff(buffer.clone(), cx) }) })? .await .ok(); let buffer_repo = git_store.read_with(cx, |git_store, cx| { git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) })?; let (git_diff_updates_tx, mut git_diff_updates_rx) = async_watch::channel(()); let _repo_subscription = if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) { cx.update(|cx| { let mut old_head = buffer_repo.read(cx).head_commit.clone(); Some(cx.subscribe(git_diff, move |_, event, cx| match event { buffer_diff::BufferDiffEvent::DiffChanged { .. } => { let new_head = buffer_repo.read(cx).head_commit.clone(); if new_head != old_head { old_head = new_head; git_diff_updates_tx.send(()).ok(); } } _ => {} })) })? } else { None }; loop { futures::select_biased! { buffer_update = buffer_updates.next() => { if let Some((author, buffer_snapshot)) = buffer_update { Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?; } else { break; } } _ = git_diff_updates_rx.changed().fuse() => { if let Some(git_diff) = git_diff.as_ref() { Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?; } } } } Ok(()) } async fn track_edits( this: &WeakEntity<ActionLog>, buffer: &Entity<Buffer>, author: ChangeAuthor, buffer_snapshot: text::BufferSnapshot, cx: &mut AsyncApp, ) -> Result<()> { let rebase = 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.diff_base.clone(); let old_snapshot = tracked_buffer.snapshot.clone(); let new_snapshot = buffer_snapshot.clone(); let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); async move { let edits = diff_snapshots(&old_snapshot, &new_snapshot); if let ChangeAuthor::User = author { apply_non_conflicting_edits( &unreviewed_edits, edits, &mut base_text, new_snapshot.as_rope(), ); } (Arc::new(base_text.to_string()), base_text) } }); anyhow::Ok(rebase) })??; let (new_base_text, new_diff_base) = rebase.await; Self::update_diff( this, buffer, buffer_snapshot, new_base_text, new_diff_base, cx, ) .await } async fn keep_committed_edits( this: &WeakEntity<ActionLog>, buffer: &Entity<Buffer>, git_diff: &Entity<BufferDiff>, cx: &mut AsyncApp, ) -> Result<()> { let buffer_snapshot = this.read_with(cx, |this, _cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; anyhow::Ok(tracked_buffer.snapshot.clone()) })??; let (new_base_text, new_diff_base) = this .read_with(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); let agent_diff_base = tracked_buffer.diff_base.clone(); let git_diff_base = git_diff.read(cx).base_text().as_rope().clone(); let buffer_text = tracked_buffer.snapshot.as_rope().clone(); anyhow::Ok(cx.background_spawn(async move { let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable(); let committed_edits = language::line_diff( &agent_diff_base.to_string(), &git_diff_base.to_string(), ) .into_iter() .map(|(old, new)| Edit { old, new }); let mut new_agent_diff_base = agent_diff_base.clone(); let mut row_delta = 0i32; for committed in committed_edits { while let Some(unreviewed) = old_unreviewed_edits.peek() { // If the committed edit matches the unreviewed // edit, assume the user wants to keep it. if committed.old == unreviewed.old { let unreviewed_new = buffer_text.slice_rows(unreviewed.new.clone()).to_string(); let committed_new = git_diff_base.slice_rows(committed.new.clone()).to_string(); if unreviewed_new == committed_new { let old_byte_start = new_agent_diff_base.point_to_offset(Point::new( (unreviewed.old.start as i32 + row_delta) as u32, 0, )); let old_byte_end = new_agent_diff_base.point_to_offset(cmp::min( Point::new( (unreviewed.old.end as i32 + row_delta) as u32, 0, ), new_agent_diff_base.max_point(), )); new_agent_diff_base .replace(old_byte_start..old_byte_end, &unreviewed_new); row_delta += unreviewed.new_len() as i32 - unreviewed.old_len() as i32; } } else if unreviewed.old.start >= committed.old.end { break; } old_unreviewed_edits.next().unwrap(); } } ( Arc::new(new_agent_diff_base.to_string()), new_agent_diff_base, ) })) })?? .await; Self::update_diff( this, buffer, buffer_snapshot, new_base_text, new_diff_base, cx, ) .await } async fn update_diff( this: &WeakEntity<ActionLog>, buffer: &Entity<Buffer>, buffer_snapshot: text::BufferSnapshot, new_base_text: Arc<String>, new_diff_base: Rope, cx: &mut AsyncApp, ) -> Result<()> { let (diff, language, language_registry) = this.read_with(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get(buffer) .context("buffer not tracked")?; anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), buffer.read(cx).language_registry().clone(), )) })??; let diff_snapshot = BufferDiff::update_diff( diff.clone(), buffer_snapshot.clone(), Some(new_base_text), true, false, language, language_registry, cx, ) .await; let mut unreviewed_edits = Patch::default(); if let Ok(diff_snapshot) = diff_snapshot { unreviewed_edits = cx .background_spawn({ let diff_snapshot = diff_snapshot.clone(); let buffer_snapshot = buffer_snapshot.clone(); let new_diff_base = new_diff_base.clone(); async move { let mut unreviewed_edits = Patch::default(); for hunk in diff_snapshot .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer_snapshot) { let old_range = new_diff_base .offset_to_point(hunk.diff_base_byte_range.start) ..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end); let new_range = hunk.range.start..hunk.range.end; unreviewed_edits.push(point_to_row_edit( Edit { old: old_range, new: new_range, }, &new_diff_base, &buffer_snapshot.as_rope(), )); } unreviewed_edits } }) .await; diff.update(cx, |diff, cx| { diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx); })?; } this.update(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get_mut(buffer) .context("buffer not tracked")?; tracked_buffer.diff_base = new_diff_base; tracked_buffer.snapshot = buffer_snapshot; tracked_buffer.unreviewed_edits = unreviewed_edits; cx.notify(); anyhow::Ok(()) })? } /// Track a buffer as read, so we can notify the model about user edits. pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { self.track_buffer_internal(buffer, false, cx); } /// Mark a buffer as edited, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { self.edited_since_project_diagnostics_check = true; self.track_buffer_internal(buffer.clone(), true, cx); } /// Mark a buffer as edited, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { self.edited_since_project_diagnostics_check = true; let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; } 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_internal(buffer.clone(), false, cx); 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(); } pub fn keep_edits_in_range( &mut self, buffer: Entity<Buffer>,