diff --git a/Cargo.lock b/Cargo.lock index 6e46b5d614..8b62a30a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,6 +425,7 @@ dependencies = [ "gpui", "language", "parking_lot", + "serde", "workspace", ] @@ -11523,6 +11524,7 @@ dependencies = [ "gpui", "itertools 0.11.0", "menu", + "serde", "settings", "smallvec", "story", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e642e090d5..10f50c91bd 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -15,9 +15,8 @@ use anyhow::{anyhow, Result}; use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; use client::telemetry::Telemetry; use collections::{BTreeSet, HashMap, HashSet}; -use editor::actions::ShowCompletions; use editor::{ - actions::{FoldAt, MoveToEndOfLine, Newline, UnfoldAt}, + actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, ToDisplayPoint}, scroll::{Autoscroll, AutoscrollStrategy}, Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint, @@ -36,7 +35,7 @@ use gpui::{ WindowContext, }; use language::{ - language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry, + language_settings::SoftWrap, AnchorRangeExt as _, AutoindentMode, Buffer, LanguageRegistry, LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _, }; use multi_buffer::MultiBufferRow; @@ -1013,6 +1012,7 @@ pub struct Context { edit_suggestions: Vec, pending_slash_commands: Vec, edits_since_last_slash_command_parse: language::Subscription, + slash_command_output_sections: Vec>, message_anchors: Vec, messages_metadata: HashMap, next_message_id: MessageId, @@ -1054,6 +1054,7 @@ impl Context { next_message_id: Default::default(), edit_suggestions: Vec::new(), pending_slash_commands: Vec::new(), + slash_command_output_sections: Vec::new(), edits_since_last_slash_command_parse, summary: None, pending_summary: Task::ready(None), @@ -1090,11 +1091,12 @@ impl Context { } fn serialize(&self, cx: &AppContext) -> SavedContext { + let buffer = self.buffer.read(cx); SavedContext { id: self.id.clone(), zed: "context".into(), version: SavedContext::VERSION.into(), - text: self.buffer.read(cx).text(), + text: buffer.text(), message_metadata: self.messages_metadata.clone(), messages: self .messages(cx) @@ -1108,6 +1110,22 @@ impl Context { .as_ref() .map(|summary| summary.text.clone()) .unwrap_or_default(), + slash_command_output_sections: self + .slash_command_output_sections + .iter() + .filter_map(|section| { + let range = section.range.to_offset(buffer); + if section.range.start.is_valid(buffer) && !range.is_empty() { + Some(SlashCommandOutputSection { + range, + icon: section.icon, + label: section.label.clone(), + }) + } else { + None + } + }) + .collect(), } } @@ -1159,6 +1177,19 @@ impl Context { next_message_id, edit_suggestions: Vec::new(), pending_slash_commands: Vec::new(), + slash_command_output_sections: saved_context + .slash_command_output_sections + .into_iter() + .map(|section| { + let buffer = buffer.read(cx); + SlashCommandOutputSection { + range: buffer.anchor_after(section.range.start) + ..buffer.anchor_before(section.range.end), + icon: section.icon, + label: section.label, + } + }) + .collect(), edits_since_last_slash_command_parse, summary: Some(Summary { text: saved_context.summary, @@ -1457,10 +1488,17 @@ impl Context { .map(|section| SlashCommandOutputSection { range: buffer.anchor_after(start + section.range.start) ..buffer.anchor_before(start + section.range.end), - render_placeholder: section.render_placeholder, + icon: section.icon, + label: section.label, }) .collect::>(); sections.sort_by(|a, b| a.range.cmp(&b.range, buffer)); + + this.slash_command_output_sections + .extend(sections.iter().cloned()); + this.slash_command_output_sections + .sort_by(|a, b| a.range.cmp(&b.range, buffer)); + ContextEvent::SlashCommandFinished { output_range: buffer.anchor_after(start) ..buffer.anchor_before(new_end), @@ -2224,6 +2262,7 @@ impl ContextEditor { cx.subscribe(&editor, Self::handle_editor_event), ]; + let sections = context.read(cx).slash_command_output_sections.clone(); let mut this = Self { context, editor, @@ -2237,6 +2276,7 @@ impl ContextEditor { _subscriptions, }; this.update_message_headers(cx); + this.insert_slash_command_output_sections(sections, cx); this } @@ -2631,21 +2671,27 @@ impl ContextEditor { FoldPlaceholder { render: Arc::new({ let editor = cx.view().downgrade(); - let render_placeholder = section.render_placeholder.clone(); - move |fold_id, fold_range, cx| { + let icon = section.icon; + let label = section.label.clone(); + move |fold_id, fold_range, _cx| { let editor = editor.clone(); - let unfold = Arc::new(move |cx: &mut WindowContext| { - editor - .update(cx, |editor, cx| { - let buffer_start = fold_range - .start - .to_point(&editor.buffer().read(cx).read(cx)); - let buffer_row = MultiBufferRow(buffer_start.row); - editor.unfold_at(&UnfoldAt { buffer_row }, cx); - }) - .ok(); - }); - render_placeholder(fold_id.into(), unfold, cx) + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ElevatedSurface) + .child(Icon::new(icon)) + .child(Label::new(label.clone()).single_line()) + .on_click(move |_, cx| { + editor + .update(cx, |editor, cx| { + let buffer_start = fold_range + .start + .to_point(&editor.buffer().read(cx).read(cx)); + let buffer_row = MultiBufferRow(buffer_start.row); + editor.unfold_at(&UnfoldAt { buffer_row }, cx); + }) + .ok(); + }) + .into_any_element() } }), constrain_width: false, diff --git a/crates/assistant/src/context_store.rs b/crates/assistant/src/context_store.rs index d216ba0999..f418b61173 100644 --- a/crates/assistant/src/context_store.rs +++ b/crates/assistant/src/context_store.rs @@ -1,5 +1,6 @@ use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata}; use anyhow::{anyhow, Result}; +use assistant_slash_command::SlashCommandOutputSection; use collections::HashMap; use fs::Fs; use futures::StreamExt; @@ -27,10 +28,22 @@ pub struct SavedContext { pub messages: Vec, pub message_metadata: HashMap, pub summary: String, + pub slash_command_output_sections: Vec>, } impl SavedContext { - pub const VERSION: &'static str = "0.2.0"; + pub const VERSION: &'static str = "0.3.0"; +} + +#[derive(Serialize, Deserialize)] +pub struct SavedContextV0_2_0 { + pub id: Option, + pub zed: String, + pub version: String, + pub text: String, + pub messages: Vec, + pub message_metadata: HashMap, + pub summary: String, } #[derive(Serialize, Deserialize)] @@ -100,6 +113,20 @@ impl ContextStore { SavedContext::VERSION => { Ok(serde_json::from_value::(saved_context_json)?) } + "0.2.0" => { + let saved_context = + serde_json::from_value::(saved_context_json)?; + Ok(SavedContext { + id: saved_context.id, + zed: saved_context.zed, + version: saved_context.version, + text: saved_context.text, + messages: saved_context.messages, + message_metadata: saved_context.message_metadata, + summary: saved_context.summary, + slash_command_output_sections: Vec::new(), + }) + } "0.1.0" => { let saved_context = serde_json::from_value::(saved_context_json)?; @@ -111,6 +138,7 @@ impl ContextStore { messages: saved_context.messages, message_metadata: saved_context.message_metadata, summary: saved_context.summary, + slash_command_output_sections: Vec::new(), }) } _ => Err(anyhow!("unrecognized saved context version: {}", version)), diff --git a/crates/assistant/src/slash_command/active_command.rs b/crates/assistant/src/slash_command/active_command.rs index 609dc83011..235a38118a 100644 --- a/crates/assistant/src/slash_command/active_command.rs +++ b/crates/assistant/src/slash_command/active_command.rs @@ -1,14 +1,13 @@ use super::{ - file_command::{codeblock_fence_for_path, EntryPlaceholder}, + file_command::{build_entry_output_section, codeblock_fence_for_path}, SlashCommand, SlashCommandOutput, }; use anyhow::{anyhow, Result}; -use assistant_slash_command::SlashCommandOutputSection; use editor::Editor; use gpui::{AppContext, Task, WeakView}; use language::LspAdapterDelegate; use std::sync::Arc; -use ui::{IntoElement, WindowContext}; +use ui::WindowContext; use workspace::Workspace; pub(crate) struct ActiveSlashCommand; @@ -81,19 +80,12 @@ impl SlashCommand for ActiveSlashCommand { let range = 0..text.len(); Ok(SlashCommandOutput { text, - sections: vec![SlashCommandOutputSection { + sections: vec![build_entry_output_section( range, - render_placeholder: Arc::new(move |id, unfold, _| { - EntryPlaceholder { - id, - path: path.clone(), - is_directory: false, - line_range: None, - unfold, - } - .into_any_element() - }), - }], + path.as_deref(), + false, + None, + )], run_commands_in_text: false, }) }) diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index bd26b724c9..d1f6572692 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -1,4 +1,4 @@ -use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput}; +use super::{SlashCommand, SlashCommandOutput}; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Result}; use assistant_slash_command::SlashCommandOutputSection; @@ -68,14 +68,8 @@ impl SlashCommand for DefaultSlashCommand { Ok(SlashCommandOutput { sections: vec![SlashCommandOutputSection { range: 0..text.len(), - render_placeholder: Arc::new(move |id, unfold, _cx| { - PromptPlaceholder { - title: "Default".into(), - id, - unfold, - } - .into_any_element() - }), + icon: IconName::Library, + label: "Default".into(), }], text, run_commands_in_text: true, diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index 0438ed2956..ada70d7675 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -2,7 +2,7 @@ use super::{SlashCommand, SlashCommandOutput}; use anyhow::{anyhow, Result}; use assistant_slash_command::SlashCommandOutputSection; use fuzzy::{PathMatch, StringMatchCandidate}; -use gpui::{svg, AppContext, Model, RenderOnce, Task, View, WeakView}; +use gpui::{AppContext, Model, Task, View, WeakView}; use language::{ Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate, OffsetRangeExt, ToOffset, @@ -14,7 +14,7 @@ use std::{ ops::Range, sync::{atomic::AtomicBool, Arc}, }; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use util::paths::PathMatcher; use util::ResultExt; use workspace::Workspace; @@ -164,14 +164,45 @@ impl SlashCommand for DiagnosticsCommand { .into_iter() .map(|(range, placeholder_type)| SlashCommandOutputSection { range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - DiagnosticsPlaceholder { - id, - unfold, - placeholder_type: placeholder_type.clone(), + icon: match placeholder_type { + PlaceholderType::Root(_, _) => IconName::ExclamationTriangle, + PlaceholderType::File(_) => IconName::File, + PlaceholderType::Diagnostic(DiagnosticType::Error, _) => { + IconName::XCircle } - .into_any_element() - }), + PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => { + IconName::ExclamationTriangle + } + }, + label: match placeholder_type { + PlaceholderType::Root(summary, source) => { + let mut label = String::new(); + label.push_str("Diagnostics"); + if let Some(source) = source { + write!(label, " ({})", source).unwrap(); + } + + if summary.error_count > 0 || summary.warning_count > 0 { + label.push(':'); + + if summary.error_count > 0 { + write!(label, " {} errors", summary.error_count).unwrap(); + if summary.warning_count > 0 { + label.push_str(","); + } + } + + if summary.warning_count > 0 { + write!(label, " {} warnings", summary.warning_count) + .unwrap(); + } + } + + label.into() + } + PlaceholderType::File(file_path) => file_path.into(), + PlaceholderType::Diagnostic(_, message) => message.into(), + }, }) .collect(), run_commands_in_text: false, @@ -223,10 +254,10 @@ fn collect_diagnostics( options: Options, cx: &mut AppContext, ) -> Task, PlaceholderType)>)>> { - let header = if let Some(path_matcher) = &options.path_matcher { - format!("diagnostics: {}", path_matcher.source()) + let error_source = if let Some(path_matcher) = &options.path_matcher { + Some(path_matcher.source().to_string()) } else { - "diagnostics".to_string() + None }; let project_handle = project.downgrade(); @@ -234,7 +265,11 @@ fn collect_diagnostics( cx.spawn(|mut cx| async move { let mut text = String::new(); - writeln!(text, "{}", &header).unwrap(); + if let Some(error_source) = error_source.as_ref() { + writeln!(text, "diagnostics: {}", error_source).unwrap(); + } else { + writeln!(text, "diagnostics").unwrap(); + } let mut sections: Vec<(Range, PlaceholderType)> = Vec::new(); let mut project_summary = DiagnosticSummary::default(); @@ -276,7 +311,7 @@ fn collect_diagnostics( } sections.push(( 0..text.len(), - PlaceholderType::Root(project_summary, header), + PlaceholderType::Root(project_summary, error_source), )); Ok((text, sections)) @@ -362,12 +397,12 @@ fn collect_diagnostic( #[derive(Clone)] pub enum PlaceholderType { - Root(DiagnosticSummary, String), + Root(DiagnosticSummary, Option), File(String), Diagnostic(DiagnosticType, String), } -#[derive(Copy, Clone, IntoElement)] +#[derive(Copy, Clone)] pub enum DiagnosticType { Warning, Error, @@ -381,64 +416,3 @@ impl DiagnosticType { } } } - -#[derive(IntoElement)] -pub struct DiagnosticsPlaceholder { - pub id: ElementId, - pub placeholder_type: PlaceholderType, - pub unfold: Arc, -} - -impl RenderOnce for DiagnosticsPlaceholder { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let unfold = self.unfold; - let (icon, content) = match self.placeholder_type { - PlaceholderType::Root(summary, title) => ( - h_flex() - .w_full() - .gap_0p5() - .when(summary.error_count > 0, |this| { - this.child(DiagnosticType::Error) - .child(Label::new(summary.error_count.to_string())) - }) - .when(summary.warning_count > 0, |this| { - this.child(DiagnosticType::Warning) - .child(Label::new(summary.warning_count.to_string())) - }) - .into_any_element(), - Label::new(title), - ), - PlaceholderType::File(file) => ( - Icon::new(IconName::File).into_any_element(), - Label::new(file), - ), - PlaceholderType::Diagnostic(diagnostic_type, message) => ( - diagnostic_type.into_any_element(), - Label::new(message).single_line(), - ), - }; - - ButtonLike::new(self.id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(icon) - .child(content) - .on_click(move |_, cx| unfold(cx)) - } -} - -impl RenderOnce for DiagnosticType { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - svg() - .size(cx.text_style().font_size) - .flex_none() - .map(|icon| match self { - DiagnosticType::Error => icon - .path(IconName::XCircle.path()) - .text_color(Color::Error.color(cx)), - DiagnosticType::Warning => icon - .path(IconName::ExclamationTriangle.path()) - .text_color(Color::Warning.color(cx)), - }) - } -} diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 9d8fed1012..37be292800 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -10,7 +10,7 @@ use gpui::{AppContext, Task, WeakView}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use http::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::LspAdapterDelegate; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use workspace::Workspace; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -152,37 +152,11 @@ impl SlashCommand for FetchSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - FetchPlaceholder { - id, - unfold, - url: url.clone(), - } - .into_any_element() - }), + icon: IconName::AtSign, + label: format!("fetch {}", url).into(), }], run_commands_in_text: false, }) }) } } - -#[derive(IntoElement)] -struct FetchPlaceholder { - pub id: ElementId, - pub unfold: Arc, - pub url: SharedString, -} - -impl RenderOnce for FetchPlaceholder { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let unfold = self.unfold; - - ButtonLike::new(self.id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::AtSign)) - .child(Label::new(format!("fetch {url}", url = self.url))) - .on_click(move |_, cx| unfold(cx)) - } -} diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 787c98c358..31016aa19e 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use assistant_slash_command::SlashCommandOutputSection; use fs::Fs; use fuzzy::PathMatch; -use gpui::{AppContext, Model, RenderOnce, SharedString, Task, View, WeakView}; +use gpui::{AppContext, Model, Task, View, WeakView}; use language::{LineEnding, LspAdapterDelegate}; use project::{PathMatchCandidateSet, Worktree}; use std::{ @@ -12,7 +12,7 @@ use std::{ path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, }; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use util::{paths::PathMatcher, ResultExt}; use workspace::Workspace; @@ -156,18 +156,13 @@ impl SlashCommand for FileSlashCommand { text, sections: ranges .into_iter() - .map(|(range, path, entry_type)| SlashCommandOutputSection { - range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - EntryPlaceholder { - path: Some(path.clone()), - is_directory: entry_type == EntryType::Directory, - line_range: None, - id, - unfold, - } - .into_any_element() - }), + .map(|(range, path, entry_type)| { + build_entry_output_section( + range, + Some(&path), + entry_type == EntryType::Directory, + None, + ) }) .collect(), run_commands_in_text: false, @@ -349,44 +344,6 @@ async fn collect_file_content( anyhow::Ok(()) } -#[derive(IntoElement)] -pub struct EntryPlaceholder { - pub path: Option, - pub is_directory: bool, - pub line_range: Option>, - pub id: ElementId, - pub unfold: Arc, -} - -impl RenderOnce for EntryPlaceholder { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let unfold = self.unfold; - let title = if let Some(path) = self.path.as_ref() { - SharedString::from(path.to_string_lossy().to_string()) - } else { - SharedString::from("untitled") - }; - let icon = if self.is_directory { - IconName::Folder - } else { - IconName::File - }; - - ButtonLike::new(self.id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(icon)) - .child(Label::new(title)) - .when_some(self.line_range, |button, line_range| { - button.child(Label::new(":")).child(Label::new(format!( - "{}-{}", - line_range.start, line_range.end - ))) - }) - .on_click(move |_, cx| unfold(cx)) - } -} - pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option>) -> String { let mut text = String::new(); write!(text, "```").unwrap(); @@ -408,3 +365,31 @@ pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option, + path: Option<&Path>, + is_directory: bool, + line_range: Option>, +) -> SlashCommandOutputSection { + let mut label = if let Some(path) = path { + path.to_string_lossy().to_string() + } else { + "untitled".to_string() + }; + if let Some(line_range) = line_range { + write!(label, ":{}-{}", line_range.start, line_range.end).unwrap(); + } + + let icon = if is_directory { + IconName::Folder + } else { + IconName::File + }; + + SlashCommandOutputSection { + range, + icon, + label: label.into(), + } +} diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 5dc87905b5..6db984469b 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -53,9 +53,8 @@ impl SlashCommand for NowSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - NowPlaceholder { id, unfold, now }.into_any_element() - }), + icon: IconName::CountdownTimer, + label: now.to_rfc3339().into(), }], run_commands_in_text: false, })) diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 4adcc8ffd7..d5f4d74404 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -10,7 +10,7 @@ use std::{ path::Path, sync::{atomic::AtomicBool, Arc}, }; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use workspace::Workspace; pub(crate) struct ProjectSlashCommand; @@ -138,15 +138,8 @@ impl SlashCommand for ProjectSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - ButtonLike::new(id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::FileTree)) - .child(Label::new("Project")) - .on_click(move |_, cx| unfold(cx)) - .into_any_element() - }), + icon: IconName::FileTree, + label: "Project".into(), }], run_commands_in_text: false, }) diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 21e22fd837..37610cc017 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -5,7 +5,7 @@ use assistant_slash_command::SlashCommandOutputSection; use gpui::{AppContext, Task, WeakView}; use language::LspAdapterDelegate; use std::sync::{atomic::AtomicBool, Arc}; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use workspace::Workspace; pub(crate) struct PromptSlashCommand; @@ -78,36 +78,11 @@ impl SlashCommand for PromptSlashCommand { text: prompt, sections: vec![SlashCommandOutputSection { range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - PromptPlaceholder { - id, - unfold, - title: title.clone(), - } - .into_any_element() - }), + icon: IconName::Library, + label: title, }], run_commands_in_text: true, }) }) } } - -#[derive(IntoElement)] -pub struct PromptPlaceholder { - pub title: SharedString, - pub id: ElementId, - pub unfold: Arc, -} - -impl RenderOnce for PromptPlaceholder { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let unfold = self.unfold; - ButtonLike::new(self.id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::Library)) - .child(Label::new(self.title)) - .on_click(move |_, cx| unfold(cx)) - } -} diff --git a/crates/assistant/src/slash_command/rustdoc_command.rs b/crates/assistant/src/slash_command/rustdoc_command.rs index ecb6cee660..3104189ead 100644 --- a/crates/assistant/src/slash_command/rustdoc_command.rs +++ b/crates/assistant/src/slash_command/rustdoc_command.rs @@ -11,7 +11,7 @@ use http::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::LspAdapterDelegate; use project::{Project, ProjectPath}; use rustdoc::{convert_rustdoc_to_markdown, CrateName, LocalProvider, RustdocSource, RustdocStore}; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use util::{maybe, ResultExt}; use workspace::Workspace; @@ -213,57 +213,26 @@ impl SlashCommand for RustdocSlashCommand { cx.foreground_executor().spawn(async move { let (source, text) = text.await?; let range = 0..text.len(); + let crate_path = module_path + .map(|module_path| format!("{}::{}", crate_name, module_path)) + .unwrap_or_else(|| crate_name.to_string()); Ok(SlashCommandOutput { text, sections: vec![SlashCommandOutputSection { range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - RustdocPlaceholder { - id, - unfold, - source, - crate_name: crate_name.clone(), - module_path: module_path.clone(), + icon: IconName::FileRust, + label: format!( + "rustdoc ({source}): {crate_path}", + source = match source { + RustdocSource::Index => "index", + RustdocSource::Local => "local", + RustdocSource::DocsDotRs => "docs.rs", } - .into_any_element() - }), + ) + .into(), }], run_commands_in_text: false, }) }) } } - -#[derive(IntoElement)] -struct RustdocPlaceholder { - pub id: ElementId, - pub unfold: Arc, - pub source: RustdocSource, - pub crate_name: CrateName, - pub module_path: Option, -} - -impl RenderOnce for RustdocPlaceholder { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let unfold = self.unfold; - - let crate_path = self - .module_path - .map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name)) - .unwrap_or(self.crate_name.to_string()); - - ButtonLike::new(self.id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::FileRust)) - .child(Label::new(format!( - "rustdoc ({source}): {crate_path}", - source = match self.source { - RustdocSource::Index => "index", - RustdocSource::Local => "local", - RustdocSource::DocsDotRs => "docs.rs", - } - ))) - .on_click(move |_, cx| unfold(cx)) - } -} diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index dcbcdd6692..ca2328ca6b 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -1,5 +1,5 @@ use super::{ - file_command::{codeblock_fence_for_path, EntryPlaceholder}, + file_command::{build_entry_output_section, codeblock_fence_for_path}, SlashCommand, SlashCommandOutput, }; use anyhow::Result; @@ -12,7 +12,7 @@ use std::{ path::PathBuf, sync::{atomic::AtomicBool, Arc}, }; -use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName}; +use ui::{prelude::*, IconName}; use util::ResultExt; use workspace::Workspace; @@ -151,34 +151,19 @@ impl SlashCommand for SearchSlashCommand { text.push_str(&excerpt); writeln!(text, "\n```\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, _| { - EntryPlaceholder { - id, - path: Some(full_path.clone()), - is_directory: false, - line_range: Some(start_row + 1..end_row + 1), - unfold, - } - .into_any_element() - }), - }); + sections.push(build_entry_output_section( + section_start_ix..section_end_ix, + Some(&full_path), + false, + Some(start_row + 1..end_row + 1), + )); } let query = SharedString::from(query); sections.push(SlashCommandOutputSection { range: 0..text.len(), - render_placeholder: Arc::new(move |id, unfold, _cx| { - ButtonLike::new(id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::MagnifyingGlass)) - .child(Label::new(query.clone())) - .on_click(move |_, cx| unfold(cx)) - .into_any_element() - }), + icon: IconName::MagnifyingGlass, + label: query, }); SlashCommandOutput { diff --git a/crates/assistant/src/slash_command/tabs_command.rs b/crates/assistant/src/slash_command/tabs_command.rs index 5945a9fd3f..6c4daf0afa 100644 --- a/crates/assistant/src/slash_command/tabs_command.rs +++ b/crates/assistant/src/slash_command/tabs_command.rs @@ -1,15 +1,14 @@ use super::{ - file_command::{codeblock_fence_for_path, EntryPlaceholder}, + file_command::{build_entry_output_section, codeblock_fence_for_path}, 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, sync::Arc}; -use ui::{IntoElement, WindowContext}; +use ui::WindowContext; use workspace::Workspace; pub(crate) struct TabsSlashCommand; @@ -89,20 +88,12 @@ impl SlashCommand for TabsSlashCommand { } 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, _| { - EntryPlaceholder { - id, - path: full_path.clone(), - is_directory: false, - line_range: None, - unfold, - } - .into_any_element() - }), - }); + sections.push(build_entry_output_section( + section_start_ix..section_end_ix, + full_path.as_deref(), + false, + None, + )); } Ok(SlashCommandOutput { diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index a2a14593da..3d764bb0be 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -18,4 +18,5 @@ derive_more.workspace = true gpui.workspace = true language.workspace = true parking_lot.workspace = true +serde.workspace = true workspace.workspace = true diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 056fe69b36..691816ed7c 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -1,14 +1,15 @@ mod slash_command_registry; use anyhow::Result; -use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext}; +use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; use language::{CodeLabel, LspAdapterDelegate}; +use serde::{Deserialize, Serialize}; pub use slash_command_registry::*; use std::{ ops::Range, sync::{atomic::AtomicBool, Arc}, }; -use workspace::Workspace; +use workspace::{ui::IconName, Workspace}; pub fn init(cx: &mut AppContext) { SlashCommandRegistry::default_global(cx); @@ -55,8 +56,9 @@ pub struct SlashCommandOutput { pub run_commands_in_text: bool, } -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct SlashCommandOutputSection { pub range: Range, - pub render_placeholder: RenderFoldPlaceholder, + pub icon: IconName, + pub label: SharedString, } diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index fd1aefc3f7..4f555f94ab 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -3,9 +3,9 @@ use std::sync::{atomic::AtomicBool, Arc}; use anyhow::{anyhow, Result}; use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; use futures::FutureExt; -use gpui::{AppContext, IntoElement, Task, WeakView, WindowContext}; +use gpui::{AppContext, Task, WeakView, WindowContext}; use language::LspAdapterDelegate; -use ui::{prelude::*, ButtonLike, ElevationIndex}; +use ui::prelude::*; use wasmtime_wasi::WasiView; use workspace::Workspace; @@ -87,18 +87,8 @@ impl SlashCommand for ExtensionSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - render_placeholder: Arc::new({ - let command_name = command_name.clone(); - move |id, unfold, _cx| { - ButtonLike::new(id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::Code)) - .child(Label::new(command_name.clone())) - .on_click(move |_event, cx| unfold(cx)) - .into_any_element() - } - }), + icon: IconName::Code, + label: command_name, }], run_commands_in_text: false, }) diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 9fec75e17b..c5cb53d3d2 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -17,6 +17,7 @@ chrono.workspace = true gpui.workspace = true itertools = { workspace = true, optional = true } menu.workspace = true +serde.workspace = true settings.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index bda16f6e13..67204c429a 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -1,4 +1,5 @@ use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation}; +use serde::{Deserialize, Serialize}; use strum::EnumIter; use crate::{prelude::*, Indicator}; @@ -76,7 +77,7 @@ impl IconSize { } } -#[derive(Debug, PartialEq, Copy, Clone, EnumIter)] +#[derive(Debug, PartialEq, Copy, Clone, EnumIter, Serialize, Deserialize)] pub enum IconName { Ai, ArrowCircle,