Include last cursor position in recent file entries + handle multibuffers

Michael Sloan created

Change summary

crates/cloud_llm_client/src/cloud_llm_client.rs |   2 
crates/editor/src/editor.rs                     |   5 
crates/zeta/src/zeta.rs                         | 210 ++++++++++--------
3 files changed, 126 insertions(+), 91 deletions(-)

Detailed changes

crates/cloud_llm_client/src/cloud_llm_client.rs 🔗

@@ -187,6 +187,8 @@ pub struct Point {
 pub struct PredictEditsRecentFile {
     /// Path to a file within the repository.
     pub path: String,
+    /// Most recent cursor position with the file.
+    pub cursor_point: Point,
     /// Milliseconds between the editor for this file being active and the request time.
     pub active_to_now_ms: u32,
 }

crates/editor/src/editor.rs 🔗

@@ -2746,6 +2746,11 @@ impl Editor {
         self.buffer.read(cx).read(cx).file_at(point).cloned()
     }
 
+    pub fn cursor_buffer_point(&self, cx: &mut App) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
+        let cursor = self.selections.newest::<Point>(cx).head();
+        self.buffer.read(cx).point_to_buffer_point(cursor, cx)
+    }
+
     pub fn active_excerpt(
         &self,
         cx: &App,

crates/zeta/src/zeta.rs 🔗

@@ -10,6 +10,7 @@ use arrayvec::ArrayVec;
 pub(crate) use completion_diff_element::*;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use edit_prediction::DataCollectionState;
+use editor::Editor;
 pub use init::*;
 use license_detection::LicenseDetectionWatcher;
 use project::git_store::Repository;
@@ -34,7 +35,7 @@ use language::{
     Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint, text_diff,
 };
 use language_model::{LlmApiToken, RefreshLlmTokenListener};
-use project::{Project, ProjectEntryId, ProjectPath};
+use project::{Project, ProjectPath};
 use release_channel::AppVersion;
 use settings::WorktreeId;
 use std::str::FromStr;
@@ -51,7 +52,7 @@ use std::{
 };
 use telemetry_events::EditPredictionRating;
 use thiserror::Error;
-use util::ResultExt;
+use util::{ResultExt, maybe};
 use uuid::Uuid;
 use workspace::Workspace;
 use workspace::notifications::{ErrorMessagePrompt, NotificationId};
@@ -239,7 +240,12 @@ pub struct Zeta {
     update_required: bool,
     user_store: Entity<UserStore>,
     license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
-    recent_project_entries: VecDeque<(ProjectEntryId, Instant)>,
+    recent_editors: VecDeque<RecentEditor>,
+}
+
+struct RecentEditor {
+    editor: WeakEntity<Editor>,
+    last_active_at: Instant,
 }
 
 impl Zeta {
@@ -292,15 +298,12 @@ impl Zeta {
         let data_collection_choice = cx.new(|_| data_collection_choice);
 
         if let Some(workspace) = &workspace {
-            cx.subscribe(
-                &workspace.read(cx).project().clone(),
-                |this, _workspace, event, _cx| match event {
-                    project::Event::ActiveEntryChanged(Some(project_entry_id)) => {
-                        this.push_recent_project_entry(*project_entry_id)
-                    }
-                    _ => {}
-                },
-            )
+            cx.subscribe(workspace, |this, _workspace, event, cx| match event {
+                workspace::Event::ActiveItemChanged => {
+                    this.handle_active_workspace_item_changed(cx)
+                }
+                _ => {}
+            })
             .detach();
         }
 
@@ -331,7 +334,7 @@ impl Zeta {
             update_required: false,
             license_detection_watchers: HashMap::default(),
             user_store,
-            recent_project_entries: VecDeque::with_capacity(MAX_RECENT_PROJECT_ENTRIES_COUNT),
+            recent_editors: VecDeque::with_capacity(MAX_RECENT_PROJECT_ENTRIES_COUNT),
         }
     }
 
@@ -1145,7 +1148,7 @@ and then another
         buffer_snapshotted_at: &Instant,
         snapshot: &BufferSnapshot,
         project: Option<&Entity<Project>>,
-        cx: &Context<Self>,
+        cx: &mut Context<Self>,
     ) -> Option<PredictEditsGitInfo> {
         let project = project?.read(cx);
         let file = snapshot.file()?;
@@ -1160,56 +1163,67 @@ and then another
             git_store.repository_and_path_for_project_path(&project_path, cx)?;
         let repo_path_str = repo_path.to_str()?;
 
-        let repository = repository.read(cx);
-        let head_sha = repository.head_commit.as_ref()?.sha.to_string();
-        let remote_origin_url = repository.remote_origin_url.clone();
-        let remote_upstream_url = repository.remote_upstream_url.clone();
-        let recent_files = self.recent_files(&buffer_snapshotted_at, repository, cx);
-
-        Some(PredictEditsGitInfo {
-            input_path: Some(repo_path_str.to_string()),
-            cursor_point: Some(cloud_llm_client::Point {
-                row: cursor_point.row,
-                column: cursor_point.column,
-            }),
-            cursor_offset: Some(cursor_offset),
-            head_sha: Some(head_sha),
-            remote_origin_url,
-            remote_upstream_url,
-            recent_files: Some(recent_files),
+        repository.update(cx, |repository, cx| {
+            let head_sha = repository.head_commit.as_ref()?.sha.to_string();
+            let remote_origin_url = repository.remote_origin_url.clone();
+            let remote_upstream_url = repository.remote_upstream_url.clone();
+            let recent_files = self.recent_files(&buffer_snapshotted_at, repository, cx);
+
+            Some(PredictEditsGitInfo {
+                input_path: Some(repo_path_str.to_string()),
+                cursor_point: Some(to_cloud_llm_client_point(cursor_point)),
+                head_sha: Some(head_sha),
+                remote_origin_url,
+                remote_upstream_url,
+                recent_files: Some(recent_files),
+            })
         })
     }
 
-    fn push_recent_project_entry(&mut self, project_entry_id: ProjectEntryId) {
-        let now = Instant::now();
-        if let Some(existing_ix) = self
-            .recent_project_entries
-            .iter()
-            .rposition(|(id, _)| *id == project_entry_id)
-        {
-            self.recent_project_entries.remove(existing_ix);
-        }
-        // filter out rapid changes in active item, particularly since this can happen rapidly when
-        // a workspace is loaded.
-        if let Some(most_recent) = self.recent_project_entries.back_mut()
-            && now.duration_since(most_recent.1) > MIN_TIME_BETWEEN_RECENT_PROJECT_ENTRIES
+    fn handle_active_workspace_item_changed(&mut self, cx: &Context<Self>) {
+        if let Some(active_editor) = self
+            .workspace
+            .read_with(cx, |workspace, cx| {
+                workspace
+                    .active_item(cx)
+                    .and_then(|item| item.act_as::<Editor>(cx))
+            })
+            .ok()
+            .flatten()
         {
-            most_recent.0 = project_entry_id;
-            most_recent.1 = now;
-            return;
-        }
-        if self.recent_project_entries.len() >= MAX_RECENT_PROJECT_ENTRIES_COUNT {
-            self.recent_project_entries.pop_front();
+            let now = Instant::now();
+            let new_recent = RecentEditor {
+                editor: active_editor.downgrade(),
+                last_active_at: now,
+            };
+            if let Some(existing_ix) = self
+                .recent_editors
+                .iter()
+                .rposition(|recent| &recent.editor == &new_recent.editor)
+            {
+                self.recent_editors.remove(existing_ix);
+            }
+            // filter out rapid changes in active item, particularly since this can happen rapidly when
+            // a workspace is loaded.
+            if let Some(previous_recent) = self.recent_editors.back_mut()
+                && now.duration_since(previous_recent.last_active_at)
+                    < MIN_TIME_BETWEEN_RECENT_PROJECT_ENTRIES
+            {
+                *previous_recent = new_recent;
+                return;
+            }
+            if self.recent_editors.len() >= MAX_RECENT_PROJECT_ENTRIES_COUNT {
+                self.recent_editors.pop_front();
+            }
+            self.recent_editors.push_back(new_recent);
         }
-        self.recent_project_entries
-            .push_back((project_entry_id, now));
     }
 
     fn recent_files(
         &mut self,
         now: &Instant,
         repository: &Repository,
-        cx: &Context<Self>,
+        cx: &mut App,
     ) -> Vec<PredictEditsRecentFile> {
         let Ok(project) = self
             .workspace
@@ -1217,50 +1231,64 @@ and then another
         else {
             return Vec::new();
         };
-        let mut results = Vec::new();
-        for ix in (0..self.recent_project_entries.len()).rev() {
-            let (entry_id, last_active_at) = &self.recent_project_entries[ix];
-            if let Some(worktree) = project.read(cx).worktree_for_entry(*entry_id, cx)
-                && let worktree = worktree.read(cx)
-                && let Some(entry) = worktree.entry_for_id(*entry_id)
-                && worktree_entry_eligible_for_collection(entry)
-            {
-                let project_path = ProjectPath {
-                    worktree_id: worktree.id(),
-                    path: entry.path.clone(),
-                };
-                let Some(repo_path) = repository.project_path_to_repo_path(&project_path, cx)
-                else {
-                    // entry not removed since queries involving other repositories might occur later
-                    continue;
-                };
-                let Some(repo_path_str) = repo_path.to_str() else {
-                    // paths may not be valid UTF-8
-                    self.recent_project_entries.remove(ix);
-                    continue;
-                };
-                if repo_path_str.len() > MAX_RECENT_FILE_PATH_LENGTH {
-                    self.recent_project_entries.remove(ix);
-                    continue;
-                }
-                let Ok(active_to_now_ms) =
-                    now.duration_since(*last_active_at).as_millis().try_into()
-                else {
-                    self.recent_project_entries.remove(ix);
-                    continue;
-                };
-                results.push(PredictEditsRecentFile {
-                    path: repo_path_str.to_string(),
-                    active_to_now_ms,
-                });
-            } else {
-                self.recent_project_entries.remove(ix);
+        let mut results = Vec::with_capacity(self.recent_editors.len());
+        for ix in (0..self.recent_editors.len()).rev() {
+            let recent_editor = &self.recent_editors[ix];
+            let keep_entry = recent_editor
+                .editor
+                .update(cx, |editor, cx| {
+                    maybe!({
+                        let (buffer, cursor_point, _) = editor.cursor_buffer_point(cx)?;
+                        let file = buffer.read(cx).file()?;
+                        let project_path = ProjectPath {
+                            worktree_id: file.worktree_id(cx),
+                            path: file.path().clone(),
+                        };
+                        let entry = project.read(cx).entry_for_path(&project_path, cx)?;
+                        if !worktree_entry_eligible_for_collection(entry) {
+                            return None;
+                        }
+                        let Some(repo_path) =
+                            repository.project_path_to_repo_path(&project_path, cx)
+                        else {
+                            // entry not removed since later queries may involve other repositories
+                            return Some(());
+                        };
+                        // paths may not be valid UTF-8
+                        let repo_path_str = repo_path.to_str()?;
+                        if repo_path_str.len() > MAX_RECENT_FILE_PATH_LENGTH {
+                            return None;
+                        }
+                        let active_to_now_ms = now
+                            .duration_since(recent_editor.last_active_at)
+                            .as_millis()
+                            .try_into()
+                            .ok()?;
+                        results.push(PredictEditsRecentFile {
+                            path: repo_path_str.to_string(),
+                            cursor_point: to_cloud_llm_client_point(cursor_point),
+                            active_to_now_ms,
+                        });
+                        Some(())
+                    })
+                })
+                .ok()
+                .flatten();
+            if keep_entry.is_none() {
+                self.recent_editors.remove(ix);
             }
         }
         results
     }
 }
 
+fn to_cloud_llm_client_point(point: language::Point) -> cloud_llm_client::Point {
+    cloud_llm_client::Point {
+        row: point.row,
+        column: point.column,
+    }
+}
+
 fn worktree_entry_eligible_for_collection(entry: &worktree::Entry) -> bool {
     entry.is_file()
         && entry.is_created()