323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
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, HighlightId, LspAdapterDelegate};
|
|
use std::{
|
|
path::PathBuf,
|
|
sync::{Arc, atomic::AtomicBool},
|
|
};
|
|
use ui::{ActiveTheme, App, Window, prelude::*};
|
|
use util::ResultExt;
|
|
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 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, 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_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: 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.to_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<PathBuf>, 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(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_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::new(id, &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 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: &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
|
|
}
|