2166 lines · 80.1 KB · Rust
RawDownload
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.