diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 97eba5993a..9aa2c257ce 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -3548,7 +3548,7 @@ mod tests { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Ok(vec![])) } diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index d01eea2f0d..c9afb8a474 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput}; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Result}; use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; -use gpui::{AppContext, Task, WeakView}; +use gpui::{Task, WeakView}; use language::LspAdapterDelegate; use std::{ fmt::Write, @@ -35,7 +35,7 @@ impl SlashCommand for DefaultSlashCommand { _query: String, _cancellation_flag: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index d8413e286e..f393f329b0 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -107,7 +107,7 @@ impl SlashCommand for DiagnosticsSlashCommand { query: String, cancellation_flag: Arc, workspace: Option>, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Task>> { let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else { return Task::ready(Err(anyhow!("workspace was dropped"))); diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index 8a528249bb..cb3cb663be 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -171,7 +171,7 @@ impl SlashCommand for DocsSlashCommand { query: String, _cancel: Arc, workspace: Option>, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Task>> { self.ensure_rust_doc_providers_are_registered(workspace, cx); diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index e3b1b92aba..9ff3382efd 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -8,7 +8,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, }; use futures::AsyncReadExt; -use gpui::{AppContext, Task, WeakView}; +use gpui::{Task, WeakView}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::LspAdapterDelegate; @@ -120,7 +120,7 @@ impl SlashCommand for FetchSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index b10a34e44e..4b5ae9bf35 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -104,7 +104,7 @@ impl SlashCommand for FileSlashCommand { query: String, cancellation_flag: Arc, workspace: Option>, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Task>> { let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else { return Task::ready(Err(anyhow!("workspace was dropped"))); diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index ccd3d713d8..3e6a053c97 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -6,7 +6,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, }; use chrono::Local; -use gpui::{AppContext, Task, WeakView}; +use gpui::{Task, WeakView}; use language::LspAdapterDelegate; use ui::prelude::*; use workspace::Workspace; @@ -35,7 +35,7 @@ impl SlashCommand for NowSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 581b216b57..8b31d29836 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -106,7 +106,7 @@ impl SlashCommand for ProjectSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 24b4802230..1b2769add8 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput}; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Context, Result}; use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; -use gpui::{AppContext, Task, WeakView}; +use gpui::{Task, WeakView}; use language::LspAdapterDelegate; use std::sync::{atomic::AtomicBool, Arc}; use ui::prelude::*; @@ -32,7 +32,7 @@ impl SlashCommand for PromptSlashCommand { query: String, _cancellation_flag: Arc, _workspace: Option>, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Task>> { let store = PromptStore::global(cx); cx.background_executor().spawn(async move { diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 5348cd96bb..a8c8513684 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -52,7 +52,7 @@ impl SlashCommand for SearchSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index 4788063aa1..6bfe933bb0 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput}; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; use editor::Editor; -use gpui::{AppContext, Task, WeakView}; +use gpui::{Task, WeakView}; use language::LspAdapterDelegate; use std::sync::Arc; use std::{path::Path, sync::atomic::AtomicBool}; @@ -29,7 +29,7 @@ impl SlashCommand for OutlineSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } diff --git a/crates/assistant/src/slash_command/tabs_command.rs b/crates/assistant/src/slash_command/tabs_command.rs index 52ab6e632f..94d097b85e 100644 --- a/crates/assistant/src/slash_command/tabs_command.rs +++ b/crates/assistant/src/slash_command/tabs_command.rs @@ -3,55 +3,23 @@ use super::{ file_command::{build_entry_output_section, codeblock_fence_for_path}, SlashCommand, SlashCommandOutput, }; -use anyhow::Result; +use anyhow::{Context, Result}; use assistant_slash_command::ArgumentCompletion; use collections::HashMap; use editor::Editor; -use gpui::{AppContext, Entity, Task, WeakView}; -use language::LspAdapterDelegate; -use std::{fmt::Write, sync::Arc}; +use gpui::{Entity, Task, WeakView}; +use language::{BufferSnapshot, LspAdapterDelegate}; +use std::{ + fmt::Write, + path::PathBuf, + sync::{atomic::AtomicBool, Arc}, +}; use ui::WindowContext; 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 { - 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 { - match name { - "active" => Some(Self::Active), - "all" => Some(Self::All), - _ => None, - } - } -} +const ALL_TABS_COMPLETION_ITEM: &str = "all"; impl SlashCommand for TabsSlashCommand { fn name(&self) -> String { @@ -59,33 +27,52 @@ impl SlashCommand for TabsSlashCommand { } fn description(&self) -> String { - "insert open tabs".into() + "insert open tabs (active tab by default)".to_owned() } fn menu_text(&self) -> String { - "Insert Open Tabs".into() + "Insert Open Tabs".to_owned() } fn requires_argument(&self) -> bool { - true + false } fn complete_argument( self: Arc, query: String, - _cancel: Arc, - _workspace: Option>, - _cx: &mut AppContext, + cancel: Arc, + workspace: Option>, + cx: &mut WindowContext, ) -> Task>> { - 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(), + let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) { + Some(ArgumentCompletion { + label: ALL_TABS_COMPLETION_ITEM.to_owned(), + new_text: ALL_TABS_COMPLETION_ITEM.to_owned(), run_command: true, }) - .collect())) + } else { + None + }; + let tab_items_search = tab_items_for_query(workspace, query, cancel, false, cx); + cx.spawn(|_| async move { + let tab_completion_items = + tab_items_search + .await? + .into_iter() + .filter_map(|(path, ..)| { + let path_string = path.as_deref()?.to_string_lossy().to_string(); + Some(ArgumentCompletion { + label: path_string.clone(), + new_text: path_string, + run_command: true, + }) + }); + Ok(all_tabs_completion_item + .into_iter() + .chain(tab_completion_items) + .collect::>()) + }) } fn run( @@ -95,89 +82,146 @@ impl SlashCommand for TabsSlashCommand { _delegate: Option>, cx: &mut WindowContext, ) -> Task> { - 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::() - .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)]) + let tab_items_search = tab_items_for_query( + Some(workspace), + argument.map(ToOwned::to_owned).unwrap_or_default(), + Arc::new(AtomicBool::new(false)), + true, + cx, + ); + + cx.background_executor().spawn(async move { + let mut sections = Vec::new(); + let mut text = String::new(); + let mut has_diagnostics = false; + for (full_path, buffer, _) in tab_items_search.await? { + let section_start_ix = text.len(); + text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None)); + for chunk in buffer.as_rope().chunks() { + text.push_str(chunk); + } + if !text.ends_with('\n') { + text.push('\n'); + } + writeln!(text, "```").unwrap(); + if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) { + has_diagnostics = true; + } + if !text.ends_with('\n') { + text.push('\n'); + } + + let section_end_ix = text.len() - 1; + sections.push(build_entry_output_section( + section_start_ix..section_end_ix, + full_path.as_deref(), + false, + None, + )); } - TabsArgument::All => { - 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); - } - } - - for editor in workspace.items_of_type::(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(Ok(mut open_buffers)) => cx.background_executor().spawn(async move { - open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp); - - let mut sections = Vec::new(); - let mut text = String::new(); - let mut has_diagnostics = false; - for (full_path, buffer, _) in open_buffers { - let section_start_ix = text.len(); - text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None)); - for chunk in buffer.as_rope().chunks() { - text.push_str(chunk); - } - if !text.ends_with('\n') { - text.push('\n'); - } - writeln!(text, "```").unwrap(); - if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) { - has_diagnostics = true; - } - if !text.ends_with('\n') { - text.push('\n'); - } - - let section_end_ix = text.len() - 1; - sections.push(build_entry_output_section( - section_start_ix..section_end_ix, - full_path.as_deref(), - false, - None, - )); - } - - Ok(SlashCommandOutput { - text, - sections, - run_commands_in_text: has_diagnostics, - }) - }), - Ok(Err(error)) | Err(error) => Task::ready(Err(error)), - } + Ok(SlashCommandOutput { + text, + sections, + run_commands_in_text: has_diagnostics, + }) + }) } } + +fn tab_items_for_query( + workspace: Option>, + mut query: String, + cancel: Arc, + use_active_tab_for_empty_query: bool, + cx: &mut WindowContext, +) -> Task, BufferSnapshot, usize)>>> { + cx.spawn(|mut cx| async move { + query.make_ascii_lowercase(); + let mut open_buffers = + workspace + .context("no workspace")? + .update(&mut cx, |workspace, cx| { + if use_active_tab_for_empty_query && query.trim().is_empty() { + let active_editor = workspace + .active_item(cx) + .context("no active item")? + .downcast::() + .context("active item is not an editor")?; + let snapshot = active_editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .context("active editor is not a singleton buffer")? + .read(cx) + .snapshot(); + let full_path = snapshot.resolve_file_path(cx, true); + return anyhow::Ok(vec![(full_path, snapshot, 0)]); + } + + 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); + } + } + + for editor in workspace.items_of_type::(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) + })??; + + let background_executor = cx.background_executor().clone(); + cx.background_executor() + .spawn(async move { + open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp); + let query = query.trim(); + if query.is_empty() || query == ALL_TABS_COMPLETION_ITEM { + return Ok(open_buffers); + } + + let match_candidates = open_buffers + .iter() + .enumerate() + .filter_map(|(id, (full_path, ..))| { + let path_string = full_path.as_deref()?.to_string_lossy().to_string(); + Some(fuzzy::StringMatchCandidate { + id, + char_bag: path_string.as_str().into(), + string: path_string, + }) + }) + .collect::>(); + let string_matches = fuzzy::match_strings( + &match_candidates, + &query, + true, + usize::MAX, + &cancel, + background_executor, + ) + .await; + + Ok(string_matches + .into_iter() + .filter_map(|string_match| open_buffers.get(string_match.candidate_id)) + .cloned() + .collect()) + }) + .await + }) +} diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 54534bf745..fe4610a14b 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -45,7 +45,7 @@ impl SlashCommand for TerminalSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Ok(vec![ArgumentCompletion { label: LINE_COUNT_ARG.to_string(), diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index d2708c38d2..97596c6aee 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/crates/assistant/src/slash_command/workflow_command.rs @@ -7,7 +7,7 @@ use anyhow::Result; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, }; -use gpui::{AppContext, Task, WeakView}; +use gpui::{Task, WeakView}; use language::LspAdapterDelegate; use ui::prelude::*; @@ -45,7 +45,7 @@ impl SlashCommand for WorkflowSlashCommand { _query: String, _cancel: Arc, _workspace: Option>, - _cx: &mut AppContext, + _cx: &mut WindowContext, ) -> Task>> { Task::ready(Ok(Vec::new())) } diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 72836dd4b6..5954ea2bb6 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -37,7 +37,7 @@ pub trait SlashCommand: 'static + Send + Sync { query: String, cancel: Arc, workspace: Option>, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Task>>; fn requires_argument(&self) -> bool; fn run( diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 69283a9eb9..ce16a25942 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -5,7 +5,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, }; use futures::FutureExt; -use gpui::{AppContext, Task, WeakView, WindowContext}; +use gpui::{Task, WeakView, WindowContext}; use language::LspAdapterDelegate; use ui::prelude::*; use wasmtime_wasi::WasiView; @@ -42,7 +42,7 @@ impl SlashCommand for ExtensionSlashCommand { query: String, _cancel: Arc, _workspace: Option>, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Task>> { cx.background_executor().spawn(async move { self.extension