Show recently-opened files when autocompleting /file without arguments (#12434)

Antonio Scandurra created

<img width="1588" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/ea63b046-64d6-419e-8135-4863748b58fa">


Release Notes:

- N/A

Change summary

crates/assistant/src/assistant_panel.rs                       |   8 
crates/assistant/src/slash_command.rs                         |   9 
crates/assistant/src/slash_command/active_command.rs          |   1 
crates/assistant/src/slash_command/file_command.rs            | 137 ++--
crates/assistant/src/slash_command/project_command.rs         |   1 
crates/assistant/src/slash_command/prompt_command.rs          |   1 
crates/assistant/src/slash_command/search_command.rs          |   1 
crates/assistant/src/slash_command/tabs_command.rs            |   1 
crates/assistant_slash_command/src/assistant_slash_command.rs |   1 
crates/collab_ui/src/chat_panel/message_editor.rs             |   1 
crates/editor/src/editor.rs                                   |   4 
crates/extension/src/extension_slash_command.rs               |   1 
crates/project/src/project.rs                                 |   4 
13 files changed, 108 insertions(+), 62 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -202,9 +202,7 @@ impl AssistantPanel {
 
                     let slash_command_registry = SlashCommandRegistry::global(cx);
 
-                    slash_command_registry.register_command(file_command::FileSlashCommand::new(
-                        workspace.project().clone(),
-                    ));
+                    slash_command_registry.register_command(file_command::FileSlashCommand);
                     slash_command_registry.register_command(
                         prompt_command::PromptSlashCommand::new(prompt_library.clone()),
                     );
@@ -4190,12 +4188,10 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
         let prompt_library = Arc::new(PromptLibrary::default());
         let slash_command_registry = SlashCommandRegistry::new();
 
-        slash_command_registry
-            .register_command(file_command::FileSlashCommand::new(project.clone()));
+        slash_command_registry.register_command(file_command::FileSlashCommand);
         slash_command_registry.register_command(prompt_command::PromptSlashCommand::new(
             prompt_library.clone(),
         ));

crates/assistant/src/slash_command.rs 🔗

@@ -102,6 +102,7 @@ impl SlashCommandCompletionProvider {
                             label: command.label(cx),
                             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();
@@ -142,7 +143,12 @@ impl SlashCommandCompletionProvider {
         *flag = new_cancel_flag.clone();
 
         if let Some(command) = self.commands.command(command_name) {
-            let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
+            let completions = command.complete_argument(
+                argument,
+                new_cancel_flag.clone(),
+                self.workspace.clone(),
+                cx,
+            );
             let command_name: Arc<str> = command_name.into();
             let editor = self.editor.clone();
             let workspace = self.workspace.clone();
@@ -157,6 +163,7 @@ impl SlashCommandCompletionProvider {
                         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();

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

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

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

@@ -1,10 +1,10 @@
 use super::{SlashCommand, SlashCommandOutput};
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use assistant_slash_command::SlashCommandOutputSection;
 use fuzzy::PathMatch;
-use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
+use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
 use language::{LineEnding, LspAdapterDelegate};
-use project::{PathMatchCandidateSet, Project};
+use project::PathMatchCandidateSet;
 use std::{
     ops::Range,
     path::{Path, PathBuf},
@@ -13,54 +13,70 @@ use std::{
 use ui::{prelude::*, ButtonLike, ElevationIndex};
 use workspace::Workspace;
 
-pub(crate) struct FileSlashCommand {
-    project: Model<Project>,
-}
+pub(crate) struct FileSlashCommand;
 
 impl FileSlashCommand {
-    pub fn new(project: Model<Project>) -> Self {
-        Self { project }
-    }
-
     fn search_paths(
         &self,
         query: String,
         cancellation_flag: Arc<AtomicBool>,
+        workspace: &View<Workspace>,
         cx: &mut AppContext,
     ) -> Task<Vec<PathMatch>> {
-        let worktrees = self
-            .project
-            .read(cx)
-            .visible_worktrees(cx)
-            .collect::<Vec<_>>();
-        let candidate_sets = worktrees
-            .into_iter()
-            .map(|worktree| {
-                let worktree = worktree.read(cx);
-                PathMatchCandidateSet {
-                    snapshot: worktree.snapshot(),
-                    include_ignored: worktree
-                        .root_entry()
-                        .map_or(false, |entry| entry.is_ignored),
-                    include_root_name: true,
-                    directories_only: false,
-                }
-            })
-            .collect::<Vec<_>>();
-
-        let executor = cx.background_executor().clone();
-        cx.foreground_executor().spawn(async move {
-            fuzzy::match_path_sets(
-                candidate_sets.as_slice(),
-                query.as_str(),
-                None,
-                false,
-                100,
-                &cancellation_flag,
-                executor,
+        if query.is_empty() {
+            let workspace = workspace.read(cx);
+            let project = workspace.project().read(cx);
+            let entries = workspace.recent_navigation_history(Some(10), cx);
+            let path_prefix: Arc<str> = "".into();
+            Task::ready(
+                entries
+                    .into_iter()
+                    .filter_map(|(entry, _)| {
+                        let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
+                        let mut full_path = PathBuf::from(worktree.read(cx).root_name());
+                        full_path.push(&entry.path);
+                        Some(PathMatch {
+                            score: 0.,
+                            positions: Vec::new(),
+                            worktree_id: entry.worktree_id.to_usize(),
+                            path: full_path.into(),
+                            path_prefix: path_prefix.clone(),
+                            distance_to_relative_ancestor: 0,
+                        })
+                    })
+                    .collect(),
             )
-            .await
-        })
+        } else {
+            let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
+            let candidate_sets = worktrees
+                .into_iter()
+                .map(|worktree| {
+                    let worktree = worktree.read(cx);
+                    PathMatchCandidateSet {
+                        snapshot: worktree.snapshot(),
+                        include_ignored: worktree
+                            .root_entry()
+                            .map_or(false, |entry| entry.is_ignored),
+                        include_root_name: true,
+                        directories_only: false,
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            let executor = cx.background_executor().clone();
+            cx.foreground_executor().spawn(async move {
+                fuzzy::match_path_sets(
+                    candidate_sets.as_slice(),
+                    query.as_str(),
+                    None,
+                    false,
+                    100,
+                    &cancellation_flag,
+                    executor,
+                )
+                .await
+            })
+        }
     }
 }
 
@@ -85,9 +101,14 @@ impl SlashCommand for FileSlashCommand {
         &self,
         query: String,
         cancellation_flag: Arc<AtomicBool>,
+        workspace: WeakView<Workspace>,
         cx: &mut AppContext,
-    ) -> gpui::Task<Result<Vec<String>>> {
-        let paths = self.search_paths(query, cancellation_flag, cx);
+    ) -> Task<Result<Vec<String>>> {
+        let Some(workspace) = workspace.upgrade() else {
+            return Task::ready(Err(anyhow!("workspace was dropped")));
+        };
+
+        let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
         cx.background_executor().spawn(async move {
             Ok(paths
                 .await
@@ -106,28 +127,34 @@ impl SlashCommand for FileSlashCommand {
     fn run(
         self: Arc<Self>,
         argument: Option<&str>,
-        _workspace: WeakView<Workspace>,
+        workspace: WeakView<Workspace>,
         _delegate: Arc<dyn LspAdapterDelegate>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let project = self.project.read(cx);
+        let Some(workspace) = workspace.upgrade() else {
+            return Task::ready(Err(anyhow!("workspace was dropped")));
+        };
+
         let Some(argument) = argument else {
-            return Task::ready(Err(anyhow::anyhow!("missing path")));
+            return Task::ready(Err(anyhow!("missing path")));
         };
 
         let path = PathBuf::from(argument);
-        let abs_path = project.worktrees().find_map(|worktree| {
-            let worktree = worktree.read(cx);
-            let worktree_root_path = Path::new(worktree.root_name());
-            let relative_path = path.strip_prefix(worktree_root_path).ok()?;
-            worktree.absolutize(&relative_path).ok()
-        });
+        let abs_path = workspace
+            .read(cx)
+            .visible_worktrees(cx)
+            .find_map(|worktree| {
+                let worktree = worktree.read(cx);
+                let worktree_root_path = Path::new(worktree.root_name());
+                let relative_path = path.strip_prefix(worktree_root_path).ok()?;
+                worktree.absolutize(&relative_path).ok()
+            });
 
         let Some(abs_path) = abs_path else {
-            return Task::ready(Err(anyhow::anyhow!("missing path")));
+            return Task::ready(Err(anyhow!("missing path")));
         };
 
-        let fs = project.fs().clone();
+        let fs = workspace.read(cx).app_state().fs.clone();
         let argument = argument.to_string();
         let text = cx.background_executor().spawn(async move {
             let mut content = fs.load(&abs_path).await?;

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

@@ -105,6 +105,7 @@ impl SlashCommand for ProjectSlashCommand {
         &self,
         _query: String,
         _cancel: Arc<AtomicBool>,
+        _workspace: 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 🔗

@@ -40,6 +40,7 @@ impl SlashCommand for PromptSlashCommand {
         &self,
         query: String,
         cancellation_flag: Arc<AtomicBool>,
+        _workspace: WeakView<Workspace>,
         cx: &mut AppContext,
     ) -> Task<Result<Vec<String>>> {
         let library = self.library.clone();

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

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

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

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

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -306,6 +306,7 @@ impl MessageEditor {
                     server_id: LanguageServerId(0), // TODO: Make this optional or something?
                     lsp_completion: Default::default(), // TODO: Make this optional or something?
                     confirm: None,
+                    show_new_completions_on_confirm: false,
                 }
             })
             .collect()

crates/editor/src/editor.rs 🔗

@@ -4004,6 +4004,10 @@ impl Editor {
             (confirm)(cx);
         }
 
+        if completion.show_new_completions_on_confirm {
+            self.show_completions(&ShowCompletions, cx);
+        }
+
         let provider = self.completion_provider.as_ref()?;
         let apply_edits = provider.apply_additional_edits_for_completion(
             buffer_handle,

crates/extension/src/extension_slash_command.rs 🔗

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

crates/project/src/project.rs 🔗

@@ -423,6 +423,8 @@ pub struct Completion {
     pub lsp_completion: lsp::CompletionItem,
     /// An optional callback to invoke when this completion is confirmed.
     pub confirm: Option<Arc<dyn Send + Sync + Fn(&mut WindowContext)>>,
+    /// If true, the editor will show a new completion menu after this completion is confirmed.
+    pub show_new_completions_on_confirm: bool,
 }
 
 impl std::fmt::Debug for Completion {
@@ -9252,6 +9254,7 @@ impl Project {
                         filter_range: Default::default(),
                     },
                     confirm: None,
+                    show_new_completions_on_confirm: false,
                 },
                 false,
                 cx,
@@ -10924,6 +10927,7 @@ async fn populate_labels_for_completions(
             documentation,
             lsp_completion,
             confirm: None,
+            show_new_completions_on_confirm: false,
         })
     }
 }