Introduce a new /tabs command (#12382)

This inserts the content of the open tabs sorted by recency.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-05-28 17:17:34 +02:00 committed by GitHub
parent b466a8b828
commit 371abd37f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 174 additions and 77 deletions

View File

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

View File

@ -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<ConversationEditor>,

View File

@ -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<Result<SlashCommandOutput>> {
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::<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 mut most_recent_buffer = None;
for editor in workspace.items_of_type::<Editor>(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)))
}

View File

@ -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<std::sync::atomic::AtomicBool>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: 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);
}
}
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));
}
}
}
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)),
}
}
}