Allow specifying a custom limit for /search results (#12423)

Antonio Scandurra created

<img width="497" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/94e94326-fb3c-4f9b-b4d9-7dd6f6f8d537">


e.g.

```
/search --9 foobar
```

Release Notes:

- N/A

Change summary

crates/assistant/src/slash_command.rs                         | 81 ++--
crates/assistant/src/slash_command/search_command.rs          | 40 ++
crates/assistant_slash_command/src/assistant_slash_command.rs |  5 
crates/language/src/language.rs                               | 11 
crates/theme/src/styles/syntax.rs                             |  5 
5 files changed, 93 insertions(+), 49 deletions(-)

Detailed changes

crates/assistant/src/slash_command.rs 🔗

@@ -73,57 +73,58 @@ impl SlashCommandCompletionProvider {
         let command_name = command_name.to_string();
         let editor = self.editor.clone();
         let workspace = self.workspace.clone();
-        let executor = cx.background_executor().clone();
-        executor.clone().spawn(async move {
+        cx.spawn(|mut cx| async move {
             let matches = match_strings(
                 &candidates,
                 &command_name,
                 true,
                 usize::MAX,
                 &Default::default(),
-                executor,
+                cx.background_executor().clone(),
             )
             .await;
 
-            Ok(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(' ');
-                    }
+            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: CodeLabel::plain(mat.string.clone(), None),
-                        server_id: LanguageServerId(0),
-                        lsp_completion: Default::default(),
-                        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,
-                                            workspace.clone(),
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            }) as Arc<_>
-                        }),
+                        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(),
+                            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,
+                                                workspace.clone(),
+                                                cx,
+                                            );
+                                        })
+                                        .ok();
+                                }) as Arc<_>
+                            }),
+                        })
                     })
-                })
-                .collect())
+                    .collect()
+            })
         })
     }
 

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

@@ -2,7 +2,7 @@ use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
 use anyhow::Result;
 use assistant_slash_command::SlashCommandOutputSection;
 use gpui::{AppContext, Task, WeakView};
-use language::{LineEnding, LspAdapterDelegate};
+use language::{CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
 use semantic_index::SemanticIndex;
 use std::{
     fmt::Write,
@@ -20,6 +20,17 @@ impl SlashCommand for SearchSlashCommand {
         "search".into()
     }
 
+    fn label(&self, cx: &AppContext) -> CodeLabel {
+        let mut label = CodeLabel::default();
+        label.push_str("search ", None);
+        label.push_str(
+            "--n",
+            cx.theme().syntax().highlight_id("comment").map(HighlightId),
+        );
+        label.filter_range = 0.."search".len();
+        label
+    }
+
     fn description(&self) -> String {
         "semantically search files".into()
     }
@@ -54,12 +65,27 @@ impl SlashCommand for SearchSlashCommand {
         let Some(argument) = argument else {
             return Task::ready(Err(anyhow::anyhow!("missing search query")));
         };
-        if argument.is_empty() {
+
+        let mut limit = None;
+        let mut query = String::new();
+        for part in argument.split(' ') {
+            if let Some(parameter) = part.strip_prefix("--") {
+                if let Ok(count) = parameter.parse::<usize>() {
+                    limit = Some(count);
+                    continue;
+                }
+            }
+
+            query.push_str(part);
+            query.push(' ');
+        }
+        query.pop();
+
+        if query.is_empty() {
             return Task::ready(Err(anyhow::anyhow!("missing search query")));
         }
 
         let project = workspace.read(cx).project().clone();
-        let argument = argument.to_string();
         let fs = project.read(cx).fs().clone();
         let project_index =
             cx.update_global(|index: &mut SemanticIndex, cx| index.project_index(project, cx));
@@ -67,7 +93,7 @@ impl SlashCommand for SearchSlashCommand {
         cx.spawn(|cx| async move {
             let results = project_index
                 .read_with(&cx, |project_index, cx| {
-                    project_index.search(argument.clone(), 5, cx)
+                    project_index.search(query.clone(), limit.unwrap_or(5), cx)
                 })?
                 .await?;
 
@@ -92,7 +118,7 @@ impl SlashCommand for SearchSlashCommand {
             let output = cx
                 .background_executor()
                 .spawn(async move {
-                    let mut text = format!("Search results for {argument}:\n");
+                    let mut text = format!("Search results for {query}:\n");
                     let mut sections = Vec::new();
                     for (result, full_path, file_content) in loaded_results {
                         let range_start = result.range.start.min(file_content.len());
@@ -140,7 +166,7 @@ impl SlashCommand for SearchSlashCommand {
                         });
                     }
 
-                    let argument = SharedString::from(argument);
+                    let query = SharedString::from(query);
                     sections.push(SlashCommandOutputSection {
                         range: 0..text.len(),
                         render_placeholder: Arc::new(move |id, unfold, _cx| {
@@ -148,7 +174,7 @@ impl SlashCommand for SearchSlashCommand {
                                 .style(ButtonStyle::Filled)
                                 .layer(ElevationIndex::ElevatedSurface)
                                 .child(Icon::new(IconName::MagnifyingGlass))
-                                .child(Label::new(argument.clone()))
+                                .child(Label::new(query.clone()))
                                 .on_click(move |_, cx| unfold(cx))
                                 .into_any_element()
                         }),

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -2,7 +2,7 @@ mod slash_command_registry;
 
 use anyhow::Result;
 use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
-use language::LspAdapterDelegate;
+use language::{CodeLabel, LspAdapterDelegate};
 pub use slash_command_registry::*;
 use std::{
     ops::Range,
@@ -16,6 +16,9 @@ pub fn init(cx: &mut AppContext) {
 
 pub trait SlashCommand: 'static + Send + Sync {
     fn name(&self) -> String;
+    fn label(&self, _cx: &AppContext) -> CodeLabel {
+        CodeLabel::plain(self.name(), None)
+    }
     fn description(&self) -> String;
     fn tooltip_text(&self) -> String;
     fn complete_argument(

crates/language/src/language.rs 🔗

@@ -535,7 +535,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
     binary
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
 pub struct CodeLabel {
     /// The text to display.
     pub text: String,
@@ -1540,6 +1540,15 @@ impl CodeLabel {
         }
         result
     }
+
+    pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
+        let start_ix = self.text.len();
+        self.text.push_str(text);
+        let end_ix = self.text.len();
+        if let Some(highlight) = highlight {
+            self.runs.push((start_ix..end_ix, highlight));
+        }
+    }
 }
 
 impl Ord for LanguageMatcher {

crates/theme/src/styles/syntax.rs 🔗

@@ -44,6 +44,11 @@ impl SyntaxTheme {
         self.get(name).color.unwrap_or_default()
     }
 
+    pub fn highlight_id(&self, name: &str) -> Option<u32> {
+        let ix = self.highlights.iter().position(|entry| entry.0 == name)?;
+        Some(ix as u32)
+    }
+
     /// Returns a new [`Arc<SyntaxTheme>`] with the given syntax styles merged in.
     pub fn merge(base: Arc<Self>, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc<Self> {
         if user_syntax_styles.is_empty() {