Merge /active command into /tabs one (#16154)

Kirill Bulatov created

Now, tabs have arguments, `active` (default, applied also for no
arguments case) and `all` to insert the active tab only or all tabs.

Release Notes:

- N/A

Change summary

crates/assistant/src/assistant.rs                    |   7 
crates/assistant/src/context.rs                      |   7 
crates/assistant/src/slash_command.rs                |   1 
crates/assistant/src/slash_command/active_command.rs | 102 ------------
crates/assistant/src/slash_command/tabs_command.rs   | 113 +++++++++++--
5 files changed, 93 insertions(+), 137 deletions(-)

Detailed changes

crates/assistant/src/assistant.rs 🔗

@@ -35,9 +35,9 @@ use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
 use serde::{Deserialize, Serialize};
 use settings::{update_settings_file, Settings, SettingsStore};
 use slash_command::{
-    active_command, default_command, diagnostics_command, docs_command, fetch_command,
-    file_command, now_command, project_command, prompt_command, search_command, symbols_command,
-    tabs_command, terminal_command, workflow_command,
+    default_command, diagnostics_command, docs_command, fetch_command, file_command, now_command,
+    project_command, prompt_command, search_command, symbols_command, tabs_command,
+    terminal_command, workflow_command,
 };
 use std::sync::Arc;
 pub(crate) use streaming_diff::*;
@@ -282,7 +282,6 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
 fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut AppContext) {
     let slash_command_registry = SlashCommandRegistry::global(cx);
     slash_command_registry.register_command(file_command::FileSlashCommand, true);
-    slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
     slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
     slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
     slash_command_registry.register_command(project_command::ProjectSlashCommand, true);

crates/assistant/src/context.rs 🔗

@@ -2522,11 +2522,7 @@ pub struct SavedContextMetadata {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        assistant_panel, prompt_library,
-        slash_command::{active_command, file_command},
-        MessageId,
-    };
+    use crate::{assistant_panel, prompt_library, slash_command::file_command, MessageId};
     use assistant_slash_command::{ArgumentCompletion, SlashCommand};
     use fs::FakeFs;
     use gpui::{AppContext, TestAppContext, WeakView};
@@ -2883,7 +2879,6 @@ mod tests {
 
         let slash_command_registry = cx.update(SlashCommandRegistry::default_global);
         slash_command_registry.register_command(file_command::FileSlashCommand, false);
-        slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
 
         let registry = Arc::new(LanguageRegistry::test(cx.executor()));
         let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());

crates/assistant/src/slash_command.rs 🔗

@@ -17,7 +17,6 @@ use std::{
 use ui::ActiveTheme;
 use workspace::Workspace;
 
-pub mod active_command;
 pub mod default_command;
 pub mod diagnostics_command;
 pub mod docs_command;

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

@@ -1,102 +0,0 @@
-use super::{
-    diagnostics_command::write_single_file_diagnostics,
-    file_command::{build_entry_output_section, codeblock_fence_for_path},
-    SlashCommand, SlashCommandOutput,
-};
-use anyhow::{anyhow, Result};
-use assistant_slash_command::ArgumentCompletion;
-use editor::Editor;
-use gpui::{AppContext, Task, WeakView};
-use language::LspAdapterDelegate;
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-use ui::WindowContext;
-use workspace::Workspace;
-
-pub(crate) struct ActiveSlashCommand;
-
-impl SlashCommand for ActiveSlashCommand {
-    fn name(&self) -> String {
-        "active".into()
-    }
-
-    fn description(&self) -> String {
-        "insert active tab".into()
-    }
-
-    fn menu_text(&self) -> String {
-        "Insert Active Tab".into()
-    }
-
-    fn complete_argument(
-        self: Arc<Self>,
-        _query: String,
-        _cancel: Arc<AtomicBool>,
-        _workspace: Option<WeakView<Workspace>>,
-        _cx: &mut AppContext,
-    ) -> Task<Result<Vec<ArgumentCompletion>>> {
-        Task::ready(Err(anyhow!("this command does not require argument")))
-    }
-
-    fn requires_argument(&self) -> bool {
-        false
-    }
-
-    fn run(
-        self: Arc<Self>,
-        _argument: Option<&str>,
-        workspace: WeakView<Workspace>,
-        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
-        cx: &mut WindowContext,
-    ) -> Task<Result<SlashCommandOutput>> {
-        let output = workspace.update(cx, |workspace, cx| {
-            let Some(active_item) = workspace.active_item(cx) else {
-                return Task::ready(Err(anyhow!("no active tab")));
-            };
-            let Some(buffer) = active_item
-                .downcast::<Editor>()
-                .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
-            else {
-                return Task::ready(Err(anyhow!("active tab is not an editor")));
-            };
-
-            let snapshot = buffer.read(cx).snapshot();
-            let path = snapshot.resolve_file_path(cx, true);
-            let task = cx.background_executor().spawn({
-                let path = path.clone();
-                async move {
-                    let mut output = String::new();
-                    output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
-                    for chunk in snapshot.as_rope().chunks() {
-                        output.push_str(chunk);
-                    }
-                    if !output.ends_with('\n') {
-                        output.push('\n');
-                    }
-                    output.push_str("```\n");
-                    let has_diagnostics =
-                        write_single_file_diagnostics(&mut output, path.as_deref(), &snapshot);
-                    if output.ends_with('\n') {
-                        output.pop();
-                    }
-                    (output, has_diagnostics)
-                }
-            });
-            cx.foreground_executor().spawn(async move {
-                let (text, has_diagnostics) = task.await;
-                let range = 0..text.len();
-                Ok(SlashCommandOutput {
-                    text,
-                    sections: vec![build_entry_output_section(
-                        range,
-                        path.as_deref(),
-                        false,
-                        None,
-                    )],
-                    run_commands_in_text: has_diagnostics,
-                })
-            })
-        });
-        output.unwrap_or_else(|error| Task::ready(Err(error)))
-    }
-}

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

@@ -3,7 +3,7 @@ use super::{
     file_command::{build_entry_output_section, codeblock_fence_for_path},
     SlashCommand, SlashCommandOutput,
 };
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use assistant_slash_command::ArgumentCompletion;
 use collections::HashMap;
 use editor::Editor;
@@ -15,6 +15,44 @@ use workspace::Workspace;
 
 pub(crate) struct TabsSlashCommand;
 
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+enum TabsArgument {
+    #[default]
+    Active,
+    All,
+}
+
+impl TabsArgument {
+    fn for_query(mut query: String) -> Vec<Self> {
+        query.make_ascii_lowercase();
+        let query = query.trim();
+
+        let mut matches = Vec::new();
+        if Self::Active.name().contains(&query) {
+            matches.push(Self::Active);
+        }
+        if Self::All.name().contains(&query) {
+            matches.push(Self::All);
+        }
+        matches
+    }
+
+    fn name(&self) -> &'static str {
+        match self {
+            Self::Active => "active",
+            Self::All => "all",
+        }
+    }
+
+    fn from_name(name: &str) -> Option<Self> {
+        match name {
+            "active" => Some(Self::Active),
+            "all" => Some(Self::All),
+            _ => None,
+        }
+    }
+}
+
 impl SlashCommand for TabsSlashCommand {
     fn name(&self) -> String {
         "tabs".into()
@@ -29,52 +67,79 @@ impl SlashCommand for TabsSlashCommand {
     }
 
     fn requires_argument(&self) -> bool {
-        false
+        true
     }
 
     fn complete_argument(
         self: Arc<Self>,
-        _query: String,
+        query: String,
         _cancel: Arc<std::sync::atomic::AtomicBool>,
         _workspace: Option<WeakView<Workspace>>,
         _cx: &mut AppContext,
     ) -> Task<Result<Vec<ArgumentCompletion>>> {
-        Task::ready(Err(anyhow!("this command does not require argument")))
+        let arguments = TabsArgument::for_query(query);
+        Task::ready(Ok(arguments
+            .into_iter()
+            .map(|arg| ArgumentCompletion {
+                label: arg.name().to_owned(),
+                new_text: arg.name().to_owned(),
+                run_command: true,
+            })
+            .collect()))
     }
 
     fn run(
         self: Arc<Self>,
-        _argument: Option<&str>,
+        argument: Option<&str>,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
     ) -> Task<Result<SlashCommandOutput>> {
-        let open_buffers = workspace.update(cx, |workspace, cx| {
-            let mut timestamps_by_entity_id = HashMap::default();
-            let mut open_buffers = Vec::new();
-
-            for pane in workspace.panes() {
-                let pane = pane.read(cx);
-                for entry in pane.activation_history() {
-                    timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
-                }
+        let argument = argument
+            .and_then(TabsArgument::from_name)
+            .unwrap_or_default();
+        let open_buffers = workspace.update(cx, |workspace, cx| match argument {
+            TabsArgument::Active => {
+                let Some(active_item) = workspace.active_item(cx) else {
+                    anyhow::bail!("no active item")
+                };
+                let Some(buffer) = active_item
+                    .downcast::<Editor>()
+                    .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
+                else {
+                    anyhow::bail!("active item is not an editor")
+                };
+                let snapshot = buffer.read(cx).snapshot();
+                let full_path = snapshot.resolve_file_path(cx, true);
+                anyhow::Ok(vec![(full_path, snapshot, 0)])
             }
+            TabsArgument::All => {
+                let mut timestamps_by_entity_id = HashMap::default();
+                let mut open_buffers = Vec::new();
 
-            for editor in workspace.items_of_type::<Editor>(cx) {
-                if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
-                    if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
-                        let snapshot = buffer.read(cx).snapshot();
-                        let full_path = snapshot.resolve_file_path(cx, true);
-                        open_buffers.push((full_path, snapshot, *timestamp));
+                for pane in workspace.panes() {
+                    let pane = pane.read(cx);
+                    for entry in pane.activation_history() {
+                        timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
                     }
                 }
-            }
 
-            open_buffers
+                for editor in workspace.items_of_type::<Editor>(cx) {
+                    if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
+                        if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
+                            let snapshot = buffer.read(cx).snapshot();
+                            let full_path = snapshot.resolve_file_path(cx, true);
+                            open_buffers.push((full_path, snapshot, *timestamp));
+                        }
+                    }
+                }
+
+                Ok(open_buffers)
+            }
         });
 
         match open_buffers {
-            Ok(mut open_buffers) => cx.background_executor().spawn(async move {
+            Ok(Ok(mut open_buffers)) => cx.background_executor().spawn(async move {
                 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
 
                 let mut sections = Vec::new();
@@ -112,7 +177,7 @@ impl SlashCommand for TabsSlashCommand {
                     run_commands_in_text: has_diagnostics,
                 })
             }),
-            Err(error) => Task::ready(Err(error)),
+            Ok(Err(error)) | Err(error) => Task::ready(Err(error)),
         }
     }
 }