317 lines · 11.1 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use assistant_slash_command::{    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,    SlashCommandResult,};use collections::{HashMap, HashSet};use editor::Editor;use futures::future::join_all;use gpui::{Task, WeakEntity};use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};use std::sync::{Arc, atomic::AtomicBool};use ui::{ActiveTheme, App, Window, prelude::*};use util::{ResultExt, paths::PathStyle};use workspace::Workspace; use crate::file_command::append_buffer_to_output; pub 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 icon(&self) -> IconName {        IconName::FileTree    }     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<WeakEntity<Workspace>>,        window: &mut Window,        cx: &mut App,    ) -> 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 Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {            return Task::ready(Err(anyhow::anyhow!("no workspace")));        };         let active_item_path = workspace.update(cx, |workspace, cx| {            let snapshot = active_item_buffer(workspace, cx).ok()?;            snapshot.resolve_file_path(true, cx)        });        let path_style = workspace.read(cx).path_style(cx);         let current_query = arguments.last().cloned().unwrap_or_default();        let tab_items_search = tab_items_for_queries(            Some(workspace.downgrade()),            &[current_query],            cancel,            false,            window,            cx,        );         let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);        window.spawn(cx, 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 = path?;                if argument_set.contains(&path) {                    return None;                }                if active_item_path.as_ref() == Some(&path) {                    return None;                }                let label = create_tab_completion_label(&path, path_style, comment_id);                Some(ArgumentCompletion {                    label,                    new_text: path,                    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();                    let label =                        create_tab_completion_label(active_item_path, path_style, 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: WeakEntity<Workspace>,        _delegate: Option<Arc<dyn LspAdapterDelegate>>,        window: &mut Window,        cx: &mut App,    ) -> Task<SlashCommandResult> {        let tab_items_search = tab_items_for_queries(            Some(workspace),            arguments,            Arc::new(AtomicBool::new(false)),            true,            window,            cx,        );         cx.background_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.into_event_stream())        })    }} fn tab_items_for_queries(    workspace: Option<WeakEntity<Workspace>>,    queries: &[String],    cancel: Arc<AtomicBool>,    strict_match: bool,    window: &mut Window,    cx: &mut App,) -> Task<anyhow::Result<Vec<(Option<String>, BufferSnapshot, usize)>>> {    let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());    let queries = queries.to_owned();    window.spawn(cx, async move |cx| {        let mut open_buffers =            workspace                .context("no workspace")?                .update(cx, |workspace, cx| {                    if strict_match && empty_query {                        let snapshot = active_item_buffer(workspace, cx)?;                        let full_path = snapshot.resolve_file_path(true, cx);                        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()                            && let Some(timestamp) =                                timestamps_by_entity_id.get(&editor.entity_id())                            && visited_buffers.insert(buffer.read(cx).remote_id())                        {                            let snapshot = buffer.read(cx).snapshot();                            let full_path = snapshot.resolve_file_path(true, cx);                            open_buffers.push((full_path, snapshot, *timestamp));                        }                    }                     Ok(open_buffers)                })??;         let background_executor = cx.background_executor().clone();        cx.background_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, ..))| Some((id, full_path.clone()?)))                    .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, ..))| {                        Some(fuzzy::StringMatchCandidate::new(id, full_path.as_ref()?))                    })                    .collect::<Vec<_>>();                let mut processed_matches = HashSet::default();                let file_queries = queries.iter().map(|query| {                    fuzzy::match_strings(                        &match_candidates,                        query,                        true,                        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 Context<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: &str,    path_style: PathStyle,    comment_id: Option<HighlightId>,) -> CodeLabel {    let (parent_path, file_name) = path_style.split(path);    let mut label = CodeLabelBuilder::default();    label.push_str(file_name, None);    label.push_str(" ", None);    label.push_str(parent_path.unwrap_or_default(), comment_id);    label.respan_filter_range(Some(file_name));    label.build()}