Autocomplete commands that don't require access to workspace in prompt library (#12674)

Antonio Scandurra created

This is useful to autocomplete prompts when writing a new one in the
prompt library.

Release Notes:

- N/A

Change summary

crates/assistant/src/assistant_panel.rs                       |   4 
crates/assistant/src/prompt_library.rs                        | 139 ----
crates/assistant/src/slash_command.rs                         | 117 ++-
crates/assistant/src/slash_command/active_command.rs          |   2 
crates/assistant/src/slash_command/default_command.rs         |   2 
crates/assistant/src/slash_command/fetch_command.rs           |   2 
crates/assistant/src/slash_command/file_command.rs            |   4 
crates/assistant/src/slash_command/project_command.rs         |   2 
crates/assistant/src/slash_command/prompt_command.rs          |   2 
crates/assistant/src/slash_command/rustdoc_command.rs         |   2 
crates/assistant/src/slash_command/search_command.rs          |   2 
crates/assistant/src/slash_command/tabs_command.rs            |   2 
crates/assistant_slash_command/src/assistant_slash_command.rs |   2 
crates/extension/src/extension_slash_command.rs               |   2 
14 files changed, 88 insertions(+), 196 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -2580,9 +2580,9 @@ impl ConversationEditor {
         let slash_command_registry = conversation.read(cx).slash_command_registry.clone();
 
         let completion_provider = SlashCommandCompletionProvider::new(
-            cx.view().downgrade(),
             slash_command_registry.clone(),
-            workspace.downgrade(),
+            Some(cx.view().downgrade()),
+            Some(workspace.downgrade()),
         );
 
         let editor = cx.new_view(|cx| {

crates/assistant/src/prompt_library.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    slash_command::SlashCommandLine, CompletionProvider, LanguageModelRequest,
+    slash_command::SlashCommandCompletionProvider, CompletionProvider, LanguageModelRequest,
     LanguageModelRequestMessage, Role,
 };
 use anyhow::{anyhow, Result};
@@ -11,17 +11,14 @@ use futures::{
     future::{self, BoxFuture, Shared},
     FutureExt,
 };
-use fuzzy::{match_strings, StringMatchCandidate};
+use fuzzy::StringMatchCandidate;
 use gpui::{
     actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
-    EventEmitter, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
-    View, WindowBounds, WindowHandle, WindowOptions,
+    EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
+    WindowBounds, WindowHandle, WindowOptions,
 };
 use heed::{types::SerdeBincode, Database, RoTxn};
-use language::{
-    language_settings::SoftWrap, Buffer, Documentation, LanguageRegistry, LanguageServerId, Point,
-    ToPoint as _,
-};
+use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
 use parking_lot::RwLock;
 use picker::{Picker, PickerDelegate};
 use rope::Rope;
@@ -482,6 +479,7 @@ impl PromptLibrary {
             self.set_active_prompt(Some(prompt_id), cx);
         } else {
             let language_registry = self.language_registry.clone();
+            let commands = SlashCommandRegistry::global(cx);
             let prompt = self.store.load(prompt_id);
             self.pending_load = cx.spawn(|this, mut cx| async move {
                 let prompt = prompt.await;
@@ -500,8 +498,9 @@ impl PromptLibrary {
                             editor.set_show_gutter(false, cx);
                             editor.set_show_wrap_guides(false, cx);
                             editor.set_show_indent_guides(false, cx);
-                            editor
-                                .set_completion_provider(Box::new(SlashCommandCompletionProvider));
+                            editor.set_completion_provider(Box::new(
+                                SlashCommandCompletionProvider::new(commands, None, None),
+                            ));
                             if focus {
                                 editor.focus(cx);
                             }
@@ -1092,123 +1091,3 @@ fn title_from_body(body: impl IntoIterator<Item = char>) -> Option<SharedString>
         None
     }
 }
-
-struct SlashCommandCompletionProvider;
-
-impl editor::CompletionProvider for SlashCommandCompletionProvider {
-    fn completions(
-        &self,
-        buffer: &Model<Buffer>,
-        buffer_position: language::Anchor,
-        cx: &mut ViewContext<Editor>,
-    ) -> Task<Result<Vec<project::Completion>>> {
-        let Some((command_name, name_range)) = buffer.update(cx, |buffer, _cx| {
-            let position = buffer_position.to_point(buffer);
-            let line_start = Point::new(position.row, 0);
-            let mut lines = buffer.text_for_range(line_start..position).lines();
-            let line = lines.next()?;
-            let call = SlashCommandLine::parse(line)?;
-
-            if call.argument.is_some() {
-                // Don't autocomplete arguments.
-                None
-            } else {
-                let name = line[call.name.clone()].to_string();
-                let name_range_start = Point::new(position.row, call.name.start as u32);
-                let name_range_end = Point::new(position.row, call.name.end as u32);
-                let name_range =
-                    buffer.anchor_after(name_range_start)..buffer.anchor_after(name_range_end);
-                Some((name, name_range))
-            }
-        }) else {
-            return Task::ready(Ok(Vec::new()));
-        };
-
-        let commands = SlashCommandRegistry::global(cx);
-        let candidates = commands
-            .command_names()
-            .into_iter()
-            .enumerate()
-            .map(|(ix, def)| StringMatchCandidate {
-                id: ix,
-                string: def.to_string(),
-                char_bag: def.as_ref().into(),
-            })
-            .collect::<Vec<_>>();
-        let command_name = command_name.to_string();
-        cx.spawn(|_, mut cx| async move {
-            let matches = match_strings(
-                &candidates,
-                &command_name,
-                true,
-                usize::MAX,
-                &Default::default(),
-                cx.background_executor().clone(),
-            )
-            .await;
-            cx.update(|cx| {
-                matches
-                    .into_iter()
-                    .filter_map(|mat| {
-                        let command = commands.command(&mat.string)?;
-                        let mut new_text = mat.string.clone();
-                        let requires_argument = command.requires_argument();
-                        if requires_argument {
-                            new_text.push(' ');
-                        }
-
-                        Some(project::Completion {
-                            old_range: name_range.clone(),
-                            documentation: Some(Documentation::SingleLine(command.description())),
-                            new_text,
-                            label: command.label(cx),
-                            server_id: LanguageServerId(0),
-                            lsp_completion: Default::default(),
-                            show_new_completions_on_confirm: false,
-                            confirm: None,
-                        })
-                    })
-                    .collect()
-            })
-        })
-    }
-
-    fn resolve_completions(
-        &self,
-        _: Model<Buffer>,
-        _: Vec<usize>,
-        _: Arc<RwLock<Box<[project::Completion]>>>,
-        _: &mut ViewContext<Editor>,
-    ) -> Task<Result<bool>> {
-        Task::ready(Ok(true))
-    }
-
-    fn apply_additional_edits_for_completion(
-        &self,
-        _: Model<Buffer>,
-        _: project::Completion,
-        _: bool,
-        _: &mut ViewContext<Editor>,
-    ) -> Task<Result<Option<language::Transaction>>> {
-        Task::ready(Ok(None))
-    }
-
-    fn is_completion_trigger(
-        &self,
-        buffer: &Model<Buffer>,
-        position: language::Anchor,
-        _text: &str,
-        _trigger_in_words: bool,
-        cx: &mut ViewContext<Editor>,
-    ) -> bool {
-        let buffer = buffer.read(cx);
-        let position = position.to_point(buffer);
-        let line_start = Point::new(position.row, 0);
-        let mut lines = buffer.text_for_range(line_start..position).lines();
-        if let Some(line) = lines.next() {
-            SlashCommandLine::parse(line).is_some()
-        } else {
-            false
-        }
-    }
-}

crates/assistant/src/slash_command.rs 🔗

@@ -27,10 +27,10 @@ pub mod search_command;
 pub mod tabs_command;
 
 pub(crate) struct SlashCommandCompletionProvider {
-    editor: WeakView<ConversationEditor>,
     commands: Arc<SlashCommandRegistry>,
     cancel_flag: Mutex<Arc<AtomicBool>>,
-    workspace: WeakView<Workspace>,
+    editor: Option<WeakView<ConversationEditor>>,
+    workspace: Option<WeakView<Workspace>>,
 }
 
 pub(crate) struct SlashCommandLine {
@@ -42,9 +42,9 @@ pub(crate) struct SlashCommandLine {
 
 impl SlashCommandCompletionProvider {
     pub fn new(
-        editor: WeakView<ConversationEditor>,
         commands: Arc<SlashCommandRegistry>,
-        workspace: WeakView<Workspace>,
+        editor: Option<WeakView<ConversationEditor>>,
+        workspace: Option<WeakView<Workspace>>,
     ) -> Self {
         Self {
             cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
@@ -98,6 +98,30 @@ impl SlashCommandCompletionProvider {
                             new_text.push(' ');
                         }
 
+                        let confirm = editor.clone().zip(workspace.clone()).and_then(
+                            |(editor, workspace)| {
+                                (!requires_argument).then(|| {
+                                    let command_name = mat.string.clone();
+                                    let command_range = command_range.clone();
+                                    let editor = editor.clone();
+                                    let workspace = workspace.clone();
+                                    Arc::new(move |cx: &mut WindowContext| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.run_command(
+                                                    command_range.clone(),
+                                                    &command_name,
+                                                    None,
+                                                    true,
+                                                    workspace.clone(),
+                                                    cx,
+                                                );
+                                            })
+                                            .ok();
+                                    }) as Arc<_>
+                                })
+                            },
+                        );
                         Some(project::Completion {
                             old_range: name_range.clone(),
                             documentation: Some(Documentation::SingleLine(command.description())),
@@ -106,26 +130,7 @@ impl SlashCommandCompletionProvider {
                             server_id: LanguageServerId(0),
                             lsp_completion: Default::default(),
                             show_new_completions_on_confirm: requires_argument,
-                            confirm: (!requires_argument).then(|| {
-                                let command_name = mat.string.clone();
-                                let command_range = command_range.clone();
-                                let editor = editor.clone();
-                                let workspace = workspace.clone();
-                                Arc::new(move |cx: &mut WindowContext| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.run_command(
-                                                command_range.clone(),
-                                                &command_name,
-                                                None,
-                                                true,
-                                                workspace.clone(),
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }) as Arc<_>
-                            }),
+                            confirm,
                         })
                     })
                     .collect()
@@ -160,34 +165,42 @@ impl SlashCommandCompletionProvider {
                 Ok(completions
                     .await?
                     .into_iter()
-                    .map(|arg| project::Completion {
-                        old_range: argument_range.clone(),
-                        label: CodeLabel::plain(arg.clone(), None),
-                        new_text: arg.clone(),
-                        documentation: None,
-                        server_id: LanguageServerId(0),
-                        lsp_completion: Default::default(),
-                        show_new_completions_on_confirm: false,
-                        confirm: Some(Arc::new({
-                            let command_name = command_name.clone();
-                            let command_range = command_range.clone();
-                            let editor = editor.clone();
-                            let workspace = workspace.clone();
-                            move |cx| {
-                                editor
-                                    .update(cx, |editor, cx| {
-                                        editor.run_command(
-                                            command_range.clone(),
-                                            &command_name,
-                                            Some(&arg),
-                                            true,
-                                            workspace.clone(),
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            }
-                        })),
+                    .map(|command_argument| {
+                        let confirm =
+                            editor
+                                .clone()
+                                .zip(workspace.clone())
+                                .map(|(editor, workspace)| {
+                                    Arc::new({
+                                        let command_range = command_range.clone();
+                                        let command_name = command_name.clone();
+                                        let command_argument = command_argument.clone();
+                                        move |cx: &mut WindowContext| {
+                                            editor
+                                                .update(cx, |editor, cx| {
+                                                    editor.run_command(
+                                                        command_range.clone(),
+                                                        &command_name,
+                                                        Some(&command_argument),
+                                                        true,
+                                                        workspace.clone(),
+                                                        cx,
+                                                    );
+                                                })
+                                                .ok();
+                                        }
+                                    }) as Arc<_>
+                                });
+                        project::Completion {
+                            old_range: argument_range.clone(),
+                            label: CodeLabel::plain(command_argument.clone(), None),
+                            new_text: command_argument.clone(),
+                            documentation: None,
+                            server_id: LanguageServerId(0),
+                            lsp_completion: Default::default(),
+                            show_new_completions_on_confirm: false,
+                            confirm,
+                        }
                     })
                     .collect())
             })

crates/assistant/src/slash_command/active_command.rs 🔗

@@ -27,7 +27,7 @@ impl SlashCommand for ActiveSlashCommand {
         &self,
         _query: String,
         _cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))

crates/assistant/src/slash_command/default_command.rs 🔗

@@ -34,7 +34,7 @@ impl SlashCommand for DefaultSlashCommand {
         &self,
         _query: String,
         _cancellation_flag: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))

crates/assistant/src/slash_command/fetch_command.rs 🔗

@@ -62,7 +62,7 @@ impl SlashCommand for FetchSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Ok(Vec::new()))

crates/assistant/src/slash_command/file_command.rs 🔗

@@ -101,10 +101,10 @@ impl SlashCommand for FileSlashCommand {
         &self,
         query: String,
         cancellation_flag: Arc<AtomicBool>,
-        workspace: WeakView<Workspace>,
+        workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
-        let Some(workspace) = workspace.upgrade() else {
+        let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
             return Task::ready(Err(anyhow!("workspace was dropped")));
         };
 

crates/assistant/src/slash_command/project_command.rs 🔗

@@ -105,7 +105,7 @@ impl SlashCommand for ProjectSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))

crates/assistant/src/slash_command/prompt_command.rs 🔗

@@ -31,7 +31,7 @@ impl SlashCommand for PromptSlashCommand {
         &self,
         query: String,
         _cancellation_flag: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         let store = PromptStore::global(cx);

crates/assistant/src/slash_command/rustdoc_command.rs 🔗

@@ -119,7 +119,7 @@ impl SlashCommand for RustdocSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Ok(Vec::new()))

crates/assistant/src/slash_command/search_command.rs 🔗

@@ -47,7 +47,7 @@ impl SlashCommand for SearchSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Ok(Vec::new()))

crates/assistant/src/slash_command/tabs_command.rs 🔗

@@ -32,7 +32,7 @@ impl SlashCommand for TabsSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<std::sync::atomic::AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Err(anyhow!("this command does not require argument")))

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -25,7 +25,7 @@ pub trait SlashCommand: 'static + Send + Sync {
         &self,
         query: String,
         cancel: Arc<AtomicBool>,
-        workspace: WeakView<Workspace>,
+        workspace: Option<WeakView<Workspace>>,
         cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>>;
     fn requires_argument(&self) -> bool;

crates/extension/src/extension_slash_command.rs 🔗

@@ -39,7 +39,7 @@ impl SlashCommand for ExtensionSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<AtomicBool>,
-        _workspace: WeakView<Workspace>,
+        _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         Task::ready(Ok(Vec::new()))