2429 lines · 88.5 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use buffer_diff::BufferDiff;use clock;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, ResultExt as _,    paths::{PathStyle, RemotePathBuf},}; /// 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    }     pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {        Some(self.tracked_buffers.get(buffer)?.snapshot.clone())    }     /// Return a unified diff patch with user edits made since last read or notification    pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {        let diffs = self            .tracked_buffers            .values()            .filter_map(|tracked| {                if !tracked.may_have_unnotified_user_edits {                    return None;                }                 let text_with_latest_user_edits = tracked.diff_base.to_string();                let text_with_last_seen_user_edits = tracked.last_seen_base.to_string();                if text_with_latest_user_edits == text_with_last_seen_user_edits {                    return None;                }                let patch = language::unified_diff(                    &text_with_last_seen_user_edits,                    &text_with_latest_user_edits,                );                 let buffer = tracked.buffer.clone();                let file_path = buffer                    .read(cx)                    .file()                    .map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())                    .unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));                 let mut result = String::new();                result.push_str(&format!("--- a/{}\n", file_path));                result.push_str(&format!("+++ b/{}\n", file_path));                result.push_str(&patch);                 Some(result)            })            .collect::<Vec<_>>();         if diffs.is_empty() {            return None;        }         let unified_diff = diffs.join("\n\n");        Some(unified_diff)    }     /// Return a unified diff patch with user edits made since last read/notification    /// and mark them as notified    pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {        let patch = self.unnotified_user_edits(cx);        self.tracked_buffers.values_mut().for_each(|tracked| {            tracked.may_have_unnotified_user_edits = false;            tracked.last_seen_base = tracked.diff_base.clone();        });        patch    }     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 last_seen_base;                let unreviewed_edits;                if is_created {                    diff_base = Rope::default();                    last_seen_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();                    last_seen_base = diff_base.clone();                    unreviewed_edits = Patch::default();                }                TrackedBuffer {                    buffer: buffer.clone(),                    diff_base,                    last_seen_base,                    unreviewed_edits,                    snapshot: text_snapshot.clone(),                    status,                    version: buffer.read(cx).version(),                    diff,                    diff_update: diff_update_tx,                    may_have_unnotified_user_edits: false,                    _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 (mut git_diff_updates_tx, mut git_diff_updates_rx) = 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.update(cx, |this, cx| {            let tracked_buffer = this                .tracked_buffers                .get_mut(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();                let edits = diff_snapshots(&old_snapshot, &new_snapshot);                let mut has_user_changes = false;                async move {                    if let ChangeAuthor::User = author {                        has_user_changes = apply_non_conflicting_edits(                            &unreviewed_edits,                            edits,                            &mut base_text,                            new_snapshot.as_rope(),                        );                    }                     (Arc::new(base_text.to_string()), base_text, has_user_changes)                }            });             anyhow::Ok(rebase)        })??;        let (new_base_text, new_diff_base, has_user_changes) = rebase.await;         this.update(cx, |this, _| {            let tracked_buffer = this                .tracked_buffers                .get_mut(buffer)                .context("buffer not tracked")                .unwrap();            tracked_buffer.may_have_unnotified_user_edits |= has_user_changes;        })?;         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();

Lines 1–500 of 2429.