Fetch code actions on cursor movement instead of on-demand

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/editor.rs       | 93 ++++++++++++++++++++------------
crates/find/src/find.rs           |  2 
crates/gpui/src/app.rs            | 66 +++++++++++++++++------
crates/gpui/src/presenter.rs      | 18 ++++++
crates/lsp/src/lsp.rs             | 11 +--
crates/project/src/project.rs     |  2 
crates/project/src/worktree.rs    |  4 
crates/workspace/src/pane.rs      |  2 
crates/workspace/src/workspace.rs |  6 +-
9 files changed, 134 insertions(+), 70 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -430,6 +430,8 @@ pub struct Editor {
     context_menu: Option<ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     next_completion_id: CompletionId,
+    available_code_actions: Option<CodeActionsMenu>,
+    code_actions_task: Option<Task<()>>,
 }
 
 pub struct EditorSnapshot {
@@ -656,6 +658,7 @@ impl CompletionsMenu {
     }
 }
 
+#[derive(Clone)]
 struct CodeActionsMenu {
     actions: Arc<[CodeAction]>,
     buffer: ModelHandle<Buffer>,
@@ -861,6 +864,8 @@ impl Editor {
             context_menu: None,
             completion_tasks: Default::default(),
             next_completion_id: 0,
+            available_code_actions: Default::default(),
+            code_actions_task: Default::default(),
         };
         this.end_selection(cx);
         this
@@ -1924,7 +1929,7 @@ impl Editor {
 
                 menu.filter(query.as_deref(), cx.background()).await;
 
-                if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
+                if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| {
                         match this.context_menu.as_ref() {
                             None => {}
@@ -2057,32 +2062,23 @@ impl Editor {
     }
 
     fn show_code_actions(&mut self, _: &ShowCodeActions, cx: &mut ViewContext<Self>) {
-        let head = self.newest_anchor_selection().head();
-        let project = if let Some(project) = self.project.clone() {
-            project
-        } else {
-            return;
-        };
-
-        let (buffer, head) = self.buffer.read(cx).text_anchor_for_position(head, cx);
-        let actions = project.update(cx, |project, cx| project.code_actions(&buffer, head, cx));
+        let mut task = self.code_actions_task.take();
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Some(prev_task) = task {
+                prev_task.await;
+                task = this
+                    .upgrade(&cx)
+                    .and_then(|this| this.update(&mut cx, |this, _| this.code_actions_task.take()));
+            }
 
-        cx.spawn(|this, mut cx| async move {
-            let actions = actions.await?;
-            if !actions.is_empty() {
+            if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
                     if this.focused {
-                        this.show_context_menu(
-                            ContextMenu::CodeActions(CodeActionsMenu {
-                                buffer,
-                                actions: actions.into(),
-                                selected_item: 0,
-                                list: UniformListState::default(),
-                            }),
-                            cx,
-                        );
+                        if let Some(menu) = this.available_code_actions.clone() {
+                            this.show_context_menu(ContextMenu::CodeActions(menu), cx);
+                        }
                     }
-                });
+                })
             }
             Ok::<_, anyhow::Error>(())
         })
@@ -4357,15 +4353,14 @@ impl Editor {
             .selections
             .iter()
             .max_by_key(|s| s.id)
-            .map(|s| s.head());
+            .map(|s| s.head())
+            .unwrap();
 
-        if let Some(new_cursor_position) = new_cursor_position.as_ref() {
-            self.push_to_nav_history(
-                old_cursor_position,
-                Some(new_cursor_position.to_point(&buffer)),
-                cx,
-            );
-        }
+        self.push_to_nav_history(
+            old_cursor_position,
+            Some(new_cursor_position.to_point(&buffer)),
+            cx,
+        );
 
         let completion_menu = match self.context_menu.as_mut() {
             Some(ContextMenu::Completions(menu)) => Some(menu),
@@ -4375,8 +4370,8 @@ impl Editor {
             }
         };
 
-        if let Some((completion_menu, cursor_position)) = completion_menu.zip(new_cursor_position) {
-            let cursor_position = cursor_position.to_offset(&buffer);
+        if let Some(completion_menu) = completion_menu {
+            let cursor_position = new_cursor_position.to_offset(&buffer);
             let (word_range, kind) =
                 buffer.surrounding_word(completion_menu.initial_position.clone());
             if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position)
@@ -4390,6 +4385,34 @@ impl Editor {
             }
         }
 
+        if let Some(project) = self.project.as_ref() {
+            let (buffer, head) = self
+                .buffer
+                .read(cx)
+                .text_anchor_for_position(new_cursor_position, cx);
+            let actions = project.update(cx, |project, cx| project.code_actions(&buffer, head, cx));
+            self.code_actions_task = Some(cx.spawn_weak(|this, mut cx| async move {
+                let actions = actions.await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.available_code_actions = actions.log_err().and_then(|actions| {
+                            if actions.is_empty() {
+                                None
+                            } else {
+                                Some(CodeActionsMenu {
+                                    actions: actions.into(),
+                                    buffer,
+                                    selected_item: 0,
+                                    list: Default::default(),
+                                })
+                            }
+                        });
+                        cx.notify();
+                    })
+                }
+            }));
+        }
+
         self.pause_cursor_blinking(cx);
         cx.emit(Event::SelectionsChanged);
     }
