diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index 4d10d383e745877cdc093425b9a2bd04f61dac54..6cb53b026cd6029a82dd1f7179f1e717b04f1fe1 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/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, } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 29e009fdf8d5a8c06d12e36253db59886dd0b9be..7d5670f4710fd4f2777f3d953d0972d9a93aded2 100644 --- a/crates/editor/src/editor.rs +++ b/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, Point, ExcerptId)> { + let cursor = self.selections.newest::(cx).head(); + self.buffer.read(cx).point_to_buffer_point(cursor, cx) + } + pub fn active_excerpt( &self, cx: &App, diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 4377df1ad5dae008fc0332b2fac8bd1a47222c44..c5eb2b4bab8cbfc97e6788ab38b5b56bed41f24b 100644 --- a/crates/zeta/src/zeta.rs +++ b/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, license_detection_watchers: HashMap>, - recent_project_entries: VecDeque<(ProjectEntryId, Instant)>, + recent_editors: VecDeque, +} + +struct RecentEditor { + editor: WeakEntity, + 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>, - cx: &Context, + cx: &mut Context, ) -> Option { 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) { + if let Some(active_editor) = self + .workspace + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .and_then(|item| item.act_as::(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, + cx: &mut App, ) -> Vec { 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()