diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index a5bcb2b5cb..0da165d164 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,5 +1,5 @@ use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager}; -use crate::slash_command::search_command; +use crate::slash_command::{search_command, tabs_command}; use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel}, codegen::{self, Codegen, CodegenKind}, @@ -210,6 +210,7 @@ impl AssistantPanel { prompt_command::PromptSlashCommand::new(prompt_library.clone()), ); slash_command_registry.register_command(active_command::ActiveSlashCommand); + slash_command_registry.register_command(tabs_command::TabsSlashCommand); slash_command_registry.register_command(project_command::ProjectSlashCommand); slash_command_registry.register_command(search_command::SearchSlashCommand); @@ -1883,15 +1884,15 @@ impl Conversation { async move { let output = output.await; this.update(&mut cx, |this, cx| match output { - Ok(output) => { + Ok(mut output) => { + if !output.text.ends_with('\n') { + output.text.push('\n'); + } + let sections = this.buffer.update(cx, |buffer, cx| { let start = command_range.start.to_offset(buffer); let old_end = command_range.end.to_offset(buffer); - let new_end = start + output.text.len(); buffer.edit([(start..old_end, output.text)], None, cx); - if buffer.chars_at(new_end).next() != Some('\n') { - buffer.edit([(new_end..new_end, "\n")], None, cx); - } let mut sections = output .sections diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index e6c74c2530..e602274b7d 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -21,6 +21,7 @@ pub mod file_command; pub mod project_command; pub mod prompt_command; pub mod search_command; +pub mod tabs_command; pub(crate) struct SlashCommandCompletionProvider { editor: WeakView, diff --git a/crates/assistant/src/slash_command/active_command.rs b/crates/assistant/src/slash_command/active_command.rs index 36465e92b8..76365fe10f 100644 --- a/crates/assistant/src/slash_command/active_command.rs +++ b/crates/assistant/src/slash_command/active_command.rs @@ -1,9 +1,8 @@ use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput}; use anyhow::{anyhow, Result}; use assistant_slash_command::SlashCommandOutputSection; -use collections::HashMap; use editor::Editor; -use gpui::{AppContext, Entity, Task, WeakView}; +use gpui::{AppContext, Task, WeakView}; use language::LspAdapterDelegate; use std::{borrow::Cow, sync::Arc}; use ui::{IntoElement, WindowContext}; @@ -45,79 +44,59 @@ impl SlashCommand for ActiveSlashCommand { cx: &mut WindowContext, ) -> Task> { let output = workspace.update(cx, |workspace, cx| { - let mut timestamps_by_entity_id = HashMap::default(); - 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 Some(active_item) = workspace.active_item(cx) else { + return Task::ready(Err(anyhow!("no active tab"))); + }; + let Some(buffer) = active_item + .downcast::() + .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton()) + else { + return Task::ready(Err(anyhow!("active tab is not an editor"))); + }; - let mut most_recent_buffer = None; - for editor in workspace.items_of_type::(cx) { - let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else { - continue; - }; + let snapshot = buffer.read(cx).snapshot(); + let path = snapshot.resolve_file_path(cx, true); + let text = cx.background_executor().spawn({ + let path = path.clone(); + async move { + let path = path + .as_ref() + .map(|path| path.to_string_lossy()) + .unwrap_or_else(|| Cow::Borrowed("untitled")); - let timestamp = timestamps_by_entity_id - .get(&editor.entity_id()) - .copied() - .unwrap_or_default(); - if most_recent_buffer - .as_ref() - .map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp) - { - most_recent_buffer = Some((buffer, timestamp)); - } - } - - if let Some((buffer, _)) = most_recent_buffer { - let snapshot = buffer.read(cx).snapshot(); - let path = snapshot.resolve_file_path(cx, true); - let text = cx.background_executor().spawn({ - let path = path.clone(); - async move { - let path = path - .as_ref() - .map(|path| path.to_string_lossy()) - .unwrap_or_else(|| Cow::Borrowed("untitled")); - - let mut output = String::with_capacity(path.len() + snapshot.len() + 9); - output.push_str("```"); - output.push_str(&path); - output.push('\n'); - for chunk in snapshot.as_rope().chunks() { - output.push_str(chunk); - } - if !output.ends_with('\n') { - output.push('\n'); - } - output.push_str("```"); - output + let mut output = String::with_capacity(path.len() + snapshot.len() + 9); + output.push_str("```"); + output.push_str(&path); + output.push('\n'); + for chunk in snapshot.as_rope().chunks() { + output.push_str(chunk); } - }); - cx.foreground_executor().spawn(async move { - let text = text.await; - let range = 0..text.len(); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, - render_placeholder: Arc::new(move |id, unfold, _| { - FilePlaceholder { - id, - path: path.clone(), - line_range: None, - unfold, - } - .into_any_element() - }), - }], - }) + if !output.ends_with('\n') { + output.push('\n'); + } + output.push_str("```"); + output + } + }); + cx.foreground_executor().spawn(async move { + let text = text.await; + let range = 0..text.len(); + Ok(SlashCommandOutput { + text, + sections: vec![SlashCommandOutputSection { + range, + render_placeholder: Arc::new(move |id, unfold, _| { + FilePlaceholder { + id, + path: path.clone(), + line_range: None, + unfold, + } + .into_any_element() + }), + }], }) - } else { - Task::ready(Err(anyhow!("no recent buffer found"))) - } + }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) } diff --git a/crates/assistant/src/slash_command/tabs_command.rs b/crates/assistant/src/slash_command/tabs_command.rs new file mode 100644 index 0000000000..af6ef3dc9b --- /dev/null +++ b/crates/assistant/src/slash_command/tabs_command.rs @@ -0,0 +1,116 @@ +use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput}; +use anyhow::{anyhow, Result}; +use assistant_slash_command::SlashCommandOutputSection; +use collections::HashMap; +use editor::Editor; +use gpui::{AppContext, Entity, Task, WeakView}; +use language::LspAdapterDelegate; +use std::{fmt::Write, path::Path, sync::Arc}; +use ui::{IntoElement, WindowContext}; +use workspace::Workspace; + +pub(crate) struct TabsSlashCommand; + +impl SlashCommand for TabsSlashCommand { + fn name(&self) -> String { + "tabs".into() + } + + fn description(&self) -> String { + "insert content from open tabs".into() + } + + fn tooltip_text(&self) -> String { + "insert open tabs".into() + } + + fn requires_argument(&self) -> bool { + false + } + + fn complete_argument( + &self, + _query: String, + _cancel: Arc, + _cx: &mut AppContext, + ) -> Task>> { + Task::ready(Err(anyhow!("this command does not require argument"))) + } + + fn run( + self: Arc, + _argument: Option<&str>, + workspace: WeakView, + _delegate: Arc, + cx: &mut WindowContext, + ) -> Task> { + 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); + } + } + + 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)); + } + } + } + + open_buffers + }); + + match open_buffers { + 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(); + for (full_path, buffer, _) in open_buffers { + let section_start_ix = text.len(); + writeln!( + text, + "```{}\n", + full_path + .as_deref() + .unwrap_or(Path::new("untitled")) + .display() + ) + .unwrap(); + for chunk in buffer.as_rope().chunks() { + text.push_str(chunk); + } + if !text.ends_with('\n') { + text.push('\n'); + } + writeln!(text, "```\n").unwrap(); + let section_end_ix = text.len() - 1; + + sections.push(SlashCommandOutputSection { + range: section_start_ix..section_end_ix, + render_placeholder: Arc::new(move |id, unfold, _| { + FilePlaceholder { + id, + path: full_path.clone(), + line_range: None, + unfold, + } + .into_any_element() + }), + }); + } + + Ok(SlashCommandOutput { text, sections }) + }), + Err(error) => Task::ready(Err(error)), + } + } +}