316 lines · 11.6 KB · Rust
RawDownload
use super::{file_command::append_buffer_to_output, SlashCommand, SlashCommandOutput};use anyhow::{Context, Result};use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};use collections::{HashMap, HashSet};use editor::Editor;use futures::future::join_all;use gpui::{Entity, Task, WeakView};use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};use std::{    path::PathBuf,    sync::{atomic::AtomicBool, Arc},};use ui::{ActiveTheme, WindowContext};use util::ResultExt;use workspace::Workspace; pub(crate) struct TabSlashCommand; const ALL_TABS_COMPLETION_ITEM: &str = "all"; impl SlashCommand for TabSlashCommand {    fn name(&self) -> String {        "tab".into()    }     fn description(&self) -> String {        "Insert open tabs (active tab by default)".to_owned()    }     fn menu_text(&self) -> String {        self.description()    }     fn requires_argument(&self) -> bool {        false    }     fn accepts_arguments(&self) -> bool {        true    }     fn complete_argument(        self: Arc<Self>,        arguments: &[String],        cancel: Arc<AtomicBool>,        workspace: Option<WeakView<Workspace>>,        cx: &mut WindowContext,    ) -> Task<Result<Vec<ArgumentCompletion>>> {        let mut has_all_tabs_completion_item = false;        let argument_set = arguments            .iter()            .filter(|argument| {                if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {                    has_all_tabs_completion_item = true;                    false                } else {                    true                }            })            .cloned()            .collect::<HashSet<_>>();        if has_all_tabs_completion_item {            return Task::ready(Ok(Vec::new()));        }         let active_item_path = workspace.as_ref().and_then(|workspace| {            workspace                .update(cx, |workspace, cx| {                    let snapshot = active_item_buffer(workspace, cx).ok()?;                    snapshot.resolve_file_path(cx, true)                })                .ok()                .flatten()        });        let current_query = arguments.last().cloned().unwrap_or_default();        let tab_items_search =            tab_items_for_queries(workspace, &[current_query], cancel, false, cx);         let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);        cx.spawn(|_| async move {            let tab_items = tab_items_search.await?;            let run_command = tab_items.len() == 1;            let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {                let path_string = path.as_deref()?.to_string_lossy().to_string();                if argument_set.contains(&path_string) {                    return None;                }                if active_item_path.is_some() && active_item_path == path {                    return None;                }                let label = create_tab_completion_label(path.as_ref()?, comment_id);                Some(ArgumentCompletion {                    label,                    new_text: path_string,                    replace_previous_arguments: false,                    after_completion: run_command.into(),                })            });             let active_item_completion = active_item_path                .as_deref()                .map(|active_item_path| {                    let path_string = active_item_path.to_string_lossy().to_string();                    let label = create_tab_completion_label(active_item_path, comment_id);                    ArgumentCompletion {                        label,                        new_text: path_string,                        replace_previous_arguments: false,                        after_completion: run_command.into(),                    }                })                .filter(|completion| !argument_set.contains(&completion.new_text));             Ok(active_item_completion                .into_iter()                .chain(Some(ArgumentCompletion {                    label: ALL_TABS_COMPLETION_ITEM.into(),                    new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),                    replace_previous_arguments: false,                    after_completion: true.into(),                }))                .chain(tab_completion_items)                .collect())        })    }     fn run(        self: Arc<Self>,        arguments: &[String],        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],        _context_buffer: BufferSnapshot,        workspace: WeakView<Workspace>,        _delegate: Option<Arc<dyn LspAdapterDelegate>>,        cx: &mut WindowContext,    ) -> Task<Result<SlashCommandOutput>> {        let tab_items_search = tab_items_for_queries(            Some(workspace),            arguments,            Arc::new(AtomicBool::new(false)),            true,            cx,        );         cx.background_executor().spawn(async move {            let mut output = SlashCommandOutput::default();            for (full_path, buffer, _) in tab_items_search.await? {                append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();            }            Ok(output)        })    }} fn tab_items_for_queries(    workspace: Option<WeakView<Workspace>>,    queries: &[String],    cancel: Arc<AtomicBool>,    strict_match: bool,    cx: &mut WindowContext,) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {    let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());    let queries = queries.to_owned();    cx.spawn(|mut cx| async move {        let mut open_buffers =            workspace                .context("no workspace")?                .update(&mut cx, |workspace, cx| {                    if strict_match && empty_query {                        let snapshot = active_item_buffer(workspace, cx)?;                        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 visited_buffers = HashSet::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::<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())                            {                                if visited_buffers.insert(buffer.read(cx).remote_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);                if empty_query                    || queries                        .iter()                        .any(|query| query == ALL_TABS_COMPLETION_ITEM)                {                    return Ok(open_buffers);                }                 let matched_items = if strict_match {                    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((id, path_string))                        })                        .fold(HashMap::default(), |mut candidates, (id, path_string)| {                            candidates                                .entry(path_string)                                .or_insert_with(Vec::new)                                .push(id);                            candidates                        });                     queries                        .iter()                        .filter_map(|query| match_candidates.get(query))                        .flatten()                        .copied()                        .filter_map(|id| open_buffers.get(id))                        .cloned()                        .collect()                } else {                    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::<Vec<_>>();                    let mut processed_matches = HashSet::default();                    let file_queries = queries.iter().map(|query| {                        fuzzy::match_strings(                            &match_candidates,                            query,                            true,                            usize::MAX,                            &cancel,                            background_executor.clone(),                        )                    });                     join_all(file_queries)                        .await                        .into_iter()                        .flatten()                        .filter(|string_match| processed_matches.insert(string_match.candidate_id))                        .filter_map(|string_match| open_buffers.get(string_match.candidate_id))                        .cloned()                        .collect()                };                Ok(matched_items)            })            .await    })} fn active_item_buffer(    workspace: &mut Workspace,    cx: &mut ui::ViewContext<Workspace>,) -> anyhow::Result<BufferSnapshot> {    let active_editor = workspace        .active_item(cx)        .context("no active item")?        .downcast::<Editor>()        .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();    Ok(snapshot)} fn create_tab_completion_label(    path: &std::path::Path,    comment_id: Option<HighlightId>,) -> CodeLabel {    let file_name = path        .file_name()        .map(|f| f.to_string_lossy())        .unwrap_or_default();    let parent_path = path        .parent()        .map(|p| p.to_string_lossy())        .unwrap_or_default();    let mut label = CodeLabel::default();    label.push_str(&file_name, None);    label.push_str(" ", None);    label.push_str(&parent_path, comment_id);    label.filter_range = 0..file_name.len();    label}