@@ -4703,7 +4726,7 @@ impl Editor {
             let this = this.downgrade();
             async move {
                 Timer::after(CURSOR_BLINK_INTERVAL).await;
-                if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
+                if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
                 }
             }
@@ -4728,7 +4751,7 @@ impl Editor {
                 let this = this.downgrade();
                 async move {
                     Timer::after(CURSOR_BLINK_INTERVAL).await;
-                    if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
+                    if let Some(this) = this.upgrade(&cx) {
                         this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
                     }
                 }

crates/find/src/find.rs 🔗

@@ -464,7 +464,7 @@ impl FindBar {
                 self.pending_search = Some(cx.spawn(|this, mut cx| async move {
                     match ranges.await {
                         Ok(ranges) => {
-                            if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) {
+                            if let Some(editor) = editor.upgrade(&cx) {
                                 this.update(&mut cx, |this, cx| {
                                     this.highlighted_editors.insert(editor.downgrade());
                                     editor.update(cx, |editor, cx| {

crates/gpui/src/app.rs 🔗

@@ -80,8 +80,14 @@ pub trait UpdateModel {
 }
 
 pub trait UpgradeModelHandle {
-    fn upgrade_model_handle<T: Entity>(&self, handle: WeakModelHandle<T>)
-        -> Option<ModelHandle<T>>;
+    fn upgrade_model_handle<T: Entity>(
+        &self,
+        handle: &WeakModelHandle<T>,
+    ) -> Option<ModelHandle<T>>;
+}
+
+pub trait UpgradeViewHandle {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
 }
 
 pub trait ReadView {
@@ -558,12 +564,18 @@ impl UpdateModel for AsyncAppContext {
 impl UpgradeModelHandle for AsyncAppContext {
     fn upgrade_model_handle<T: Entity>(
         &self,
-        handle: WeakModelHandle<T>,
+        handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>> {
         self.0.borrow_mut().upgrade_model_handle(handle)
     }
 }
 
+impl UpgradeViewHandle for AsyncAppContext {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+        self.0.borrow_mut().upgrade_view_handle(handle)
+    }
+}
+
 impl ReadModelWith for AsyncAppContext {
     fn read_model_with<E: Entity, T>(
         &self,
@@ -1732,12 +1744,18 @@ impl UpdateModel for MutableAppContext {
 impl UpgradeModelHandle for MutableAppContext {
     fn upgrade_model_handle<T: Entity>(
         &self,
-        handle: WeakModelHandle<T>,
+        handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>> {
         self.cx.upgrade_model_handle(handle)
     }
 }
 
+impl UpgradeViewHandle for MutableAppContext {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+        self.cx.upgrade_view_handle(handle)
+    }
+}
+
 impl ReadView for MutableAppContext {
     fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
         if let Some(view) = self.cx.views.get(&(handle.window_id, handle.view_id)) {
@@ -1846,7 +1864,7 @@ impl ReadModel for AppContext {
 impl UpgradeModelHandle for AppContext {
     fn upgrade_model_handle<T: Entity>(
         &self,
-        handle: WeakModelHandle<T>,
+        handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>> {
         if self.models.contains_key(&handle.model_id) {
             Some(ModelHandle::new(handle.model_id, &self.ref_counts))
@@ -1856,6 +1874,20 @@ impl UpgradeModelHandle for AppContext {
     }
 }
 
+impl UpgradeViewHandle for AppContext {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+        if self.ref_counts.lock().is_entity_alive(handle.view_id) {
+            Some(ViewHandle::new(
+                handle.window_id,
+                handle.view_id,
+                &self.ref_counts,
+            ))
+        } else {
+            None
+        }
+    }
+}
+
 impl ReadView for AppContext {
     fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
         if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) {
@@ -2228,7 +2260,7 @@ impl<M> UpdateModel for ModelContext<'_, M> {
 impl<M> UpgradeModelHandle for ModelContext<'_, M> {
     fn upgrade_model_handle<T: Entity>(
         &self,
-        handle: WeakModelHandle<T>,
+        handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>> {
         self.cx.upgrade_model_handle(handle)
     }
@@ -2558,12 +2590,18 @@ impl<V> ReadModel for ViewContext<'_, V> {
 impl<V> UpgradeModelHandle for ViewContext<'_, V> {
     fn upgrade_model_handle<T: Entity>(
         &self,
-        handle: WeakModelHandle<T>,
+        handle: &WeakModelHandle<T>,
     ) -> Option<ModelHandle<T>> {
         self.cx.upgrade_model_handle(handle)
     }
 }
 
+impl<V> UpgradeViewHandle for ViewContext<'_, V> {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+        self.cx.upgrade_view_handle(handle)
+    }
+}
+
 impl<V: View> UpdateModel for ViewContext<'_, V> {
     fn update_model<T: Entity, O>(
         &mut self,
@@ -2861,7 +2899,7 @@ impl<T: Entity> WeakModelHandle<T> {
         self.model_id
     }
 
-    pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
+    pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
         cx.upgrade_model_handle(self)
     }
 }
@@ -3277,16 +3315,8 @@ impl<T: View> WeakViewHandle<T> {
         self.view_id
     }
 
-    pub fn upgrade(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
-        if cx.ref_counts.lock().is_entity_alive(self.view_id) {
-            Some(ViewHandle::new(
-                self.window_id,
-                self.view_id,
-                &cx.ref_counts,
-            ))
-        } else {
-            None
-        }
+    pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<ViewHandle<T>> {
+        cx.upgrade_view_handle(self)
     }
 }
 

crates/gpui/src/presenter.rs 🔗

@@ -7,7 +7,8 @@ use crate::{
     platform::Event,
     text_layout::TextLayoutCache,
     Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle,
-    ReadModel, ReadView, Scene, View, ViewHandle,
+    ReadModel, ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
+    WeakModelHandle, WeakViewHandle,
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
@@ -270,6 +271,21 @@ impl<'a> ReadModel for LayoutContext<'a> {
     }
 }
 
+impl<'a> UpgradeModelHandle for LayoutContext<'a> {
+    fn upgrade_model_handle<T: Entity>(
+        &self,
+        handle: &WeakModelHandle<T>,
+    ) -> Option<ModelHandle<T>> {
+        self.app.upgrade_model_handle(handle)
+    }
+}
+
+impl<'a> UpgradeViewHandle for LayoutContext<'a> {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+        self.app.upgrade_view_handle(handle)
+    }
+}
+
 pub struct PaintContext<'a> {
     rendered_views: &'a mut HashMap<usize, ElementBox>,
     pub scene: &'a mut Scene,

crates/lsp/src/lsp.rs 🔗

@@ -664,14 +664,9 @@ mod tests {
         }));
         let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
 
-        let server = cx.read(|cx| {
-            LanguageServer::new(
-                Path::new("rust-analyzer"),
-                root_dir.path(),
-                cx.background().clone(),
-            )
-            .unwrap()
-        });
+        let server =
+            LanguageServer::new(Path::new("rust-analyzer"), root_dir.path(), cx.background())
+                .unwrap();
         server.next_idle_notification().await;
 
         server

crates/project/src/project.rs 🔗

@@ -867,7 +867,7 @@ impl Project {
         // Process all the LSP events.
         cx.spawn_weak(|this, mut cx| async move {
             while let Ok(message) = diagnostics_rx.recv().await {
-                let this = cx.read(|cx| this.upgrade(cx))?;
+                let this = this.upgrade(&cx)?;
                 match message {
                     LspEvent::DiagnosticsStart => {
                         this.update(&mut cx, |this, cx| {

crates/project/src/worktree.rs 🔗

@@ -291,7 +291,7 @@ impl Worktree {
                     let this = worktree_handle.downgrade();
                     cx.spawn(|mut cx| async move {
                         while let Some(_) = snapshot_rx.recv().await {
-                            if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
+                            if let Some(this) = this.upgrade(&cx) {
                                 this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
                             } else {
                                 break;
@@ -516,7 +516,7 @@ impl LocalWorktree {
 
             cx.spawn_weak(|this, mut cx| async move {
                 while let Ok(scan_state) = scan_states_rx.recv().await {
-                    if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
+                    if let Some(handle) = this.upgrade(&cx) {
                         let to_send = handle.update(&mut cx, |this, cx| {
                             last_scan_state_tx.blocking_send(scan_state).ok();
                             this.poll_snapshot(cx);

crates/workspace/src/pane.rs 🔗

@@ -221,7 +221,7 @@ impl Pane {
             let task = workspace.load_path(project_path, cx);
             cx.spawn(|workspace, mut cx| async move {
                 let item = task.await;
-                if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) {
+                if let Some(pane) = pane.upgrade(&cx) {
                     if let Some(item) = item.log_err() {
                         workspace.update(&mut cx, |workspace, cx| {
                             pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode));

crates/workspace/src/workspace.rs 🔗

@@ -326,7 +326,7 @@ impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
     }
 
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
-        WeakModelHandle::<T>::upgrade(*self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
+        WeakModelHandle::<T>::upgrade(self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
     }
 }
 
@@ -591,7 +591,7 @@ impl Workspace {
 
             while stream.recv().await.is_some() {
                 cx.update(|cx| {
-                    if let Some(this) = this.upgrade(&cx) {
+                    if let Some(this) = this.upgrade(cx) {
                         this.update(cx, |_, cx| cx.notify());
                     }
                 })
@@ -774,7 +774,7 @@ impl Workspace {
             let item = load_task.await?;
             this.update(&mut cx, |this, cx| {
                 let pane = pane
-                    .upgrade(&cx)
+                    .upgrade(cx)
                     .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
                 Ok(this.open_item_in_pane(item, &pane, cx))
             })