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

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
This commit is contained in:
Kirill Bulatov 2024-08-13 13:15:57 +03:00 committed by GitHub
parent c2b254a67a
commit 081cbcebd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 93 additions and 137 deletions

View File

@ -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);

View File

@ -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());

View File

@ -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;

View File

@ -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)))
}
}

View File

@ -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)),
}
}
}