From 98fc57eefb047f1e6ef476bb59265d039507714e Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 10 Oct 2024 23:33:25 +0100 Subject: [PATCH 01/49] context_servers: Fix protocol serialization The protocol requires that certain elements can't be null but instead must be skipped. We correctly skip these now. --- .../slash_command/context_server_command.rs | 12 +- crates/context_servers/src/manager.rs | 2 +- crates/context_servers/src/protocol.rs | 6 +- crates/context_servers/src/types.rs | 135 ++++++++++++++---- 4 files changed, 116 insertions(+), 39 deletions(-) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 6b1ae39186d28..45feedba87a0f 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -6,7 +6,7 @@ use assistant_slash_command::{ use collections::HashMap; use context_servers::{ manager::{ContextServer, ContextServerManager}, - protocol::PromptInfo, + types::Prompt, }; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; @@ -18,11 +18,11 @@ use workspace::Workspace; pub struct ContextServerSlashCommand { server_id: String, - prompt: PromptInfo, + prompt: Prompt, } impl ContextServerSlashCommand { - pub fn new(server: &Arc, prompt: PromptInfo) -> Self { + pub fn new(server: &Arc, prompt: Prompt) -> Self { Self { server_id: server.id.clone(), prompt, @@ -154,7 +154,7 @@ impl SlashCommand for ContextServerSlashCommand { } } -fn completion_argument(prompt: &PromptInfo, arguments: &[String]) -> Result<(String, String)> { +fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> { if arguments.is_empty() { return Err(anyhow!("No arguments given")); } @@ -170,7 +170,7 @@ fn completion_argument(prompt: &PromptInfo, arguments: &[String]) -> Result<(Str } } -fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result> { +fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result> { match &prompt.arguments { Some(args) if args.len() > 1 => Err(anyhow!( "Prompt has more than one argument, which is not supported" @@ -199,7 +199,7 @@ fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result bool { +pub fn acceptable_prompt(prompt: &Prompt) -> bool { match &prompt.arguments { None => true, Some(args) if args.len() <= 1 => true, diff --git a/crates/context_servers/src/manager.rs b/crates/context_servers/src/manager.rs index 08e403a43442e..3c21fd53fb582 100644 --- a/crates/context_servers/src/manager.rs +++ b/crates/context_servers/src/manager.rs @@ -85,7 +85,7 @@ impl ContextServer { )?; let protocol = crate::protocol::ModelContextProtocol::new(client); - let client_info = types::EntityInfo { + let client_info = types::Implementation { name: "Zed".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; diff --git a/crates/context_servers/src/protocol.rs b/crates/context_servers/src/protocol.rs index 87da217f7d193..5e30eb4e3cbeb 100644 --- a/crates/context_servers/src/protocol.rs +++ b/crates/context_servers/src/protocol.rs @@ -11,8 +11,6 @@ use collections::HashMap; use crate::client::Client; use crate::types; -pub use types::PromptInfo; - const PROTOCOL_VERSION: u32 = 1; pub struct ModelContextProtocol { @@ -26,7 +24,7 @@ impl ModelContextProtocol { pub async fn initialize( self, - client_info: types::EntityInfo, + client_info: types::Implementation, ) -> Result { let params = types::InitializeParams { protocol_version: PROTOCOL_VERSION, @@ -96,7 +94,7 @@ impl InitializedContextServerProtocol { } /// List the MCP prompts. - pub async fn list_prompts(&self) -> Result> { + pub async fn list_prompts(&self) -> Result> { self.check_capability(ServerCapability::Prompts)?; let response: types::PromptsListResponse = self diff --git a/crates/context_servers/src/types.rs b/crates/context_servers/src/types.rs index cd95ecd7adb36..04ac87c704d06 100644 --- a/crates/context_servers/src/types.rs +++ b/crates/context_servers/src/types.rs @@ -15,6 +15,7 @@ pub enum RequestType { PromptsGet, PromptsList, CompletionComplete, + Ping, } impl RequestType { @@ -30,6 +31,7 @@ impl RequestType { RequestType::PromptsGet => "prompts/get", RequestType::PromptsList => "prompts/list", RequestType::CompletionComplete => "completion/complete", + RequestType::Ping => "ping", } } } @@ -39,14 +41,15 @@ impl RequestType { pub struct InitializeParams { pub protocol_version: u32, pub capabilities: ClientCapabilities, - pub client_info: EntityInfo, + pub client_info: Implementation, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CallToolParams { pub name: String, - pub arguments: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option>, } #[derive(Debug, Serialize)] @@ -77,6 +80,7 @@ pub struct LoggingSetLevelParams { #[serde(rename_all = "camelCase")] pub struct PromptsGetParams { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option>, } @@ -101,6 +105,13 @@ pub struct PromptReference { pub name: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResourceReference { + pub r#type: PromptReferenceType, + pub uri: Url, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum PromptReferenceType { @@ -110,13 +121,6 @@ pub enum PromptReferenceType { Resource, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ResourceReference { - pub r#type: String, - pub uri: String, -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionArgument { @@ -129,7 +133,7 @@ pub struct CompletionArgument { pub struct InitializeResponse { pub protocol_version: u32, pub capabilities: ServerCapabilities, - pub server_info: EntityInfo, + pub server_info: Implementation, } #[derive(Debug, Deserialize)] @@ -141,13 +145,39 @@ pub struct ResourcesReadResponse { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResourcesListResponse { + #[serde(skip_serializing_if = "Option::is_none")] pub resource_templates: Option>, - pub resources: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub resources: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SamplingMessage { + pub role: SamplingRole, + pub content: SamplingContent, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SamplingRole { + User, + Assistant, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum SamplingContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image")] + Image { data: String, mime_type: String }, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PromptsGetResponse { + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub prompt: String, } @@ -155,7 +185,7 @@ pub struct PromptsGetResponse { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PromptsListResponse { - pub prompts: Vec, + pub prompts: Vec, } #[derive(Debug, Deserialize)] @@ -168,61 +198,91 @@ pub struct CompletionCompleteResponse { #[serde(rename_all = "camelCase")] pub struct CompletionResult { pub values: Vec, + #[serde(skip_serializing_if = "Option::is_none")] pub total: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub has_more: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct PromptInfo { +pub struct Prompt { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option>, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PromptArgument { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub required: Option, } -// Shared Types - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] pub experimental: Option>, - pub sampling: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sampling: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ServerCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] pub experimental: Option>, - pub logging: Option>, - pub prompts: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub logging: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prompts: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, - pub tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PromptsCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] + pub list_changed: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResourcesCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] pub subscribe: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub list_changed: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolsCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] + pub list_changed: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Tool { pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub input_schema: serde_json::Value, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntityInfo { +pub struct Implementation { pub name: String, pub version: String, } @@ -231,6 +291,10 @@ pub struct EntityInfo { #[serde(rename_all = "camelCase")] pub struct Resource { pub uri: Url, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub mime_type: Option, } @@ -238,17 +302,23 @@ pub struct Resource { #[serde(rename_all = "camelCase")] pub struct ResourceContent { pub uri: Url, + #[serde(skip_serializing_if = "Option::is_none")] pub mime_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, - pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub blob: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResourceTemplate { pub uri_template: String, - pub name: Option, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -260,13 +330,16 @@ pub enum LoggingLevel { Error, } -// Client Notifications - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub enum NotificationType { Initialized, Progress, + Message, + ResourcesUpdated, + ResourcesListChanged, + ToolsListChanged, + PromptsListChanged, } impl NotificationType { @@ -274,6 +347,11 @@ impl NotificationType { match self { NotificationType::Initialized => "notifications/initialized", NotificationType::Progress => "notifications/progress", + NotificationType::Message => "notifications/message", + NotificationType::ResourcesUpdated => "notifications/resources/updated", + NotificationType::ResourcesListChanged => "notifications/resources/list_changed", + NotificationType::ToolsListChanged => "notifications/tools/list_changed", + NotificationType::PromptsListChanged => "notifications/prompts/list_changed", } } } @@ -288,12 +366,13 @@ pub enum ClientNotification { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProgressParams { - pub progress_token: String, + pub progress_token: ProgressToken, pub progress: f64, + #[serde(skip_serializing_if = "Option::is_none")] pub total: Option, } -// Helper Types that don't map directly to the protocol +pub type ProgressToken = String; pub enum CompletionTotal { Exact(u32), From c330135d72b2844e06251ea21136e9d1efecb612 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 2 Sep 2024 17:33:24 +0100 Subject: [PATCH 02/49] context_servers: Remove id from notifications JSON RPC notification do not have to contain an id. We do not generate or use id's to ensure we don't rely on them being there. --- crates/context_servers/src/client.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/context_servers/src/client.rs b/crates/context_servers/src/client.rs index aff186b115672..022a746c558cf 100644 --- a/crates/context_servers/src/client.rs +++ b/crates/context_servers/src/client.rs @@ -26,7 +26,7 @@ const JSON_RPC_VERSION: &str = "2.0"; const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); type ResponseHandler = Box)>; -type NotificationHandler = Box; +type NotificationHandler = Box; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(untagged)] @@ -94,7 +94,6 @@ enum CspResult { #[derive(Serialize, Deserialize)] struct Notification<'a, T> { jsonrpc: &'static str, - id: RequestId, #[serde(borrow)] method: &'a str, params: T, @@ -103,7 +102,6 @@ struct Notification<'a, T> { #[derive(Debug, Clone, Deserialize)] struct AnyNotification<'a> { jsonrpc: &'a str, - id: RequestId, method: String, #[serde(default)] params: Option, @@ -246,11 +244,7 @@ impl Client { if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) { - handler( - notification.id, - notification.params.unwrap_or(Value::Null), - cx.clone(), - ); + handler(notification.params.unwrap_or(Value::Null), cx.clone()); } } } @@ -378,10 +372,8 @@ impl Client { /// Sends a notification to the context server without expecting a response. /// This function serializes the notification and sends it through the outbound channel. pub fn notify(&self, method: &str, params: impl Serialize) -> Result<()> { - let id = self.next_id.fetch_add(1, SeqCst); let notification = serde_json::to_string(&Notification { jsonrpc: JSON_RPC_VERSION, - id: RequestId::Int(id), method, params, }) @@ -396,7 +388,7 @@ impl Client { { self.notification_handlers .lock() - .insert(method, Box::new(move |_, params, cx| f(params, cx))); + .insert(method, Box::new(move |params, cx| f(params, cx))); } pub fn name(&self) -> &str { From 8fa202be2ec40e7b81f35983be76eb44b29384a2 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 11 Oct 2024 00:07:36 +0100 Subject: [PATCH 03/49] context_servers: Add a method to list resources similarly to list_prompts --- crates/context_servers/src/protocol.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/context_servers/src/protocol.rs b/crates/context_servers/src/protocol.rs index 5e30eb4e3cbeb..451db56ef31df 100644 --- a/crates/context_servers/src/protocol.rs +++ b/crates/context_servers/src/protocol.rs @@ -105,6 +105,18 @@ impl InitializedContextServerProtocol { Ok(response.prompts) } + /// List the MCP resources. + pub async fn list_resources(&self) -> Result { + self.check_capability(ServerCapability::Resources)?; + + let response: types::ResourcesListResponse = self + .inner + .request(types::RequestType::ResourcesList.as_str(), ()) + .await?; + + Ok(response) + } + /// Executes a prompt with the given arguments and returns the result. pub async fn run_prompt>( &self, From 525c8381990639084ebf2978b839b8a067f293d1 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 11 Oct 2024 00:31:25 +0100 Subject: [PATCH 04/49] context_servers: Pass prompt description as description/menu_text --- .../src/slash_command/context_server_command.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 45feedba87a0f..ee2221c041fde 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -36,11 +36,17 @@ impl SlashCommand for ContextServerSlashCommand { } fn description(&self) -> String { - format!("Run context server command: {}", self.prompt.name) + match &self.prompt.description { + Some(desc) => desc.clone(), + None => format!("Run '{}' from {}", self.prompt.name, self.server_id), + } } fn menu_text(&self) -> String { - format!("Run '{}' from {}", self.prompt.name, self.server_id) + match &self.prompt.description { + Some(desc) => desc.clone(), + None => format!("Run '{}' from {}", self.prompt.name, self.server_id), + } } fn requires_argument(&self) -> bool { From 9426903b9d2d71fe77b0769f8390cc22a620452d Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 11 Oct 2024 19:47:51 +0100 Subject: [PATCH 05/49] context_servers: Show argument as a label Unlike normal commands which usually always have autocompletion, context server commands often don't. This makes it hard to understand what can be passed, particularly as commands change often and depending on config. We hence show the name of the argument now as a label (arguable all commands could benefit from that). --- .../src/slash_command/context_server_command.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index ee2221c041fde..3db057d07494c 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -1,3 +1,4 @@ +use super::create_label_for_command; use anyhow::{anyhow, Result}; use assistant_slash_command::{ AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput, @@ -8,7 +9,7 @@ use context_servers::{ manager::{ContextServer, ContextServerManager}, types::Prompt, }; -use gpui::{Task, WeakView, WindowContext}; +use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -35,6 +36,16 @@ impl SlashCommand for ContextServerSlashCommand { self.prompt.name.clone() } + fn label(&self, cx: &AppContext) -> language::CodeLabel { + let mut parts = vec![self.prompt.name.as_str()]; + if let Some(args) = &self.prompt.arguments { + if let Some(arg) = args.first() { + parts.push(arg.name.as_str()); + } + } + create_label_for_command(&parts[0], &parts[1..], cx) + } + fn description(&self) -> String { match &self.prompt.description { Some(desc) => desc.clone(), From 23e8842724431e1b113765d7c62c2134c8d59520 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Fri, 11 Oct 2024 20:03:45 +0100 Subject: [PATCH 06/49] context_servers: Make clippy happy --- crates/context_servers/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/context_servers/src/client.rs b/crates/context_servers/src/client.rs index 022a746c558cf..6681023c00871 100644 --- a/crates/context_servers/src/client.rs +++ b/crates/context_servers/src/client.rs @@ -382,13 +382,13 @@ impl Client { Ok(()) } - pub fn on_notification(&self, method: &'static str, mut f: F) + pub fn on_notification(&self, method: &'static str, f: F) where F: 'static + Send + FnMut(Value, AsyncAppContext), { self.notification_handlers .lock() - .insert(method, Box::new(move |params, cx| f(params, cx))); + .insert(method, Box::new(f)); } pub fn name(&self) -> &str { From c9c7412429fed53ae43cabc3ba1c4964b633ab1e Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 15 Oct 2024 16:10:04 +0100 Subject: [PATCH 07/49] slash_commands: Introduce stream based slash commands This commit replaces the existing SlashCommandOutput with a stream based approach. This allows for slash commands to (1) stream results if needed (2) produce multi-turn messages, e.g. asssistant or system messages (3) will allow for intermediate progress updates during slash command execution. The commit does the following * Introduce `SlashOutputEvent` as events that slash commands can emit. * Introduce `SlashCommandResult` as an alias to a boxed stream of events. * Move all commands to the new command result format. --- Cargo.lock | 2 + crates/assistant/src/assistant_panel.rs | 4 +- crates/assistant/src/context.rs | 336 ++++++++++++------ crates/assistant/src/context/context_tests.rs | 6 +- crates/assistant/src/slash_command.rs | 98 ++++- .../src/slash_command/auto_command.rs | 22 +- .../slash_command/cargo_workspace_command.rs | 28 +- .../slash_command/context_server_command.rs | 39 +- .../src/slash_command/default_command.rs | 28 +- .../src/slash_command/delta_command.rs | 44 +-- .../src/slash_command/diagnostics_command.rs | 130 ++++--- .../src/slash_command/docs_command.rs | 37 +- .../src/slash_command/fetch_command.rs | 29 +- .../src/slash_command/file_command.rs | 225 +++--------- .../src/slash_command/now_command.rs | 25 +- .../src/slash_command/project_command.rs | 76 ++-- .../src/slash_command/prompt_command.rs | 30 +- .../src/slash_command/search_command.rs | 74 ++-- .../src/slash_command/symbols_command.rs | 28 +- .../src/slash_command/tab_command.rs | 23 +- .../src/slash_command/terminal_command.rs | 26 +- .../src/slash_command/workflow_command.rs | 25 +- crates/assistant_slash_command/Cargo.toml | 2 + .../src/assistant_slash_command.rs | 40 ++- .../extension/src/extension_slash_command.rs | 49 ++- 25 files changed, 834 insertions(+), 592 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fb0c1ad6dc8e..f9a3a87f78688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,8 +452,10 @@ dependencies = [ "anyhow", "collections", "derive_more", + "futures 0.3.30", "gpui", "language", + "language_model", "parking_lot", "serde", "serde_json", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 943cbbb29ff01..856f7485a4672 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -4,10 +4,10 @@ use crate::{ prompt_library::open_prompt_library, prompts::PromptBuilder, slash_command::{ + codeblock_fence_for_path, default_command::DefaultSlashCommand, docs_command::{DocsSlashCommand, DocsSlashCommandArgs}, - file_command::{self, codeblock_fence_for_path}, - SlashCommandCompletionProvider, SlashCommandRegistry, + file_command, SlashCommandCompletionProvider, SlashCommandRegistry, }, slash_command_picker, terminal_inline_assistant::TerminalInlineAssistant, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 610652a371442..8d0d58edcc6d1 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,3 +1,41 @@ +// todo!() +// - implement run_commands_in_text +// - When slash command wants to insert a message, but it wants to insert it after a message that has the same Role and it emits a `StartMessage { merge_same_roles: bool (name TBD) }`, we should ignore it +// - When a section ends, we should run the following code: +// // this.slash_command_output_sections +// // .extend(sections.iter().cloned()); +// // this.slash_command_output_sections +// // .sort_by(|a, b| a.range.cmp(&b.range, buffer)); + +// // let output_range = +// // buffer.anchor_after(start)..buffer.anchor_before(new_end); +// // this.finished_slash_commands.insert(command_id); + +// // ( +// // ContextOperation::SlashCommandFinished { +// // id: command_id, +// // output_range: output_range.clone(), +// // sections: sections.clone(), +// // version, +// // }, +// // ContextEvent::SlashCommandFinished { +// // output_range, +// // sections, +// // run_commands_in_output: output.run_commands_in_text, +// // expand_result, +// // }, +// // ) +// - Adapt all the commands to use the streaming API +// - Animation example: +// Icon::new(IconName::ArrowCircle) +// .size(IconSize::Small) +// .with_animation( +// "arrow-circle", +// Animation::new(Duration::from_secs(2)).repeat(), +// |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), +// ) +// .into_any_element(), + #[cfg(test)] mod context_tests; @@ -7,7 +45,7 @@ use crate::{ }; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ - SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, + SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, }; use assistant_tool::ToolRegistry; use client::{self, proto, telemetry::Telemetry}; @@ -41,7 +79,7 @@ use std::{ cmp::{self, max, Ordering}, fmt::Debug, iter, mem, - ops::Range, + ops::{Range, RangeBounds}, path::{Path, PathBuf}, str::FromStr as _, sync::Arc, @@ -51,6 +89,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use text::BufferSnapshot; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; +use workspace::ui::IconName; #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct ContextId(String); @@ -1784,7 +1823,7 @@ impl Context { pub fn insert_command_output( &mut self, command_range: Range, - output: Task>, + output: Task, ensure_trailing_newline: bool, expand_result: bool, cx: &mut ModelContext, @@ -1794,98 +1833,150 @@ impl Context { let insert_output_task = cx.spawn(|this, mut cx| { let command_range = command_range.clone(); async move { - let output = output.await; - this.update(&mut cx, |this, cx| match output { - Ok(mut output) => { - // Ensure section ranges are valid. - for section in &mut output.sections { - section.range.start = section.range.start.min(output.text.len()); - section.range.end = section.range.end.min(output.text.len()); - while !output.text.is_char_boundary(section.range.start) { - section.range.start -= 1; - } - while !output.text.is_char_boundary(section.range.end) { - section.range.end += 1; - } - } + let mut stream = output.await?; + + struct PendingSection { + start: language::Anchor, + icon: IconName, + label: SharedString, + metadata: Option, + } - // Ensure there is a newline after the last section. - if ensure_trailing_newline { - let has_newline_after_last_section = - output.sections.last().map_or(false, |last_section| { - output.text[last_section.range.end..].ends_with('\n') + let position = this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + let start = command_range.start.to_offset(buffer); + let end = command_range.end.to_offset(buffer); + buffer.edit([(start..end, "")], None, cx); + buffer.anchor_after(command_range.end) + }) + })?; + let mut finished_sections: Vec> = + Vec::new(); + let mut pending_section_stack: Vec = Vec::new(); + + while let Some(event) = stream.next().await { + match event { + SlashCommandEvent::StartMessage { role } => { + this.update(&mut cx, |this, cx| { + let offset = this + .buffer + .read_with(cx, |buffer, _cx| position.to_offset(buffer)); + this.insert_message_at_offset( + offset, + role, + MessageStatus::Pending, + cx, + ); + })?; + } + SlashCommandEvent::StartSection { + icon, + label, + metadata, + ensure_newline, + } => { + this.read_with(&cx, |this, cx| { + let buffer = this.buffer.read(cx); + log::info!("Slash command output section start: {:?}", position); + pending_section_stack.push(PendingSection { + start: buffer.anchor_before(position), + icon, + label, + metadata, }); - if !has_newline_after_last_section { - output.text.push('\n'); - } + })?; } - - let version = this.version.clone(); - let command_id = SlashCommandId(this.next_timestamp()); - let (operation, event) = 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); - - let mut sections = output - .sections - .into_iter() - .map(|section| SlashCommandOutputSection { - range: buffer.anchor_after(start + section.range.start) - ..buffer.anchor_before(start + section.range.end), - icon: section.icon, - label: section.label, - metadata: section.metadata, + SlashCommandEvent::Content { + text, + run_commands_in_text, + } => { + // assert!(!run_commands_in_text, "not yet implemented"); + + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + let text = if !text.ends_with('\n') { + text + "\n" + } else { + text + }; + buffer.edit([(position..position, text)], None, cx) }) - .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)); - - let output_range = - buffer.anchor_after(start)..buffer.anchor_before(new_end); - this.finished_slash_commands.insert(command_id); - - ( - ContextOperation::SlashCommandFinished { - id: command_id, - output_range: output_range.clone(), - sections: sections.clone(), - version, - }, - ContextEvent::SlashCommandFinished { - output_range, - sections, - run_commands_in_output: output.run_commands_in_text, - expand_result, - }, - ) - }); - - this.push_op(operation, cx); - cx.emit(event); - } - Err(error) => { - if let Some(pending_command) = - this.pending_command_for_position(command_range.start, cx) - { - pending_command.status = - PendingSlashCommandStatus::Error(error.to_string()); - cx.emit(ContextEvent::PendingSlashCommandsUpdated { - removed: vec![pending_command.source_range.clone()], - updated: vec![pending_command.clone()], - }); + })?; + } + SlashCommandEvent::Progress { message, complete } => { + todo!() + } + SlashCommandEvent::EndSection { metadata } => { + if let Some(pending_section) = pending_section_stack.pop() { + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + let start = pending_section.start; + let end = buffer.anchor_before(position); + + buffer.edit([(position..position, "\n")], None, cx); + log::info!("Slash command output section end: {:?}", end); + + let slash_command_output_section = + SlashCommandOutputSection { + range: start..end, + icon: pending_section.icon, + label: pending_section.label, + metadata: metadata.or(pending_section.metadata), + }; + finished_sections + .push(slash_command_output_section.clone()); + this.slash_command_output_sections + .push(slash_command_output_section); + }); + })?; + } } } + } + assert!(pending_section_stack.is_empty()); + this.update(&mut cx, |this, cx| { + let command_id = SlashCommandId(this.next_timestamp()); + this.finished_slash_commands.insert(command_id); + + let version = this.version.clone(); + let (op, ev) = this.buffer.update(cx, |buffer, cx| { + let start = command_range.start; + let output_range = start..position; + + this.slash_command_output_sections + .sort_by(|a, b| a.range.cmp(&b.range, buffer)); + finished_sections.sort_by(|a, b| a.range.cmp(&b.range, buffer)); + + // Remove the command range from the buffer + ( + ContextOperation::SlashCommandFinished { + id: command_id, + output_range: output_range.clone(), + sections: finished_sections.clone(), + version, + }, + ContextEvent::SlashCommandFinished { + output_range, + sections: finished_sections, + run_commands_in_output: false, + expand_result, + }, + ) + }); + + this.push_op(op, cx); + cx.emit(ev); }) - .ok(); } }); + let insert_output_task = cx.background_executor().spawn(async move { + if let Err(error) = insert_output_task.await { + log::error!("failed to run command: {:?}", error) + } + }); + + // We are inserting a pending command and update it. if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) { pending_command.status = PendingSlashCommandStatus::Running { _task: insert_output_task.shared(), @@ -2366,43 +2457,54 @@ impl Context { next_message_ix += 1; } - let start = self.buffer.update(cx, |buffer, cx| { - let offset = self - .message_anchors - .get(next_message_ix) - .map_or(buffer.len(), |message| { - buffer.clip_offset(message.start.to_offset(buffer) - 1, Bias::Left) - }); - buffer.edit([(offset..offset, "\n")], None, cx); - buffer.anchor_before(offset + 1) - }); - - let version = self.version.clone(); - let anchor = MessageAnchor { - id: MessageId(self.next_timestamp()), - start, - }; - let metadata = MessageMetadata { - role, - status, - timestamp: anchor.id.0, - cache: None, - }; - self.insert_message(anchor.clone(), metadata.clone(), cx); - self.push_op( - ContextOperation::InsertMessage { - anchor: anchor.clone(), - metadata, - version, - }, - cx, - ); - Some(anchor) + let buffer = self.buffer.read(cx); + let offset = self + .message_anchors + .get(next_message_ix) + .map_or(buffer.len(), |message| { + buffer.clip_offset(message.start.to_offset(buffer) - 1, Bias::Left) + }); + Some(self.insert_message_at_offset(offset, role, status, cx)) } else { None } } + fn insert_message_at_offset( + &mut self, + offset: usize, + role: Role, + status: MessageStatus, + cx: &mut ModelContext, + ) -> MessageAnchor { + let start = self.buffer.update(cx, |buffer, cx| { + buffer.edit([(offset..offset, "\n")], None, cx); + buffer.anchor_before(offset + 1) + }); + + let version = self.version.clone(); + let anchor = MessageAnchor { + id: MessageId(self.next_timestamp()), + start, + }; + let metadata = MessageMetadata { + role, + status, + timestamp: anchor.id.0, + cache: None, + }; + self.insert_message(anchor.clone(), metadata.clone(), cx); + self.push_op( + ContextOperation::InsertMessage { + anchor: anchor.clone(), + metadata, + version, + }, + cx, + ); + anchor + } + pub fn insert_content(&mut self, content: Content, cx: &mut ModelContext) { let buffer = self.buffer.read(cx); let insertion_ix = match self diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 2d6a2894c9521..ccbcd5311a04a 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, }; use collections::HashSet; @@ -1105,10 +1105,12 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std context.insert_command_output( command_range, Task::ready(Ok(SlashCommandOutput { + role: Some(Role::User), text: output_text, sections, run_commands_in_text: false, - })), + } + .into())), true, false, cx, diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index e430e35622a22..0d4df2b325878 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -1,22 +1,32 @@ use crate::assistant_panel::ContextEditor; use anyhow::Result; use assistant_slash_command::AfterCompletion; -pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry}; +pub use assistant_slash_command::{ + SlashCommand, SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, + SlashCommandResult, +}; use editor::{CompletionProvider, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext}; -use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint}; +use language::{ + Anchor, Buffer, BufferSnapshot, CodeLabel, Documentation, HighlightId, LanguageServerId, + LineEnding, ToPoint, +}; use parking_lot::{Mutex, RwLock}; use project::CompletionIntent; use rope::Point; +use serde::{Deserialize, Serialize}; use std::{ + fmt::Write, ops::Range, + ops::RangeInclusive, + path::Path, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, }, }; -use ui::ActiveTheme; +use ui::{ActiveTheme, IconName}; use workspace::Workspace; pub mod auto_command; pub mod cargo_workspace_command; @@ -433,3 +443,85 @@ pub fn create_label_for_command( label.filter_range = 0..command_name.len(); label } + +/// Creates a Markdown code fence with path and line range information. +/// +/// Given an optional file path and line range, this function returns a Markdown code block header, +/// including file extension syntax highlighting, full file path and line numbers if provided. +pub fn codeblock_fence_for_path( + path: Option<&Path>, + row_range: Option>, +) -> String { + let mut text = String::new(); + write!(text, "```").unwrap(); + + if let Some(path) = path { + if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { + write!(text, "{} ", extension).unwrap(); + } + + write!(text, "{}", path.display()).unwrap(); + } else { + write!(text, "untitled").unwrap(); + } + + if let Some(row_range) = row_range { + write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap(); + } + + text.push('\n'); + text +} + +#[derive(Serialize, Deserialize)] +pub struct FileCommandMetadata { + pub path: String, +} + +/// Converts a buffer's contents into a formatted Markdown code block. +pub fn buffer_to_output( + buffer: &BufferSnapshot, + path: Option<&Path>, +) -> Result> { + let mut events = Vec::new(); + + let mut content = buffer.text(); + LineEnding::normalize(&mut content); + + let fence = codeblock_fence_for_path(path, None); + + let label = path.map_or("untitled".to_string(), |p| p.to_string_lossy().to_string()); + + let metadata = path.and_then(|path| { + serde_json::to_value(FileCommandMetadata { + path: path.to_string_lossy().to_string(), + }) + .ok() + }); + + events.push(SlashCommandEvent::StartSection { + icon: IconName::File, + label: label.into(), + metadata, + ensure_newline: true, + }); + + let mut code_content = String::new(); + code_content.push_str(&fence); + code_content.push_str(&content); + if !code_content.ends_with('\n') { + code_content.push('\n'); + } + code_content.push_str("```\n"); + + events.push(SlashCommandEvent::Content { + text: code_content.into(), + run_commands_in_text: false, + }); + + events.push(SlashCommandEvent::EndSection { metadata: None }); + + // TODO: collect_buffer_diagnostics(events, buffer, false); + + Ok(events) +} diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index 14bbb7c8412b4..9503d8d151460 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -1,9 +1,10 @@ use super::create_label_for_command; -use super::{SlashCommand, SlashCommandOutput}; +use super::SlashCommand; use anyhow::{anyhow, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection}; use feature_flags::FeatureFlag; -use futures::StreamExt; +use futures::stream::BoxStream; +use futures::stream::{self, StreamExt}; use gpui::{AppContext, AsyncAppContext, Task, WeakView}; use language::{CodeLabel, LspAdapterDelegate}; use language_model::{ @@ -92,7 +93,7 @@ impl SlashCommand for AutoCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task>> { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); }; @@ -140,11 +141,14 @@ impl SlashCommand for AutoCommand { prompt.push('\n'); prompt.push_str(&original_prompt); - Ok(SlashCommandOutput { - text: prompt, - sections: Vec::new(), - run_commands_in_text: true, - }) + Ok(stream::iter(vec![ + SlashCommandEvent::StartMessage { role: Role::User }, + SlashCommandEvent::Content { + text: prompt, + run_commands_in_text: true, + }, + ]) + .boxed()) }) } } diff --git a/crates/assistant/src/slash_command/cargo_workspace_command.rs b/crates/assistant/src/slash_command/cargo_workspace_command.rs index baf16d7f014cb..6cc9c0fa42911 100644 --- a/crates/assistant/src/slash_command/cargo_workspace_command.rs +++ b/crates/assistant/src/slash_command/cargo_workspace_command.rs @@ -1,7 +1,10 @@ -use super::{SlashCommand, SlashCommandOutput}; +use super::SlashCommand; use anyhow::{anyhow, Context, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, +}; use fs::Fs; +use futures::stream::{self, StreamExt}; use gpui::{AppContext, Model, Task, WeakView}; use language::{BufferSnapshot, LspAdapterDelegate}; use project::{Project, ProjectPath}; @@ -123,7 +126,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let output = workspace.update(cx, |workspace, cx| { let project = workspace.project().clone(); let fs = workspace.project().read(cx).fs().clone(); @@ -135,17 +138,20 @@ impl SlashCommand for CargoWorkspaceSlashCommand { cx.foreground_executor().spawn(async move { let text = output.await?; - let range = 0..text.len(); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, + Ok(stream::iter(vec![ + SlashCommandEvent::StartSection { icon: IconName::FileTree, label: "Project".into(), metadata: None, - }], - run_commands_in_text: false, - }) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]) + .boxed()) }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 3db057d07494c..74a1eb189b3f8 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -1,8 +1,8 @@ use super::create_label_for_command; use anyhow::{anyhow, Result}; use assistant_slash_command::{ - AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput, - SlashCommandOutputSection, + as_stream_vec, AfterCompletion, ArgumentCompletion, Role, SlashCommand, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use collections::HashMap; use context_servers::{ @@ -14,7 +14,6 @@ use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use std::sync::atomic::AtomicBool; use std::sync::Arc; use text::LineEnding; -use ui::{IconName, SharedString}; use workspace::Workspace; pub struct ContextServerSlashCommand { @@ -128,7 +127,7 @@ impl SlashCommand for ContextServerSlashCommand { _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let server_id = self.server_id.clone(); let prompt_name = self.prompt.name.clone(); @@ -150,20 +149,30 @@ impl SlashCommand for ContextServerSlashCommand { // We must normalize the line endings here, since servers might return CR characters. LineEnding::normalize(&mut prompt); - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: 0..(prompt.len()), - icon: IconName::ZedAssistant, - label: SharedString::from( - result - .description - .unwrap_or(format!("Result from {}", prompt_name)), - ), + let mut events = Vec::new(); + events.push(SlashCommandEvent::StartMessage { + role: Role::Assistant, + }); + + if let Some(description) = result.description { + events.push(SlashCommandEvent::StartSection { + icon: ui::IconName::Info, + label: description.into(), metadata: None, - }], + ensure_newline: false, + }); + } + + events.push(SlashCommandEvent::Content { text: prompt, run_commands_in_text: false, - }) + }); + + if result.description.is_some() { + events.push(SlashCommandEvent::EndSection { metadata: None }); + } + + Ok(as_stream_vec(events)) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 4199840300a24..0fc9c8c6b6862 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -1,7 +1,10 @@ -use super::{SlashCommand, SlashCommandOutput}; +use super::SlashCommand; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, +}; +use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::{ @@ -48,7 +51,7 @@ impl SlashCommand for DefaultSlashCommand { _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let store = PromptStore::global(cx); cx.background_executor().spawn(async move { let store = store.await?; @@ -67,16 +70,21 @@ impl SlashCommand for DefaultSlashCommand { text.push('\n'); } - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: 0..text.len(), + let stream = stream::iter(vec![ + SlashCommandEvent::StartSection { icon: IconName::Library, label: "Default".into(), metadata: None, - }], - text, - run_commands_in_text: true, - }) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: true, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]); + + Ok(stream.boxed()) }) } } diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs index 6f697ecbb9bcb..5e1d6011b8611 100644 --- a/crates/assistant/src/slash_command/delta_command.rs +++ b/crates/assistant/src/slash_command/delta_command.rs @@ -1,10 +1,15 @@ -use crate::slash_command::file_command::{FileCommandMetadata, FileSlashCommand}; +use crate::slash_command::file_command::FileSlashCommand; +use crate::slash_command::FileCommandMetadata; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use collections::HashSet; -use futures::future; +use futures::{ + future, + stream::{self, StreamExt}, +}; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; @@ -48,7 +53,7 @@ impl SlashCommand for DeltaSlashCommand { workspace: WeakView, delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let mut paths = HashSet::default(); let mut file_command_old_outputs = Vec::new(); let mut file_command_new_outputs = Vec::new(); @@ -77,33 +82,32 @@ impl SlashCommand for DeltaSlashCommand { } cx.background_executor().spawn(async move { - let mut output = SlashCommandOutput::default(); + let mut events = Vec::new(); let file_command_new_outputs = future::join_all(file_command_new_outputs).await; for (old_text, new_output) in file_command_old_outputs .into_iter() .zip(file_command_new_outputs) { - if let Ok(new_output) = new_output { - if let Some(file_command_range) = new_output.sections.first() { - let new_text = &new_output.text[file_command_range.range.clone()]; - if old_text.chars().ne(new_text.chars()) { - output.sections.extend(new_output.sections.into_iter().map( - |section| SlashCommandOutputSection { - range: output.text.len() + section.range.start - ..output.text.len() + section.range.end, - icon: section.icon, - label: section.label, - metadata: section.metadata, - }, - )); - output.text.push_str(&new_output.text); + if let Ok(new_events) = new_output { + let new_content = stream::StreamExt::collect::>(new_events).await; + { + if let Some(first_content) = new_content.iter().find_map(|event| { + if let SlashCommandEvent::Content { text, .. } = event { + Some(text) + } else { + None + } + }) { + if old_text.chars().ne(first_content.chars()) { + events.extend(new_content); + } } } } } - Ok(output) + Ok(stream::iter(events).boxed()) }) } } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index 146a4e5d366dd..a3ccca0bfff70 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -1,6 +1,9 @@ -use super::{create_label_for_command, SlashCommand, SlashCommandOutput}; +use super::{create_label_for_command, SlashCommand}; use anyhow::{anyhow, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, +}; +use futures::stream::{self, BoxStream, StreamExt}; use fuzzy::{PathMatch, StringMatchCandidate}; use gpui::{AppContext, Model, Task, View, WeakView}; use language::{ @@ -167,7 +170,7 @@ impl SlashCommand for DiagnosticsSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow!("workspace was dropped"))); }; @@ -176,7 +179,12 @@ impl SlashCommand for DiagnosticsSlashCommand { let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx); - cx.spawn(move |_| async move { task.await?.ok_or_else(|| anyhow!("No diagnostics found")) }) + cx.spawn(move |_| async move { + match task.await? { + Some(stream) => Ok(stream), + None => Err(anyhow!("No diagnostics found")), + } + }) } } @@ -217,7 +225,7 @@ fn collect_diagnostics( project: Model, options: Options, cx: &mut AppContext, -) -> Task>> { +) -> Task>>> { let error_source = if let Some(path_matcher) = &options.path_matcher { debug_assert_eq!(path_matcher.sources().len(), 1); Some(path_matcher.sources().first().cloned().unwrap_or_default()) @@ -258,12 +266,18 @@ fn collect_diagnostics( .collect(); cx.spawn(|mut cx| async move { - let mut output = SlashCommandOutput::default(); + let mut events = Vec::new(); if let Some(error_source) = error_source.as_ref() { - writeln!(output.text, "diagnostics: {}", error_source).unwrap(); + events.push(SlashCommandEvent::Content { + text: format!("diagnostics: {}\n", error_source), + run_commands_in_text: false, + }); } else { - writeln!(output.text, "diagnostics").unwrap(); + events.push(SlashCommandEvent::Content { + text: "diagnostics\n".to_string(), + run_commands_in_text: false, + }); } let mut project_summary = DiagnosticSummary::default(); @@ -281,10 +295,19 @@ fn collect_diagnostics( continue; } - let last_end = output.text.len(); let file_path = path.to_string_lossy().to_string(); if !glob_is_exact_file_match { - writeln!(&mut output.text, "{file_path}").unwrap(); + events.push(SlashCommandEvent::StartSection { + icon: IconName::File, + label: file_path.clone().into(), + metadata: None, + ensure_newline: false, + }); + events.push(SlashCommandEvent::Content { + text: format!("{}\n", file_path), + run_commands_in_text: false, + }); + events.push(SlashCommandEvent::EndSection { metadata: None }); } if let Some(buffer) = project_handle @@ -293,21 +316,15 @@ fn collect_diagnostics( .log_err() { let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?; - collect_buffer_diagnostics(&mut output, &snapshot, options.include_warnings); - } - - if !glob_is_exact_file_match { - output.sections.push(SlashCommandOutputSection { - range: last_end..output.text.len().saturating_sub(1), - icon: IconName::File, - label: file_path.into(), - metadata: None, - }); + events.extend(collect_buffer_diagnostics( + &snapshot, + options.include_warnings, + )); } } // No diagnostics found - if output.sections.is_empty() { + if events.is_empty() { return Ok(None); } @@ -332,51 +349,52 @@ fn collect_diagnostics( } } - output.sections.insert( + events.insert( 0, - SlashCommandOutputSection { - range: 0..output.text.len(), + SlashCommandEvent::StartSection { icon: IconName::Warning, label: label.into(), metadata: None, + ensure_newline: false, }, ); - Ok(Some(output)) + Ok(Some(futures::stream::iter(events).boxed())) }) } pub fn collect_buffer_diagnostics( - output: &mut SlashCommandOutput, snapshot: &BufferSnapshot, include_warnings: bool, -) { - for (_, group) in snapshot.diagnostic_groups(None) { - let entry = &group.entries[group.primary_ix]; - collect_diagnostic(output, entry, &snapshot, include_warnings) - } +) -> Vec { + snapshot + .diagnostic_groups(None) + .into_iter() + .flat_map(|(_, group)| { + let entry = &group.entries[group.primary_ix]; + collect_diagnostic(entry, snapshot, include_warnings) + }) + .collect() } fn collect_diagnostic( - output: &mut SlashCommandOutput, entry: &DiagnosticEntry, snapshot: &BufferSnapshot, include_warnings: bool, -) { +) -> Vec { const EXCERPT_EXPANSION_SIZE: u32 = 2; const MAX_MESSAGE_LENGTH: usize = 2000; let (ty, icon) = match entry.diagnostic.severity { DiagnosticSeverity::WARNING => { if !include_warnings { - return; + return vec![]; } ("warning", IconName::Warning) } DiagnosticSeverity::ERROR => ("error", IconName::XCircle), - _ => return, + _ => return vec![], }; - let prev_len = output.text.len(); let range = entry.range.to_point(snapshot); let diagnostic_row_number = range.start.row + 1; @@ -386,11 +404,11 @@ fn collect_diagnostic( let excerpt_range = Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot); - output.text.push_str("```"); + let mut text = String::from("```"); if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) { - output.text.push_str(&language_name); + text.push_str(&language_name); } - output.text.push('\n'); + text.push('\n'); let mut buffer_text = String::new(); for chunk in snapshot.text_for_range(excerpt_range) { @@ -399,26 +417,34 @@ fn collect_diagnostic( for (i, line) in buffer_text.lines().enumerate() { let line_number = start_row + i as u32 + 1; - writeln!(output.text, "{}", line).unwrap(); + writeln!(text, "{}", line).unwrap(); if line_number == diagnostic_row_number { - output.text.push_str("//"); - let prev_len = output.text.len(); - write!(output.text, " {}: ", ty).unwrap(); - let padding = output.text.len() - prev_len; + text.push_str("//"); + let prev_len = text.len(); + write!(text, " {}: ", ty).unwrap(); + let padding = text.len() - prev_len; let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH) .replace('\n', format!("\n//{:padding$}", "").as_str()); - writeln!(output.text, "{message}").unwrap(); + writeln!(text, "{message}").unwrap(); } } - writeln!(output.text, "```").unwrap(); - output.sections.push(SlashCommandOutputSection { - range: prev_len..output.text.len().saturating_sub(1), - icon, - label: entry.diagnostic.message.clone().into(), - metadata: None, - }); + writeln!(text, "```").unwrap(); + + vec![ + SlashCommandEvent::StartSection { + icon, + label: entry.diagnostic.message.clone().into(), + metadata: None, + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ] } diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index 399ede9d99954..3548d59ce22b0 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -5,8 +5,10 @@ use std::time::Duration; use anyhow::{anyhow, bail, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; +use futures::stream::StreamExt; use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView}; use indexed_docs::{ DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName, @@ -274,7 +276,9 @@ impl SlashCommand for DocsSlashCommand { _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { + use futures::stream::{self, StreamExt}; + if arguments.is_empty() { return Task::ready(Err(anyhow!("missing an argument"))); }; @@ -343,19 +347,22 @@ impl SlashCommand for DocsSlashCommand { cx.foreground_executor().spawn(async move { let (provider, text, ranges) = task.await?; - Ok(SlashCommandOutput { - text, - sections: ranges - .into_iter() - .map(|(key, range)| SlashCommandOutputSection { - range, - icon: IconName::FileDoc, - label: format!("docs ({provider}): {key}",).into(), - metadata: None, - }) - .collect(), - run_commands_in_text: false, - }) + + let events = vec![ + SlashCommandEvent::StartSection { + icon: IconName::FileDoc, + label: format!("docs ({provider})").into(), + metadata: None, + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]; + + Ok(stream::iter(events).boxed()) }) } } diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 3a01bb645a36b..f9d163c3cae80 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -5,9 +5,13 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, +}; +use futures::{ + stream::{self, StreamExt}, + AsyncReadExt, }; -use futures::AsyncReadExt; use gpui::{Task, WeakView}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; @@ -133,7 +137,7 @@ impl SlashCommand for FetchSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let Some(argument) = arguments.first() else { return Task::ready(Err(anyhow!("missing URL"))); }; @@ -156,17 +160,20 @@ impl SlashCommand for FetchSlashCommand { bail!("no textual content found"); } - let range = 0..text.len(); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, + Ok(stream::iter(vec![ + SlashCommandEvent::StartSection { icon: IconName::AtSign, label: format!("fetch {}", url).into(), metadata: None, - }], - run_commands_in_text: false, - }) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]) + .boxed()) }) } } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 6da56d064178a..7633356b6d5fc 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -1,14 +1,17 @@ -use super::{diagnostics_command::collect_buffer_diagnostics, SlashCommand, SlashCommandOutput}; +use super::{ + buffer_to_output, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, +}; +// use super::diagnostics_command::collect_buffer_diagnostics; use anyhow::{anyhow, Context as _, Result}; -use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{AfterCompletion, ArgumentCompletion}; +use futures::stream::{self, StreamExt}; use fuzzy::PathMatch; use gpui::{AppContext, Model, Task, View, WeakView}; use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate}; -use project::{PathMatchCandidateSet, Project}; +use project::{Entry, PathMatchCandidateSet, Project}; use serde::{Deserialize, Serialize}; use std::{ - fmt::Write, - ops::{Range, RangeInclusive}, path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, }; @@ -181,7 +184,7 @@ impl SlashCommand for FileSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow!("workspace was dropped"))); }; @@ -198,7 +201,7 @@ fn collect_files( project: Model, glob_inputs: &[String], cx: &mut AppContext, -) -> Task> { +) -> Task { let Ok(matchers) = glob_inputs .into_iter() .map(|glob_input| { @@ -218,12 +221,12 @@ fn collect_files( .collect::>(); cx.spawn(|mut cx| async move { - let mut output = SlashCommandOutput::default(); + let mut events = Vec::new(); for snapshot in snapshots { let worktree_id = snapshot.id(); - let mut directory_stack: Vec<(Arc, String, usize)> = Vec::new(); - let mut folded_directory_names_stack = Vec::new(); + let mut folded_directory_names = Vec::new(); let mut is_top_level_directory = true; + let mut directory_stack = Vec::new(); for entry in snapshot.entries(false, 0) { let mut path_including_worktree_name = PathBuf::new(); @@ -237,19 +240,6 @@ fn collect_files( continue; } - while let Some((dir, _, _)) = directory_stack.last() { - if entry.path.starts_with(dir) { - break; - } - let (_, entry_name, start) = directory_stack.pop().unwrap(); - output.sections.push(build_entry_output_section( - start..output.text.len().saturating_sub(1), - Some(&PathBuf::from(entry_name)), - true, - None, - )); - } - let filename = entry .path .file_name() @@ -258,158 +248,85 @@ fn collect_files( .unwrap_or_default() .to_string(); + while !directory_stack.is_empty() + && !entry.path.starts_with(directory_stack.last().unwrap()) + { + log::info!("end dir"); + directory_stack.pop(); + events.push(SlashCommandEvent::EndSection { metadata: None }); + } + if entry.is_dir() { - // Auto-fold directories that contain no files + log::info!("start dir"); let mut child_entries = snapshot.child_entries(&entry.path); if let Some(child) = child_entries.next() { if child_entries.next().is_none() && child.kind.is_dir() { if is_top_level_directory { is_top_level_directory = false; - folded_directory_names_stack.push( + folded_directory_names.push( path_including_worktree_name.to_string_lossy().to_string(), ); } else { - folded_directory_names_stack.push(filename.to_string()); + folded_directory_names.push(filename.to_string()) } - continue; } } else { // Skip empty directories - folded_directory_names_stack.clear(); + folded_directory_names.clear(); continue; } - let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/"); - let entry_start = output.text.len(); - if prefix_paths.is_empty() { + + let prefix_paths = folded_directory_names.drain(..).as_slice().join("/"); + let dirname = if prefix_paths.is_empty() { if is_top_level_directory { - output - .text - .push_str(&path_including_worktree_name.to_string_lossy()); is_top_level_directory = false; + path_including_worktree_name.to_string_lossy().to_string() } else { - output.text.push_str(&filename); + filename } - directory_stack.push((entry.path.clone(), filename, entry_start)); } else { - let entry_name = format!("{}/{}", prefix_paths, &filename); - output.text.push_str(&entry_name); - directory_stack.push((entry.path.clone(), entry_name, entry_start)); - } - output.text.push('\n'); + format!("{}/{}", prefix_paths, &filename) + }; + events.push(SlashCommandEvent::StartSection { + icon: IconName::Folder, + label: dirname.clone().into(), + metadata: None, + ensure_newline: true, + }); + events.push(SlashCommandEvent::Content { + text: dirname, + run_commands_in_text: false, + }); + directory_stack.push(entry.path.clone()); } else if entry.is_file() { - let Some(open_buffer_task) = project_handle + let open_buffer_task = project_handle .update(&mut cx, |project, cx| { project.open_buffer((worktree_id, &entry.path), cx) }) - .ok() - else { + .ok(); + let Some(open_buffer_task) = open_buffer_task else { continue; }; if let Some(buffer) = open_buffer_task.await.log_err() { let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; - append_buffer_to_output( - &snapshot, - Some(&path_including_worktree_name), - &mut output, - ) - .log_err(); + let mut events_from_buffer = + buffer_to_output(&snapshot, Some(&path_including_worktree_name))?; + events.append(&mut events_from_buffer); } } } - while let Some((dir, entry, start)) = directory_stack.pop() { - if directory_stack.is_empty() { - let mut root_path = PathBuf::new(); - root_path.push(snapshot.root_name()); - root_path.push(&dir); - output.sections.push(build_entry_output_section( - start..output.text.len(), - Some(&root_path), - true, - None, - )); - } else { - output.sections.push(build_entry_output_section( - start..output.text.len(), - Some(&PathBuf::from(entry.as_str())), - true, - None, - )); - } + // Close any remaining open directories + while !directory_stack.is_empty() { + log::info!("end dir"); + directory_stack.pop(); + events.push(SlashCommandEvent::EndSection { metadata: None }); } } - Ok(output) + Ok(stream::iter(events).boxed()) }) } -pub fn codeblock_fence_for_path( - path: Option<&Path>, - row_range: Option>, -) -> String { - let mut text = String::new(); - write!(text, "```").unwrap(); - - if let Some(path) = path { - if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { - write!(text, "{} ", extension).unwrap(); - } - - write!(text, "{}", path.display()).unwrap(); - } else { - write!(text, "untitled").unwrap(); - } - - if let Some(row_range) = row_range { - write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap(); - } - - text.push('\n'); - text -} - -#[derive(Serialize, Deserialize)] -pub struct FileCommandMetadata { - pub path: String, -} - -pub fn build_entry_output_section( - range: Range, - 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(), - metadata: if is_directory { - None - } else { - path.and_then(|path| { - serde_json::to_value(FileCommandMetadata { - path: path.to_string_lossy().to_string(), - }) - .ok() - }) - }, - } -} - /// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix /// check. Only subpaths pass the prefix check, rather than any prefix. mod custom_path_matcher { @@ -492,36 +409,6 @@ mod custom_path_matcher { } } -pub fn append_buffer_to_output( - buffer: &BufferSnapshot, - path: Option<&Path>, - output: &mut SlashCommandOutput, -) -> Result<()> { - let prev_len = output.text.len(); - - let mut content = buffer.text(); - LineEnding::normalize(&mut content); - output.text.push_str(&codeblock_fence_for_path(path, None)); - output.text.push_str(&content); - if !output.text.ends_with('\n') { - output.text.push('\n'); - } - output.text.push_str("```"); - output.text.push('\n'); - - let section_ix = output.sections.len(); - collect_buffer_diagnostics(output, buffer, false); - - output.sections.insert( - section_ix, - build_entry_output_section(prev_len..output.text.len(), path, false, None), - ); - - output.text.push('\n'); - - Ok(()) -} - #[cfg(test)] mod test { use fs::FakeFs; @@ -716,7 +603,7 @@ mod test { assert_eq!(result.sections[6].label, "summercamp"); assert_eq!(result.sections[7].label, "zed/assets/themes"); - // Ensure that the project lasts until after the last await + // Ensure that the project lasts until after the last awai drop(project); } } diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 221ba05cafc62..0f1e91dd96c14 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -3,9 +3,11 @@ use std::sync::Arc; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use chrono::Local; +use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; @@ -48,20 +50,23 @@ impl SlashCommand for NowSlashCommand { _workspace: WeakView, _delegate: Option>, _cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let now = Local::now(); let text = format!("Today is {now}.", now = now.to_rfc2822()); - let range = 0..text.len(); - Task::ready(Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, + Task::ready(Ok(stream::iter(vec![ + SlashCommandEvent::StartSection { icon: IconName::CountdownTimer, label: now.to_rfc2822().into(), metadata: None, - }], - run_commands_in_text: false, - })) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]) + .boxed())) } } diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 58fef8f338771..d3cc098c7a651 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -1,24 +1,17 @@ -use super::{ - create_label_for_command, search_command::add_search_result_section, SlashCommand, - SlashCommandOutput, -}; +use super::{create_label_for_command, SlashCommand}; use crate::PromptBuilder; use anyhow::{anyhow, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, +}; use feature_flags::FeatureFlag; +use futures::stream::{self, StreamExt}; use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{Anchor, CodeLabel, LspAdapterDelegate}; use language_model::{LanguageModelRegistry, LanguageModelTool}; use schemars::JsonSchema; use semantic_index::SemanticDb; use serde::Deserialize; - -pub struct ProjectSlashCommandFeatureFlag; - -impl FeatureFlag for ProjectSlashCommandFeatureFlag { - const NAME: &'static str = "project-slash-command"; -} - use std::{ fmt::Write as _, ops::DerefMut, @@ -27,6 +20,12 @@ use std::{ use ui::{BorrowAppContext as _, IconName}; use workspace::Workspace; +pub struct ProjectSlashCommandFeatureFlag; + +impl FeatureFlag for ProjectSlashCommandFeatureFlag { + const NAME: &'static str = "project-slash-command"; +} + pub struct ProjectSlashCommand { prompt_builder: Arc, } @@ -76,7 +75,7 @@ impl SlashCommand for ProjectSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let model_registry = LanguageModelRegistry::read_global(cx); let current_model = model_registry.active_model(); let prompt_builder = self.prompt_builder.clone(); @@ -125,44 +124,49 @@ impl SlashCommand for ProjectSlashCommand { cx.background_executor() .spawn(async move { + let mut events = Vec::new(); + events.push(SlashCommandEvent::StartSection { + icon: IconName::Book, + label: "Project context".into(), + metadata: None, + ensure_newline: false, + }); + let mut output = "Project context:\n".to_string(); - let mut sections = Vec::new(); + events.push(SlashCommandEvent::Content { + text: output.clone(), + run_commands_in_text: true, + }); for (ix, query) in search_queries.into_iter().enumerate() { - let start_ix = output.len(); - writeln!(&mut output, "Results for {query}:").unwrap(); let mut has_results = false; + events.push(SlashCommandEvent::StartSection { + icon: IconName::MagnifyingGlass, + label: query.clone().into(), + metadata: None, + ensure_newline: false, + }); + + let mut section_text = format!("Results for {query}:\n"); for result in &results { if result.query_index == ix { - add_search_result_section(result, &mut output, &mut sections); + writeln!(&mut section_text, "{}", result.excerpt_content).unwrap(); has_results = true; } } + if has_results { - sections.push(SlashCommandOutputSection { - range: start_ix..output.len(), - icon: IconName::MagnifyingGlass, - label: query.into(), - metadata: None, + events.push(SlashCommandEvent::Content { + text: section_text, + run_commands_in_text: true, }); - output.push('\n'); - } else { - output.truncate(start_ix); + events.push(SlashCommandEvent::EndSection { metadata: None }); } } - sections.push(SlashCommandOutputSection { - range: 0..output.len(), - icon: IconName::Book, - label: "Project context".into(), - metadata: None, - }); + events.push(SlashCommandEvent::EndSection { metadata: None }); - Ok(SlashCommandOutput { - text: output, - sections, - run_commands_in_text: true, - }) + Ok(stream::iter(events).boxed()) }) .await }) diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 978c6d7504cae..0d5de0ad05b60 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -1,7 +1,10 @@ -use super::{SlashCommand, SlashCommandOutput}; +use super::SlashCommand; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Context, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, +}; +use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; @@ -61,7 +64,7 @@ impl SlashCommand for PromptSlashCommand { _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let title = arguments.to_owned().join(" "); if title.trim().is_empty() { return Task::ready(Err(anyhow!("missing prompt name"))); @@ -90,17 +93,22 @@ impl SlashCommand for PromptSlashCommand { if prompt.is_empty() { prompt.push('\n'); } - let range = 0..prompt.len(); - Ok(SlashCommandOutput { - text: prompt, - sections: vec![SlashCommandOutputSection { - range, + + let stream = stream::iter(vec![ + SlashCommandEvent::StartSection { icon: IconName::Library, label: title, metadata: None, - }], - run_commands_in_text: true, - }) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text: prompt, + run_commands_in_text: true, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]); + + Ok(stream.boxed()) }) } } diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index c7183e95bbc85..b890bd0947c45 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -1,11 +1,11 @@ -use super::{ - create_label_for_command, - file_command::{build_entry_output_section, codeblock_fence_for_path}, - SlashCommand, SlashCommandOutput, -}; +use super::{codeblock_fence_for_path, create_label_for_command}; use anyhow::Result; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, +}; use feature_flags::FeatureFlag; +use futures::stream::{self, StreamExt}; use gpui::{AppContext, Task, WeakView}; use language::{CodeLabel, LspAdapterDelegate}; use semantic_index::{LoadedSearchResult, SemanticDb}; @@ -63,7 +63,7 @@ impl SlashCommand for SearchSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); }; @@ -107,40 +107,32 @@ impl SlashCommand for SearchSlashCommand { let loaded_results = SemanticDb::load_results(results, &fs, &cx).await?; - let output = cx - .background_executor() + cx.background_executor() .spawn(async move { - let mut text = format!("Search results for {query}:\n"); - let mut sections = Vec::new(); - for loaded_result in &loaded_results { - add_search_result_section(loaded_result, &mut text, &mut sections); - } - - let query = SharedString::from(query); - sections.push(SlashCommandOutputSection { - range: 0..text.len(), + let mut events = Vec::new(); + events.push(SlashCommandEvent::StartSection { icon: IconName::MagnifyingGlass, - label: query, + label: SharedString::from(format!("Search results for {query}:")), metadata: None, + ensure_newline: false, }); - SlashCommandOutput { - text, - sections, - run_commands_in_text: false, + for loaded_result in loaded_results { + add_search_result_section(&loaded_result, &mut events); } - }) - .await; - Ok(output) + events.push(SlashCommandEvent::EndSection { metadata: None }); + + Ok(stream::iter(events).boxed()) + }) + .await }) } } pub fn add_search_result_section( loaded_result: &LoadedSearchResult, - text: &mut String, - sections: &mut Vec>, + events: &mut Vec, ) { let LoadedSearchResult { path, @@ -149,22 +141,24 @@ pub fn add_search_result_section( row_range, .. } = loaded_result; - let section_start_ix = text.len(); - text.push_str(&codeblock_fence_for_path( - Some(&path), - Some(row_range.clone()), - )); + let mut text = codeblock_fence_for_path(Some(&path), Some(row_range.clone())); text.push_str(&excerpt_content); if !text.ends_with('\n') { text.push('\n'); } writeln!(text, "```\n").unwrap(); - let section_end_ix = text.len() - 1; - sections.push(build_entry_output_section( - section_start_ix..section_end_ix, - Some(&full_path), - false, - Some(row_range.start() + 1..row_range.end() + 1), - )); + + let path_str = path.to_string_lossy().to_string(); + events.push(SlashCommandEvent::StartSection { + icon: IconName::File, + label: path_str.into(), + metadata: None, + ensure_newline: false, + }); + events.push(SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }); + events.push(SlashCommandEvent::EndSection { metadata: None }); } diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index 887b57ba9956c..564df7cfc2558 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -1,7 +1,10 @@ -use super::{SlashCommand, SlashCommandOutput}; +use super::SlashCommand; use anyhow::{anyhow, Context as _, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, +}; use editor::Editor; +use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::Arc; @@ -46,7 +49,7 @@ impl SlashCommand for OutlineSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let output = workspace.update(cx, |workspace, cx| { let Some(active_item) = workspace.active_item(cx) else { return Task::ready(Err(anyhow!("no active tab"))); @@ -74,16 +77,21 @@ impl SlashCommand for OutlineSlashCommand { outline_text.push('\n'); } - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: 0..outline_text.len(), + let events = vec![ + SlashCommandEvent::StartSection { icon: IconName::ListTree, label: path.to_string_lossy().to_string().into(), metadata: None, - }], - text: outline_text, - run_commands_in_text: false, - }) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text: outline_text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]; + + Ok(stream::iter(events).boxed()) }) }); diff --git a/crates/assistant/src/slash_command/tab_command.rs b/crates/assistant/src/slash_command/tab_command.rs index 0bff4730d8e5c..fc4fc90aa33a7 100644 --- a/crates/assistant/src/slash_command/tab_command.rs +++ b/crates/assistant/src/slash_command/tab_command.rs @@ -1,9 +1,9 @@ -use super::{file_command::append_buffer_to_output, SlashCommand, SlashCommandOutput}; +use super::{buffer_to_output, SlashCommand}; use anyhow::{Context, Result}; -use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection}; +use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult}; use collections::{HashMap, HashSet}; use editor::Editor; -use futures::future::join_all; +use futures::{future::join_all, stream, StreamExt}; use gpui::{Entity, Task, WeakView}; use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate}; use std::{ @@ -132,7 +132,7 @@ impl SlashCommand for TabSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let tab_items_search = tab_items_for_queries( Some(workspace), arguments, @@ -142,11 +142,16 @@ impl SlashCommand for TabSlashCommand { ); cx.background_executor().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) + let stream = { + let search_results = tab_items_search.await?; + stream::iter(search_results).flat_map(|(full_path, buffer, _)| { + let output = buffer_to_output(&buffer, full_path.as_deref()) + .log_err() + .unwrap_or_default(); + futures::stream::iter(output) + }) + }; + Ok(stream.boxed()) }) } } diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 1d4959fb19957..281144e53b644 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -3,8 +3,10 @@ use std::sync::Arc; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; +use futures::stream::{self, StreamExt}; use gpui::{AppContext, Task, View, WeakView}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; @@ -62,7 +64,7 @@ impl SlashCommand for TerminalSlashCommand { workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); }; @@ -85,18 +87,22 @@ impl SlashCommand for TerminalSlashCommand { let mut text = String::new(); text.push_str("Terminal output:\n"); text.push_str(&lines.join("\n")); - let range = 0..text.len(); - Task::ready(Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, + let events = vec![ + SlashCommandEvent::StartSection { icon: IconName::Terminal, label: "Terminal".into(), metadata: None, - }], - run_commands_in_text: false, - })) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]; + + Task::ready(Ok(stream::iter(events).boxed())) } } diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index 071b4feaf436e..ad1c7f66fabcb 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/crates/assistant/src/slash_command/workflow_command.rs @@ -5,8 +5,10 @@ use std::sync::atomic::AtomicBool; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; +use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; @@ -58,22 +60,25 @@ impl SlashCommand for WorkflowSlashCommand { _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let prompt_builder = self.prompt_builder.clone(); cx.spawn(|_cx| async move { let text = prompt_builder.generate_workflow_prompt()?; - let range = 0..text.len(); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, + Ok(stream::iter(vec![ + SlashCommandEvent::StartSection { icon: IconName::Route, label: "Workflow".into(), metadata: None, - }], - run_commands_in_text: false, - }) + ensure_newline: false, + }, + SlashCommandEvent::Content { + text, + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]) + .boxed()) }) } } diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index a58a84312fc3e..eb75d0a7a7d1f 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -15,8 +15,10 @@ path = "src/assistant_slash_command.rs" anyhow.workspace = true collections.workspace = true derive_more.workspace = true +futures.workspace = true gpui.workspace = true language.workspace = true +language_model.workspace = true parking_lot.workspace = true serde.workspace = true serde_json.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 36e229d49a246..65c53535a7072 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -1,8 +1,10 @@ mod slash_command_registry; use anyhow::Result; +use futures::stream::{BoxStream, StreamExt}; use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; +pub use language_model::Role; use serde::{Deserialize, Serialize}; pub use slash_command_registry::*; use std::{ @@ -56,6 +58,8 @@ pub struct ArgumentCompletion { pub replace_previous_arguments: bool, } +pub type SlashCommandResult = Result>; + pub trait SlashCommand: 'static + Send + Sync { fn name(&self) -> String; fn label(&self, _cx: &AppContext) -> CodeLabel { @@ -87,7 +91,7 @@ pub trait SlashCommand: 'static + Send + Sync { // perhaps another kind of delegate is needed here. delegate: Option>, cx: &mut WindowContext, - ) -> Task>; + ) -> Task; } pub type RenderFoldPlaceholder = Arc< @@ -96,8 +100,40 @@ pub type RenderFoldPlaceholder = Arc< + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, >; +pub enum SlashCommandEvent { + StartMessage { + role: Role, + }, + StartSection { + icon: IconName, + label: SharedString, + metadata: Option, + ensure_newline: bool, + }, + Content { + text: String, + run_commands_in_text: bool, + }, + Progress { + message: SharedString, + complete: f32, + }, + EndSection { + metadata: Option, + }, +} + +pub fn as_stream(event: SlashCommandEvent) -> BoxStream<'static, SlashCommandEvent> { + futures::stream::once(async { event }).boxed() +} + +pub fn as_stream_vec(events: Vec) -> BoxStream<'static, SlashCommandEvent> { + futures::stream::iter(events.into_iter()).boxed() +} + #[derive(Debug, Default, PartialEq)] -pub struct SlashCommandOutput { +pub struct SlashCommandMessage { + pub role: Option, pub text: String, pub sections: Vec>, pub run_commands_in_text: bool, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 3dfbc4c03d9bb..563e1941ede36 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -1,12 +1,12 @@ -use std::sync::{atomic::AtomicBool, Arc}; - use anyhow::{anyhow, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, + as_stream_vec, ArgumentCompletion, SlashCommand, SlashCommandOutputSection, SlashCommandResult, }; +use assistant_slash_command::{Role, SlashCommandEvent}; use futures::FutureExt; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; +use std::sync::{atomic::AtomicBool, Arc}; use ui::prelude::*; use wasmtime_wasi::WasiView; use workspace::Workspace; @@ -87,7 +87,7 @@ impl SlashCommand for ExtensionSlashCommand { _workspace: WeakView, delegate: Option>, cx: &mut WindowContext, - ) -> Task> { + ) -> Task { let arguments = arguments.to_owned(); let output = cx.background_executor().spawn(async move { self.extension @@ -114,20 +114,33 @@ impl SlashCommand for ExtensionSlashCommand { }); cx.foreground_executor().spawn(async move { let output = output.await?; - Ok(SlashCommandOutput { - text: output.text, - sections: output - .sections - .into_iter() - .map(|section| SlashCommandOutputSection { - range: section.range.into(), - icon: IconName::Code, - label: section.label.into(), - metadata: None, - }) - .collect(), - run_commands_in_text: false, - }) + + let events = vec![ + SlashCommandEvent::StartMessage { + role: Role::Assistant, + }, + SlashCommandEvent::Content { + run_commands_in_text: false, + text: "Here is some fake output from the extension slash command:".to_string(), + }, + SlashCommandEvent::StartSection { + icon: IconName::Code, + label: "Code Output".into(), + metadata: None, + ensure_newline: true, + }, + SlashCommandEvent::Content { + run_commands_in_text: false, + text: "let x = 42;\nprintln!(\"The answer is {}\", x);".to_string(), + }, + SlashCommandEvent::EndSection { metadata: None }, + SlashCommandEvent::Content { + run_commands_in_text: false, + text: "\nThis concludes the fake output.".to_string(), + }, + ]; + + return Ok(as_stream_vec(events)); }) } } From 3ded3e4dedfb08bc459b7779c1052920b881b539 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 14 Oct 2024 19:42:49 +0100 Subject: [PATCH 08/49] assistant: Fix style --- crates/assistant/src/context.rs | 18 ++++++++++++------ .../slash_command/context_server_command.rs | 7 ++++--- .../src/slash_command/diagnostics_command.rs | 2 +- .../src/slash_command/docs_command.rs | 7 +++---- .../src/slash_command/file_command.rs | 5 ++--- .../src/slash_command/project_command.rs | 2 +- .../src/slash_command/search_command.rs | 1 - .../extension/src/extension_slash_command.rs | 2 +- 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 8d0d58edcc6d1..db7c5c42ef124 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,5 +1,8 @@ // todo!() // - implement run_commands_in_text +// - fix file tests +// - fix extension +// - fix ensure newline // - When slash command wants to insert a message, but it wants to insert it after a message that has the same Role and it emits a `StartMessage { merge_same_roles: bool (name TBD) }`, we should ignore it // - When a section ends, we should run the following code: // // this.slash_command_output_sections @@ -79,7 +82,7 @@ use std::{ cmp::{self, max, Ordering}, fmt::Debug, iter, mem, - ops::{Range, RangeBounds}, + ops::Range, path::{Path, PathBuf}, str::FromStr as _, sync::Arc, @@ -1824,7 +1827,7 @@ impl Context { &mut self, command_range: Range, output: Task, - ensure_trailing_newline: bool, + _ensure_trailing_newline: bool, expand_result: bool, cx: &mut ModelContext, ) { @@ -1873,7 +1876,7 @@ impl Context { icon, label, metadata, - ensure_newline, + ensure_newline: _, } => { this.read_with(&cx, |this, cx| { let buffer = this.buffer.read(cx); @@ -1888,7 +1891,7 @@ impl Context { } SlashCommandEvent::Content { text, - run_commands_in_text, + run_commands_in_text: _, } => { // assert!(!run_commands_in_text, "not yet implemented"); @@ -1903,7 +1906,10 @@ impl Context { }) })?; } - SlashCommandEvent::Progress { message, complete } => { + SlashCommandEvent::Progress { + message: _, + complete: _, + } => { todo!() } SlashCommandEvent::EndSection { metadata } => { @@ -1939,7 +1945,7 @@ impl Context { this.finished_slash_commands.insert(command_id); let version = this.version.clone(); - let (op, ev) = this.buffer.update(cx, |buffer, cx| { + let (op, ev) = this.buffer.update(cx, |buffer, _cx| { let start = command_range.start; let output_range = start..position; diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 74a1eb189b3f8..14dd531d26ea3 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -14,6 +14,7 @@ use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use std::sync::atomic::AtomicBool; use std::sync::Arc; use text::LineEnding; +use ui::IconName; use workspace::Workspace; pub struct ContextServerSlashCommand { @@ -154,10 +155,10 @@ impl SlashCommand for ContextServerSlashCommand { role: Role::Assistant, }); - if let Some(description) = result.description { + if let Some(ref description) = result.description { events.push(SlashCommandEvent::StartSection { - icon: ui::IconName::Info, - label: description.into(), + icon: IconName::Ai, + label: description.clone().into(), metadata: None, ensure_newline: false, }); diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index a3ccca0bfff70..fec9181c0be28 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use assistant_slash_command::{ ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, }; -use futures::stream::{self, BoxStream, StreamExt}; +use futures::stream::{BoxStream, StreamExt}; use fuzzy::{PathMatch, StringMatchCandidate}; use gpui::{AppContext, Model, Task, View, WeakView}; use language::{ diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index 3548d59ce22b0..60287a6a15fa8 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -8,7 +8,6 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, }; -use futures::stream::StreamExt; use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView}; use indexed_docs::{ DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName, @@ -317,7 +316,7 @@ impl SlashCommand for DocsSlashCommand { .await; } - let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') { + let (text, _ranges) = if let Some((prefix, _)) = key.split_once('*') { let docs = store.load_many_by_prefix(prefix.to_string()).await?; let mut text = String::new(); @@ -341,12 +340,12 @@ impl SlashCommand for DocsSlashCommand { (text, vec![(key, range)]) }; - anyhow::Ok((provider, text, ranges)) + anyhow::Ok((provider, text, _ranges)) } }); cx.foreground_executor().spawn(async move { - let (provider, text, ranges) = task.await?; + let (provider, text, _) = task.await?; let events = vec![ SlashCommandEvent::StartSection { diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 7633356b6d5fc..f63ddac2bedc6 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -8,9 +8,8 @@ use assistant_slash_command::{AfterCompletion, ArgumentCompletion}; use futures::stream::{self, StreamExt}; use fuzzy::PathMatch; use gpui::{AppContext, Model, Task, View, WeakView}; -use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate}; -use project::{Entry, PathMatchCandidateSet, Project}; -use serde::{Deserialize, Serialize}; +use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate}; +use project::{PathMatchCandidateSet, Project}; use std::{ path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index d3cc098c7a651..3c10470b78f0c 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -132,7 +132,7 @@ impl SlashCommand for ProjectSlashCommand { ensure_newline: false, }); - let mut output = "Project context:\n".to_string(); + let output = "Project context:\n".to_string(); events.push(SlashCommandEvent::Content { text: output.clone(), run_commands_in_text: true, diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index b890bd0947c45..48f12c040ca97 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -136,7 +136,6 @@ pub fn add_search_result_section( ) { let LoadedSearchResult { path, - full_path, excerpt_content, row_range, .. diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 563e1941ede36..b3135507708a7 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -113,7 +113,7 @@ impl SlashCommand for ExtensionSlashCommand { .await }); cx.foreground_executor().spawn(async move { - let output = output.await?; + let _output = output.await?; let events = vec![ SlashCommandEvent::StartMessage { From 9156c3fc955fbb316fe52e47dea9da0266d048e6 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 14 Oct 2024 20:20:06 +0100 Subject: [PATCH 09/49] assistant: run_commands_in_output -> run_commands_in_ranges We are updating the way we deal with commands in output text. Since commands can now emit content events one at a time, each event can determine if it should be run. To support this, we now grab a range and run only commands in the ranges of the content that specified to do so. --- crates/assistant/src/assistant_panel.rs | 6 ++-- crates/assistant/src/context.rs | 44 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 856f7485a4672..504ce82db9d7b 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2204,7 +2204,7 @@ impl ContextEditor { ContextEvent::SlashCommandFinished { output_range, sections, - run_commands_in_output, + run_commands_in_ranges, expand_result, } => { self.insert_slash_command_output_sections( @@ -2213,11 +2213,11 @@ impl ContextEditor { cx, ); - if *run_commands_in_output { + for range in run_commands_in_ranges { let commands = self.context.update(cx, |context, cx| { context.reparse(cx); context - .pending_commands_for_range(output_range.clone(), cx) + .pending_commands_for_range(range.clone(), cx) .to_vec() }); diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index db7c5c42ef124..360a0a73bd4fb 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -2,7 +2,6 @@ // - implement run_commands_in_text // - fix file tests // - fix extension -// - fix ensure newline // - When slash command wants to insert a message, but it wants to insert it after a message that has the same Role and it emits a `StartMessage { merge_same_roles: bool (name TBD) }`, we should ignore it // - When a section ends, we should run the following code: // // this.slash_command_output_sections @@ -353,7 +352,7 @@ pub enum ContextEvent { SlashCommandFinished { output_range: Range, sections: Vec>, - run_commands_in_output: bool, + run_commands_in_ranges: Vec>, expand_result: bool, }, UsePendingTools, @@ -877,7 +876,7 @@ impl Context { output_range, sections, expand_result: false, - run_commands_in_output: false, + run_commands_in_ranges: vec![], }); } } @@ -1827,7 +1826,7 @@ impl Context { &mut self, command_range: Range, output: Task, - _ensure_trailing_newline: bool, + ensure_trailing_newline: bool, expand_result: bool, cx: &mut ModelContext, ) { @@ -1843,6 +1842,7 @@ impl Context { icon: IconName, label: SharedString, metadata: Option, + ensure_newline: bool, } let position = this.update(&mut cx, |this, cx| { @@ -1856,6 +1856,8 @@ impl Context { let mut finished_sections: Vec> = Vec::new(); let mut pending_section_stack: Vec = Vec::new(); + let mut run_commands_in_ranges: Vec> = Vec::new(); + let mut has_newline = false; while let Some(event) = stream.next().await { match event { @@ -1876,7 +1878,7 @@ impl Context { icon, label, metadata, - ensure_newline: _, + ensure_newline, } => { this.read_with(&cx, |this, cx| { let buffer = this.buffer.read(cx); @@ -1886,24 +1888,37 @@ impl Context { icon, label, metadata, + ensure_newline, }); })?; } SlashCommandEvent::Content { text, - run_commands_in_text: _, + run_commands_in_text, } => { - // assert!(!run_commands_in_text, "not yet implemented"); - this.update(&mut cx, |this, cx| { - this.buffer.update(cx, |buffer, cx| { - let text = if !text.ends_with('\n') { + let start = this.buffer.read(cx).anchor_before(position); + + let result = this.buffer.update(cx, |buffer, cx| { + let ensure_newline = pending_section_stack + .last() + .map(|ps| ps.ensure_newline) + .unwrap_or(false); + let text = if ensure_newline && !text.ends_with('\n') { text + "\n" } else { text }; + has_newline = text.ends_with("\n"); buffer.edit([(position..position, text)], None, cx) - }) + }); + + let end = this.buffer.read(cx).anchor_before(position); + if run_commands_in_text { + run_commands_in_ranges.push(start..end); + } + + result })?; } SlashCommandEvent::Progress { @@ -1919,7 +1934,10 @@ impl Context { let start = pending_section.start; let end = buffer.anchor_before(position); - buffer.edit([(position..position, "\n")], None, cx); + if !has_newline && ensure_trailing_newline { + buffer.edit([(position..position, "\n")], None, cx); + } + log::info!("Slash command output section end: {:?}", end); let slash_command_output_section = @@ -1964,7 +1982,7 @@ impl Context { ContextEvent::SlashCommandFinished { output_range, sections: finished_sections, - run_commands_in_output: false, + run_commands_in_ranges, expand_result, }, ) From 3b93237386bbd8907295a8d8fd65cb9bedc72536 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 14 Oct 2024 21:16:22 +0100 Subject: [PATCH 10/49] assistant: Fix context tests --- crates/assistant/src/context.rs | 2 - crates/assistant/src/context/context_tests.rs | 67 ++++--- .../src/slash_command/file_command.rs | 178 ++++++++++++------ 3 files changed, 167 insertions(+), 80 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 360a0a73bd4fb..7d25d21d50f5d 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,5 +1,4 @@ // todo!() -// - implement run_commands_in_text // - fix file tests // - fix extension // - When slash command wants to insert a message, but it wants to insert it after a message that has the same Role and it emits a `StartMessage { merge_same_roles: bool (name TBD) }`, we should ignore it @@ -27,7 +26,6 @@ // // expand_result, // // }, // // ) -// - Adapt all the commands to use the streaming API // - Animation example: // Icon::new(IconName::ArrowCircle) // .size(IconSize::Small) diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index ccbcd5311a04a..349f7833338d2 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -7,10 +7,11 @@ use crate::{ use anyhow::Result; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandRegistry, + SlashCommandRegistry, SlashCommandResult, }; use collections::HashSet; use fs::FakeFs; +use futures::stream::{self, StreamExt}; use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView}; use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate}; use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role}; @@ -28,7 +29,7 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; use text::{network::Network, OffsetRangeExt as _, ReplicaId}; -use ui::{Context as _, WindowContext}; +use ui::{Context as _, IconName, WindowContext}; use unindent::Unindent; use util::{ test::{generate_marked_text, marked_text_ranges}, @@ -1074,43 +1075,50 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std offset + 1..offset + 1 + command_text.len() }); - let output_len = rng.gen_range(1..=10); let output_text = RandomCharIter::new(&mut rng) .filter(|c| *c != '\r') - .take(output_len) + .take(10) .collect::(); + let mut events = vec![SlashCommandEvent::StartMessage { role: Role::User }]; + let num_sections = rng.gen_range(0..=3); - let mut sections = Vec::with_capacity(num_sections); + let mut section_start = 0; for _ in 0..num_sections { - let section_start = rng.gen_range(0..output_len); - let section_end = rng.gen_range(section_start..=output_len); - sections.push(SlashCommandOutputSection { - range: section_start..section_end, - icon: ui::IconName::Ai, + let section_end = rng.gen_range(section_start..=output_text.len()); + events.push(SlashCommandEvent::StartSection { + icon: IconName::Ai, label: "section".into(), metadata: None, + ensure_newline: false, + }); + events.push(SlashCommandEvent::Content { + text: output_text[section_start..section_end].to_string(), + run_commands_in_text: false, + }); + events.push(SlashCommandEvent::EndSection { metadata: None }); + section_start = section_end; + } + + if section_start < output_text.len() { + events.push(SlashCommandEvent::Content { + text: output_text[section_start..].to_string(), + run_commands_in_text: false, }); } log::info!( - "Context {}: insert slash command output at {:?} with {:?}", + "Context {}: insert slash command output at {:?} with {:?} events", context_index, command_range, - sections + events.len() ); let command_range = context.buffer.read(cx).anchor_after(command_range.start) ..context.buffer.read(cx).anchor_after(command_range.end); context.insert_command_output( command_range, - Task::ready(Ok(SlashCommandOutput { - role: Some(Role::User), - text: output_text, - sections, - run_commands_in_text: false, - } - .into())), + Task::ready(Ok(stream::iter(events).boxed())), true, false, cx, @@ -1429,11 +1437,20 @@ impl SlashCommand for FakeSlashCommand { _workspace: WeakView, _delegate: Option>, _cx: &mut WindowContext, - ) -> Task> { - Task::ready(Ok(SlashCommandOutput { - text: format!("Executed fake command: {}", self.0), - sections: vec![], - run_commands_in_text: false, - })) + ) -> Task { + Task::ready(Ok(stream::iter(vec![ + SlashCommandEvent::StartSection { + icon: IconName::Ai, + label: "Fake Command".into(), + metadata: None, + ensure_newline: false, + }, + SlashCommandEvent::Content { + text: format!("Executed fake command: {}", self.0), + run_commands_in_text: false, + }, + SlashCommandEvent::EndSection { metadata: None }, + ]) + .boxed())) } } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index f63ddac2bedc6..96687955dd268 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -411,12 +411,14 @@ mod custom_path_matcher { #[cfg(test)] mod test { use fs::FakeFs; + use futures::StreamExt; use gpui::TestAppContext; use project::Project; use serde_json::json; use settings::SettingsStore; use crate::slash_command::file_command::collect_files; + use assistant_slash_command::SlashCommandEvent; pub fn init_test(cx: &mut gpui::TestAppContext) { if std::env::var("RUST_LOG").is_ok() { @@ -426,7 +428,6 @@ mod test { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); - // release_channel::init(SemanticVersion::default(), cx); language::init(cx); Project::init_settings(cx); }); @@ -455,32 +456,52 @@ mod test { let project = Project::test(fs, ["/root".as_ref()], cx).await; - let result_1 = cx + let mut result_1 = cx .update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx)) .await .unwrap(); - assert!(result_1.text.starts_with("root/dir")); + let mut events_1 = Vec::new(); + while let Some(event) = result_1.next().await { + events_1.push(event); + } + + // Check first event is StartSection with "root/dir" + assert!(matches!(&events_1[0], + SlashCommandEvent::StartSection { label, .. } if label.starts_with("root/dir"))); + // 4 files + 2 directories - assert_eq!(result_1.sections.len(), 6); + assert_eq!(events_1.len(), 18); // 2 events per section (start/end) + content events - let result_2 = cx + let mut result_2 = cx .update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx)) .await .unwrap(); - assert_eq!(result_1, result_2); + let mut events_2 = Vec::new(); + while let Some(event) = result_2.next().await { + events_2.push(event); + } - let result = cx + assert_eq!(events_1.len(), events_2.len()); + + let mut result = cx .update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx)) .await .unwrap(); - assert!(result.text.starts_with("root/dir")); + let mut events = Vec::new(); + while let Some(event) = result.next().await { + events.push(event); + } + + // Check first event is StartSection with "root/dir" + assert!(matches!(&events[0], + SlashCommandEvent::StartSection { label, .. } if label.starts_with("root/dir"))); + // 5 files + 2 directories - assert_eq!(result.sections.len(), 7); + assert_eq!(events.len(), 21); // 2 events per section (start/end) + content events - // Ensure that the project lasts until after the last await drop(project); } @@ -517,36 +538,44 @@ mod test { let project = Project::test(fs, ["/zed".as_ref()], cx).await; - let result = cx + let mut result = cx .update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx)) .await .unwrap(); - // Sanity check - assert!(result.text.starts_with("zed/assets/themes\n")); - assert_eq!(result.sections.len(), 7); - - // Ensure that full file paths are included in the real output - assert!(result.text.contains("zed/assets/themes/andromeda/LICENSE")); - assert!(result.text.contains("zed/assets/themes/ayu/LICENSE")); - assert!(result.text.contains("zed/assets/themes/summercamp/LICENSE")); + let mut events = Vec::new(); + while let Some(event) = result.next().await { + events.push(event); + } - assert_eq!(result.sections[5].label, "summercamp"); + // Check first event is StartSection with themes path + assert!(matches!(&events[0], + SlashCommandEvent::StartSection { label, .. } if label.starts_with("zed/assets/themes"))); + + // Check we have the right number of sections (7 sections) + let section_events: Vec<_> = events + .iter() + .filter(|e| matches!(e, SlashCommandEvent::StartSection { .. })) + .collect(); + assert_eq!(section_events.len(), 7); + + // Check content is included in the events + let content_events: Vec<_> = events + .iter() + .filter(|e| matches!(e, SlashCommandEvent::Content { text, .. })) + .collect(); + + let content = content_events.iter().fold(String::new(), |mut acc, e| { + if let SlashCommandEvent::Content { text, .. } = e { + acc.push_str(text); + } + acc + }); - // Ensure that things are in descending order, with properly relativized paths - assert_eq!( - result.sections[0].label, - "zed/assets/themes/andromeda/LICENSE" - ); - assert_eq!(result.sections[1].label, "andromeda"); - assert_eq!(result.sections[2].label, "zed/assets/themes/ayu/LICENSE"); - assert_eq!(result.sections[3].label, "ayu"); - assert_eq!( - result.sections[4].label, - "zed/assets/themes/summercamp/LICENSE" - ); + assert!(content.contains("zed/assets/themes/andromeda/LICENSE")); + assert!(content.contains("zed/assets/themes/ayu/LICENSE")); + assert!(content.contains("zed/assets/themes/summercamp/LICENSE")); - // Ensure that the project lasts until after the last await drop(project); } @@ -578,31 +607,74 @@ mod test { let project = Project::test(fs, ["/zed".as_ref()], cx).await; - let result = cx + let mut result = cx .update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx)) .await .unwrap(); - assert!(result.text.starts_with("zed/assets/themes\n")); - assert_eq!(result.sections[0].label, "zed/assets/themes/LICENSE"); - assert_eq!( - result.sections[1].label, - "zed/assets/themes/summercamp/LICENSE" - ); - assert_eq!( - result.sections[2].label, - "zed/assets/themes/summercamp/subdir/LICENSE" - ); - assert_eq!( - result.sections[3].label, - "zed/assets/themes/summercamp/subdir/subsubdir/LICENSE" - ); - assert_eq!(result.sections[4].label, "subsubdir"); - assert_eq!(result.sections[5].label, "subdir"); - assert_eq!(result.sections[6].label, "summercamp"); - assert_eq!(result.sections[7].label, "zed/assets/themes"); + let mut events = Vec::new(); + while let Some(event) = result.next().await { + events.push(event); + } + + // Check content of events with pattern matching + let mut i = 0; + // Check we get all expected events + let events_str = events + .iter() + .map(|e| match e { + SlashCommandEvent::StartSection { label, .. } => format!("StartSection: {}", label), + SlashCommandEvent::Content { text, .. } => format!("Content: {}", text), + SlashCommandEvent::EndSection { .. } => "EndSection".to_string(), + _ => "Unknown event".to_string(), + }) + .collect::>() + .join("\n"); + + for event in &events { + match event { + SlashCommandEvent::StartSection { label, .. } => { + match i { + 0 => assert!(label.starts_with("zed/assets/themes")), + 2 => assert!(label.starts_with("summercamp")), + 4 => assert!(label.starts_with("subdir")), + 6 => assert!(label.starts_with("subsubdir")), + _ => (), + } + i += 1; + } + SlashCommandEvent::Content { text, .. } => match i { + 1 => assert!( + text.contains("zed/assets/themes"), + "Expected text to contain 'LICENSE' but got: {}", + text + ), + 2 => assert!( + text.contains("LICENSE"), + "Expected text to contain 'LICENSE' but got: {}", + text + ), + 4 => assert!( + text.contains("summercamp/LICENSE"), + "Expected text to contain 'summercamp/LICENSE' but got: {}", + text + ), + 6 => assert!( + text.contains("subdir/LICENSE"), + "Expected text to contain 'subdir/LICENSE' but got: {}", + text + ), + 8 => assert!( + text.contains("subsubdir/LICENSE"), + "Expected text to contain 'subsubdir/LICENSE' but got: {}", + text + ), + _ => (), + }, + _ => (), + } + } - // Ensure that the project lasts until after the last awai drop(project); } } From d4ea37c397383ba90d905fdb7091724aac14ed9f Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 15 Oct 2024 12:18:49 +0100 Subject: [PATCH 11/49] assistant: Merge roles when merge_same_roles true When a slash command issues multiple `StartMessage` events with the same role we merge them if merge_same_roles is set to true. --- crates/assistant/src/context.rs | 32 ++++++++++++------- .../src/slash_command/auto_command.rs | 11 +++---- .../slash_command/context_server_command.rs | 3 +- .../src/assistant_slash_command.rs | 1 + .../extension/src/extension_slash_command.rs | 13 +------- 5 files changed, 28 insertions(+), 32 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 7d25d21d50f5d..92cf19f2adbe7 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1856,21 +1856,29 @@ impl Context { let mut pending_section_stack: Vec = Vec::new(); let mut run_commands_in_ranges: Vec> = Vec::new(); let mut has_newline = false; + let mut last_role: Option = None; while let Some(event) = stream.next().await { match event { - SlashCommandEvent::StartMessage { role } => { - this.update(&mut cx, |this, cx| { - let offset = this - .buffer - .read_with(cx, |buffer, _cx| position.to_offset(buffer)); - this.insert_message_at_offset( - offset, - role, - MessageStatus::Pending, - cx, - ); - })?; + SlashCommandEvent::StartMessage { + role, + merge_same_roles, + } => { + if !merge_same_roles && Some(role) != last_role { + this.update(&mut cx, |this, cx| { + let offset = this + .buffer + .read_with(cx, |buffer, _cx| position.to_offset(buffer)); + this.insert_message_at_offset( + offset, + role, + MessageStatus::Pending, + cx, + ); + })?; + } + + last_role = Some(role); } SlashCommandEvent::StartSection { icon, diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index 9503d8d151460..5160f94dab900 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -141,13 +141,10 @@ impl SlashCommand for AutoCommand { prompt.push('\n'); prompt.push_str(&original_prompt); - Ok(stream::iter(vec![ - SlashCommandEvent::StartMessage { role: Role::User }, - SlashCommandEvent::Content { - text: prompt, - run_commands_in_text: true, - }, - ]) + Ok(stream::iter(vec![SlashCommandEvent::Content { + text: prompt, + run_commands_in_text: true, + }]) .boxed()) }) } diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 14dd531d26ea3..e474f70562149 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -152,7 +152,8 @@ impl SlashCommand for ContextServerSlashCommand { let mut events = Vec::new(); events.push(SlashCommandEvent::StartMessage { - role: Role::Assistant, + role: Role::User, + merge_same_roles: true, }); if let Some(ref description) = result.description { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 65c53535a7072..7775d96c36d49 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -103,6 +103,7 @@ pub type RenderFoldPlaceholder = Arc< pub enum SlashCommandEvent { StartMessage { role: Role, + merge_same_roles: bool, }, StartSection { icon: IconName, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index b3135507708a7..9cd59dc2b766c 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; +use assistant_slash_command::SlashCommandEvent; use assistant_slash_command::{ as_stream_vec, ArgumentCompletion, SlashCommand, SlashCommandOutputSection, SlashCommandResult, }; -use assistant_slash_command::{Role, SlashCommandEvent}; use futures::FutureExt; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; @@ -116,13 +116,6 @@ impl SlashCommand for ExtensionSlashCommand { let _output = output.await?; let events = vec![ - SlashCommandEvent::StartMessage { - role: Role::Assistant, - }, - SlashCommandEvent::Content { - run_commands_in_text: false, - text: "Here is some fake output from the extension slash command:".to_string(), - }, SlashCommandEvent::StartSection { icon: IconName::Code, label: "Code Output".into(), @@ -134,10 +127,6 @@ impl SlashCommand for ExtensionSlashCommand { text: "let x = 42;\nprintln!(\"The answer is {}\", x);".to_string(), }, SlashCommandEvent::EndSection { metadata: None }, - SlashCommandEvent::Content { - run_commands_in_text: false, - text: "\nThis concludes the fake output.".to_string(), - }, ]; return Ok(as_stream_vec(events)); From f2e7d022e3e4932c5c70a6a6f1c52aca8d9696cb Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 15 Oct 2024 14:15:14 +0100 Subject: [PATCH 12/49] context_servers: Update protocol and return map of SamplingMessages from slash commands --- .../slash_command/context_server_command.rs | 44 ++++++++++++------- crates/context_servers/src/types.rs | 20 +++++---- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index e474f70562149..e243237c5be75 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -7,7 +7,7 @@ use assistant_slash_command::{ use collections::HashMap; use context_servers::{ manager::{ContextServer, ContextServerManager}, - types::Prompt, + types::{Prompt, SamplingContent, SamplingRole}, }; use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; @@ -145,32 +145,42 @@ impl SlashCommand for ContextServerSlashCommand { return Err(anyhow!("Context server not initialized")); }; let result = protocol.run_prompt(&prompt_name, prompt_args).await?; - let mut prompt = result.prompt; - - // We must normalize the line endings here, since servers might return CR characters. - LineEnding::normalize(&mut prompt); let mut events = Vec::new(); - events.push(SlashCommandEvent::StartMessage { - role: Role::User, - merge_same_roles: true, - }); - if let Some(ref description) = result.description { + for message in result.messages { + events.push(SlashCommandEvent::StartMessage { + role: match message.role { + SamplingRole::User => Role::User, + SamplingRole::Assistant => Role::Assistant, + }, + merge_same_roles: true, + }); + events.push(SlashCommandEvent::StartSection { icon: IconName::Ai, - label: description.clone().into(), + label: "".into(), metadata: None, ensure_newline: false, }); - } - events.push(SlashCommandEvent::Content { - text: prompt, - run_commands_in_text: false, - }); + match message.content { + SamplingContent::Text { text } => { + let mut normalized_text = text; + LineEnding::normalize(&mut normalized_text); + events.push(SlashCommandEvent::Content { + text: normalized_text, + run_commands_in_text: false, + }); + } + SamplingContent::Image { + data: _, + mime_type: _, + } => { + todo!() + } + } - if result.description.is_some() { events.push(SlashCommandEvent::EndSection { metadata: None }); } diff --git a/crates/context_servers/src/types.rs b/crates/context_servers/src/types.rs index 04ac87c704d06..9817988fe49f5 100644 --- a/crates/context_servers/src/types.rs +++ b/crates/context_servers/src/types.rs @@ -2,6 +2,8 @@ use collections::HashMap; use serde::{Deserialize, Serialize}; use url::Url; +pub const PROTOCOL_VERSION: u32 = 1; + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub enum RequestType { @@ -16,6 +18,7 @@ pub enum RequestType { PromptsList, CompletionComplete, Ping, + ToolsList, } impl RequestType { @@ -32,6 +35,7 @@ impl RequestType { RequestType::PromptsList => "prompts/list", RequestType::CompletionComplete => "completion/complete", RequestType::Ping => "ping", + RequestType::ToolsList => "tools/list", } } } @@ -128,7 +132,7 @@ pub struct CompletionArgument { pub value: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct InitializeResponse { pub protocol_version: u32, @@ -136,13 +140,13 @@ pub struct InitializeResponse { pub server_info: Implementation, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResourcesReadResponse { pub contents: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResourcesListResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -174,27 +178,27 @@ pub enum SamplingContent { Image { data: String, mime_type: String }, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PromptsGetResponse { #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - pub prompt: String, + pub messages: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PromptsListResponse { pub prompts: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionCompleteResponse { pub completion: CompletionResult, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionResult { pub values: Vec, From 3b66a6fd2e7a4661f84cd41e23cd3b774f3f2e7a Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 15 Oct 2024 15:38:21 +0100 Subject: [PATCH 13/49] assistant: Introduce a content type for slash commands --- Cargo.lock | 2 + crates/assistant/Cargo.toml | 2 + crates/assistant/src/assistant_panel.rs | 2 +- crates/assistant/src/context.rs | 77 ++++++++++++------- crates/assistant/src/slash_command.rs | 8 +- .../src/slash_command/auto_command.rs | 11 ++- .../slash_command/cargo_workspace_command.rs | 7 +- .../slash_command/context_server_command.rs | 20 ++--- .../src/slash_command/default_command.rs | 7 +- .../src/slash_command/delta_command.rs | 10 ++- .../src/slash_command/diagnostics_command.rs | 19 ++--- .../src/slash_command/docs_command.rs | 9 +-- .../src/slash_command/fetch_command.rs | 8 +- .../src/slash_command/file_command.rs | 20 +++-- .../src/slash_command/now_command.rs | 8 +- .../src/slash_command/project_command.rs | 17 ++-- .../src/slash_command/prompt_command.rs | 7 +- .../src/slash_command/search_command.rs | 9 +-- .../src/slash_command/symbols_command.rs | 8 +- .../src/slash_command/terminal_command.rs | 8 +- .../src/slash_command/workflow_command.rs | 8 +- .../src/assistant_slash_command.rs | 16 ++-- .../extension/src/extension_slash_command.rs | 8 +- 23 files changed, 171 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9a3a87f78688..ad8b8092a83f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,7 @@ dependencies = [ "assistant_slash_command", "assistant_tool", "async-watch", + "base64 0.22.1", "cargo_toml", "chrono", "client", @@ -396,6 +397,7 @@ dependencies = [ "heed", "html_to_markdown", "http_client", + "image", "indexed_docs", "indoc", "language", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 9e61eee18aaf8..f1a5f8eeb1e38 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -27,6 +27,7 @@ assets.workspace = true assistant_slash_command.workspace = true assistant_tool.workspace = true async-watch.workspace = true +base64.workspace = true cargo_toml.workspace = true chrono.workspace = true client.workspace = true @@ -46,6 +47,7 @@ handlebars.workspace = true heed.workspace = true html_to_markdown.workspace = true http_client.workspace = true +image.workspace = true indexed_docs.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 504ce82db9d7b..88a612dcf946c 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2202,7 +2202,7 @@ impl ContextEditor { }) } ContextEvent::SlashCommandFinished { - output_range, + output_range: _output_range, sections, run_commands_in_ranges, expand_result, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 92cf19f2adbe7..f22ba821982a7 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -45,7 +45,8 @@ use crate::{ }; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ - SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, + SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, + SlashCommandResult, }; use assistant_tool::ToolRegistry; use client::{self, proto, telemetry::Telemetry}; @@ -1898,35 +1899,57 @@ impl Context { }); })?; } - SlashCommandEvent::Content { - text, - run_commands_in_text, - } => { - this.update(&mut cx, |this, cx| { - let start = this.buffer.read(cx).anchor_before(position); - - let result = this.buffer.update(cx, |buffer, cx| { - let ensure_newline = pending_section_stack - .last() - .map(|ps| ps.ensure_newline) - .unwrap_or(false); - let text = if ensure_newline && !text.ends_with('\n') { - text + "\n" - } else { - text + SlashCommandEvent::Content(content) => match content { + SlashCommandContentType::Image { image } => { + this.update(&mut cx, |this, cx| { + let Some(render_image) = image.to_image_data(cx).log_err() + else { + return; }; - has_newline = text.ends_with("\n"); - buffer.edit([(position..position, text)], None, cx) - }); + let image_id = image.id(); + let image_task = + LanguageModelImage::from_image(image, cx).shared(); + this.insert_content( + Content::Image { + anchor: position, + image_id, + image: image_task, + render_image, + }, + cx, + ); + })?; + } + SlashCommandContentType::Text { + text, + run_commands_in_text, + } => { + this.update(&mut cx, |this, cx| { + let start = this.buffer.read(cx).anchor_before(position); + + let result = this.buffer.update(cx, |buffer, cx| { + let ensure_newline = pending_section_stack + .last() + .map(|ps| ps.ensure_newline) + .unwrap_or(false); + let text = if ensure_newline && !text.ends_with('\n') { + text + "\n" + } else { + text + }; + has_newline = text.ends_with("\n"); + buffer.edit([(position..position, text)], None, cx) + }); - let end = this.buffer.read(cx).anchor_before(position); - if run_commands_in_text { - run_commands_in_ranges.push(start..end); - } + let end = this.buffer.read(cx).anchor_before(position); + if run_commands_in_text { + run_commands_in_ranges.push(start..end); + } - result - })?; - } + result + })?; + } + }, SlashCommandEvent::Progress { message: _, complete: _, diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 0d4df2b325878..1d9e61b34b937 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -1,6 +1,6 @@ use crate::assistant_panel::ContextEditor; use anyhow::Result; -use assistant_slash_command::AfterCompletion; +use assistant_slash_command::{AfterCompletion, SlashCommandContentType}; pub use assistant_slash_command::{ SlashCommand, SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, @@ -514,10 +514,10 @@ pub fn buffer_to_output( } code_content.push_str("```\n"); - events.push(SlashCommandEvent::Content { - text: code_content.into(), + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { + text: code_content, run_commands_in_text: false, - }); + })); events.push(SlashCommandEvent::EndSection { metadata: None }); diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index 5160f94dab900..3793ee6709546 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -1,6 +1,7 @@ use super::create_label_for_command; use super::SlashCommand; use anyhow::{anyhow, Result}; +use assistant_slash_command::SlashCommandContentType; use assistant_slash_command::{ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection}; use feature_flags::FeatureFlag; use futures::stream::BoxStream; @@ -141,10 +142,12 @@ impl SlashCommand for AutoCommand { prompt.push('\n'); prompt.push_str(&original_prompt); - Ok(stream::iter(vec![SlashCommandEvent::Content { - text: prompt, - run_commands_in_text: true, - }]) + Ok(stream::iter(vec![SlashCommandEvent::Content( + SlashCommandContentType::Text { + text: prompt, + run_commands_in_text: true, + }, + )]) .boxed()) }) } diff --git a/crates/assistant/src/slash_command/cargo_workspace_command.rs b/crates/assistant/src/slash_command/cargo_workspace_command.rs index 6cc9c0fa42911..22aac18e30696 100644 --- a/crates/assistant/src/slash_command/cargo_workspace_command.rs +++ b/crates/assistant/src/slash_command/cargo_workspace_command.rs @@ -1,7 +1,8 @@ use super::SlashCommand; use anyhow::{anyhow, Context, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use fs::Fs; use futures::stream::{self, StreamExt}; @@ -145,10 +146,10 @@ impl SlashCommand for CargoWorkspaceSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]) .boxed()) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index e243237c5be75..62ef2cacfe758 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -1,8 +1,8 @@ use super::create_label_for_command; use anyhow::{anyhow, Result}; use assistant_slash_command::{ - as_stream_vec, AfterCompletion, ArgumentCompletion, Role, SlashCommand, SlashCommandEvent, - SlashCommandOutputSection, SlashCommandResult, + as_stream_vec, AfterCompletion, ArgumentCompletion, Role, SlashCommand, + SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, }; use collections::HashMap; use context_servers::{ @@ -168,16 +168,18 @@ impl SlashCommand for ContextServerSlashCommand { SamplingContent::Text { text } => { let mut normalized_text = text; LineEnding::normalize(&mut normalized_text); - events.push(SlashCommandEvent::Content { - text: normalized_text, - run_commands_in_text: false, - }); + events.push(SlashCommandEvent::Content( + SlashCommandContentType::Text { + text: normalized_text, + run_commands_in_text: false, + }, + )); } SamplingContent::Image { - data: _, - mime_type: _, + data: _data, + mime_type: _mime_type, } => { - todo!() + todo!("unsupported") } } diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 0fc9c8c6b6862..35fd1a00d4a06 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -2,7 +2,8 @@ use super::SlashCommand; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; @@ -77,10 +78,10 @@ impl SlashCommand for DefaultSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: true, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]); diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs index 5e1d6011b8611..1caaec064e727 100644 --- a/crates/assistant/src/slash_command/delta_command.rs +++ b/crates/assistant/src/slash_command/delta_command.rs @@ -2,8 +2,8 @@ use crate::slash_command::file_command::FileSlashCommand; use crate::slash_command::FileCommandMetadata; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use collections::HashSet; use futures::{ @@ -93,7 +93,11 @@ impl SlashCommand for DeltaSlashCommand { let new_content = stream::StreamExt::collect::>(new_events).await; { if let Some(first_content) = new_content.iter().find_map(|event| { - if let SlashCommandEvent::Content { text, .. } = event { + if let SlashCommandEvent::Content(SlashCommandContentType::Text { + text, + .. + }) = event + { Some(text) } else { None diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index fec9181c0be28..e7a8b3261d7e5 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -1,7 +1,8 @@ use super::{create_label_for_command, SlashCommand}; use anyhow::{anyhow, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use futures::stream::{BoxStream, StreamExt}; use fuzzy::{PathMatch, StringMatchCandidate}; @@ -269,15 +270,15 @@ fn collect_diagnostics( let mut events = Vec::new(); if let Some(error_source) = error_source.as_ref() { - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: format!("diagnostics: {}\n", error_source), run_commands_in_text: false, - }); + })); } else { - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: "diagnostics\n".to_string(), run_commands_in_text: false, - }); + })); } let mut project_summary = DiagnosticSummary::default(); @@ -303,10 +304,10 @@ fn collect_diagnostics( metadata: None, ensure_newline: false, }); - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: format!("{}\n", file_path), run_commands_in_text: false, - }); + })); events.push(SlashCommandEvent::EndSection { metadata: None }); } @@ -441,10 +442,10 @@ fn collect_diagnostic( metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ] } diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index 60287a6a15fa8..665e240ed19f1 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -5,8 +5,8 @@ use std::time::Duration; use anyhow::{anyhow, bail, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView}; use indexed_docs::{ @@ -346,7 +346,6 @@ impl SlashCommand for DocsSlashCommand { cx.foreground_executor().spawn(async move { let (provider, text, _) = task.await?; - let events = vec![ SlashCommandEvent::StartSection { icon: IconName::FileDoc, @@ -354,10 +353,10 @@ impl SlashCommand for DocsSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]; diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index f9d163c3cae80..1c28bf00e8e55 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use futures::{ stream::{self, StreamExt}, @@ -167,10 +167,10 @@ impl SlashCommand for FetchSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]) .boxed()) diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 96687955dd268..6a0daa44ec29d 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -4,7 +4,7 @@ use super::{ }; // use super::diagnostics_command::collect_buffer_diagnostics; use anyhow::{anyhow, Context as _, Result}; -use assistant_slash_command::{AfterCompletion, ArgumentCompletion}; +use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandContentType}; use futures::stream::{self, StreamExt}; use fuzzy::PathMatch; use gpui::{AppContext, Model, Task, View, WeakView}; @@ -292,10 +292,10 @@ fn collect_files( metadata: None, ensure_newline: true, }); - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: dirname, run_commands_in_text: false, - }); + })); directory_stack.push(entry.path.clone()); } else if entry.is_file() { let open_buffer_task = project_handle @@ -418,7 +418,7 @@ mod test { use settings::SettingsStore; use crate::slash_command::file_command::collect_files; - use assistant_slash_command::SlashCommandEvent; + use assistant_slash_command::{SlashCommandContentType, SlashCommandEvent}; pub fn init_test(cx: &mut gpui::TestAppContext) { if std::env::var("RUST_LOG").is_ok() { @@ -564,9 +564,8 @@ mod test { .iter() .filter(|e| matches!(e, SlashCommandEvent::Content { text, .. })) .collect(); - let content = content_events.iter().fold(String::new(), |mut acc, e| { - if let SlashCommandEvent::Content { text, .. } = e { + if let SlashCommandEvent::Content(SlashCommandContentType::Text { text, .. }) = e { acc.push_str(text); } acc @@ -624,7 +623,12 @@ mod test { .iter() .map(|e| match e { SlashCommandEvent::StartSection { label, .. } => format!("StartSection: {}", label), - SlashCommandEvent::Content { text, .. } => format!("Content: {}", text), + SlashCommandEvent::Content(SlashCommandContentType::Text { text, .. }) => { + format!("Content: {}", text) + } + SlashCommandEvent::Content(SlashCommandContentType::Image { .. }) => { + "Content: Image".to_string() + } SlashCommandEvent::EndSection { .. } => "EndSection".to_string(), _ => "Unknown event".to_string(), }) @@ -643,7 +647,7 @@ mod test { } i += 1; } - SlashCommandEvent::Content { text, .. } => match i { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, .. }) => match i { 1 => assert!( text.contains("zed/assets/themes"), "Expected text to contain 'LICENSE' but got: {}", diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 0f1e91dd96c14..54253104aafb7 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use chrono::Local; use futures::stream::{self, StreamExt}; @@ -61,10 +61,10 @@ impl SlashCommand for NowSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]) .boxed())) diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 3c10470b78f0c..fceeee0726f06 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -2,7 +2,8 @@ use super::{create_label_for_command, SlashCommand}; use crate::PromptBuilder; use anyhow::{anyhow, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use feature_flags::FeatureFlag; use futures::stream::{self, StreamExt}; @@ -133,10 +134,10 @@ impl SlashCommand for ProjectSlashCommand { }); let output = "Project context:\n".to_string(); - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: output.clone(), run_commands_in_text: true, - }); + })); for (ix, query) in search_queries.into_iter().enumerate() { let mut has_results = false; @@ -156,10 +157,12 @@ impl SlashCommand for ProjectSlashCommand { } if has_results { - events.push(SlashCommandEvent::Content { - text: section_text, - run_commands_in_text: true, - }); + events.push(SlashCommandEvent::Content( + SlashCommandContentType::Text { + text: section_text, + run_commands_in_text: true, + }, + )); events.push(SlashCommandEvent::EndSection { metadata: None }); } } diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 0d5de0ad05b60..a4cb8721ce1bb 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -2,7 +2,8 @@ use super::SlashCommand; use crate::prompt_library::PromptStore; use anyhow::{anyhow, Context, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; @@ -101,10 +102,10 @@ impl SlashCommand for PromptSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text: prompt, run_commands_in_text: true, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]); diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 48f12c040ca97..6e1fb34e7a683 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -1,8 +1,8 @@ use super::{codeblock_fence_for_path, create_label_for_command}; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use feature_flags::FeatureFlag; use futures::stream::{self, StreamExt}; @@ -147,7 +147,6 @@ pub fn add_search_result_section( text.push('\n'); } writeln!(text, "```\n").unwrap(); - let path_str = path.to_string_lossy().to_string(); events.push(SlashCommandEvent::StartSection { icon: IconName::File, @@ -155,9 +154,9 @@ pub fn add_search_result_section( metadata: None, ensure_newline: false, }); - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }); + })); events.push(SlashCommandEvent::EndSection { metadata: None }); } diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index 564df7cfc2558..d289e0e4f12f9 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -1,7 +1,8 @@ use super::SlashCommand; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ - ArgumentCompletion, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, + SlashCommandResult, }; use editor::Editor; use futures::stream::{self, StreamExt}; @@ -76,7 +77,6 @@ impl SlashCommand for OutlineSlashCommand { outline_text.push_str(&item.string); outline_text.push('\n'); } - let events = vec![ SlashCommandEvent::StartSection { icon: IconName::ListTree, @@ -84,10 +84,10 @@ impl SlashCommand for OutlineSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text: outline_text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]; diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 281144e53b644..0be38a48cd149 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use futures::stream::{self, StreamExt}; use gpui::{AppContext, Task, View, WeakView}; @@ -95,10 +95,10 @@ impl SlashCommand for TerminalSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]; diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index ad1c7f66fabcb..33914a7b1eafe 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/crates/assistant/src/slash_command/workflow_command.rs @@ -5,8 +5,8 @@ use std::sync::atomic::AtomicBool; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, }; use futures::stream::{self, StreamExt}; use gpui::{Task, WeakView}; @@ -72,10 +72,10 @@ impl SlashCommand for WorkflowSlashCommand { metadata: None, ensure_newline: false, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text, run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]) .boxed()) diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 7775d96c36d49..0833738e47dc6 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -2,7 +2,7 @@ mod slash_command_registry; use anyhow::Result; use futures::stream::{BoxStream, StreamExt}; -use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; +use gpui::{AnyElement, AppContext, ElementId, Image, SharedString, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; use serde::{Deserialize, Serialize}; @@ -100,6 +100,15 @@ pub type RenderFoldPlaceholder = Arc< + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, >; +pub enum SlashCommandContentType { + Text { + text: String, + run_commands_in_text: bool, + }, + Image { + image: Image, + }, +} pub enum SlashCommandEvent { StartMessage { role: Role, @@ -111,10 +120,7 @@ pub enum SlashCommandEvent { metadata: Option, ensure_newline: bool, }, - Content { - text: String, - run_commands_in_text: bool, - }, + Content(SlashCommandContentType), Progress { message: SharedString, complete: f32, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 9cd59dc2b766c..5820a60f3d09b 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; -use assistant_slash_command::SlashCommandEvent; use assistant_slash_command::{ as_stream_vec, ArgumentCompletion, SlashCommand, SlashCommandOutputSection, SlashCommandResult, }; +use assistant_slash_command::{SlashCommandContentType, SlashCommandEvent}; use futures::FutureExt; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; @@ -122,10 +122,10 @@ impl SlashCommand for ExtensionSlashCommand { metadata: None, ensure_newline: true, }, - SlashCommandEvent::Content { - run_commands_in_text: false, + SlashCommandEvent::Content(SlashCommandContentType::Text { text: "let x = 42;\nprintln!(\"The answer is {}\", x);".to_string(), - }, + run_commands_in_text: false, + }), SlashCommandEvent::EndSection { metadata: None }, ]; From b04ef6a5d0d24c49e5f6721be41383f185ca1590 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Wed, 16 Oct 2024 13:48:46 +0100 Subject: [PATCH 14/49] assistant: remove ensure_newline from StartSection --- crates/assistant/src/context.rs | 19 +++---------------- crates/assistant/src/context/context_tests.rs | 2 -- crates/assistant/src/slash_command.rs | 1 - .../slash_command/cargo_workspace_command.rs | 1 - .../slash_command/context_server_command.rs | 1 - .../src/slash_command/default_command.rs | 1 - .../src/slash_command/diagnostics_command.rs | 3 --- .../src/slash_command/docs_command.rs | 1 - .../src/slash_command/fetch_command.rs | 1 - .../src/slash_command/file_command.rs | 1 - .../src/slash_command/now_command.rs | 1 - .../src/slash_command/project_command.rs | 2 -- .../src/slash_command/prompt_command.rs | 1 - .../src/slash_command/search_command.rs | 2 -- .../src/slash_command/symbols_command.rs | 1 - .../src/slash_command/terminal_command.rs | 1 - .../src/slash_command/workflow_command.rs | 1 - .../src/assistant_slash_command.rs | 2 +- .../extension/src/extension_slash_command.rs | 1 - 19 files changed, 4 insertions(+), 39 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f22ba821982a7..91b78382fe904 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,5 +1,4 @@ // todo!() -// - fix file tests // - fix extension // - When slash command wants to insert a message, but it wants to insert it after a message that has the same Role and it emits a `StartMessage { merge_same_roles: bool (name TBD) }`, we should ignore it // - When a section ends, we should run the following code: @@ -1841,7 +1840,6 @@ impl Context { icon: IconName, label: SharedString, metadata: Option, - ensure_newline: bool, } let position = this.update(&mut cx, |this, cx| { @@ -1856,7 +1854,7 @@ impl Context { Vec::new(); let mut pending_section_stack: Vec = Vec::new(); let mut run_commands_in_ranges: Vec> = Vec::new(); - let mut has_newline = false; + let mut text_ends_with_newline = false; let mut last_role: Option = None; while let Some(event) = stream.next().await { @@ -1885,7 +1883,6 @@ impl Context { icon, label, metadata, - ensure_newline, } => { this.read_with(&cx, |this, cx| { let buffer = this.buffer.read(cx); @@ -1895,7 +1892,6 @@ impl Context { icon, label, metadata, - ensure_newline, }); })?; } @@ -1928,16 +1924,7 @@ impl Context { let start = this.buffer.read(cx).anchor_before(position); let result = this.buffer.update(cx, |buffer, cx| { - let ensure_newline = pending_section_stack - .last() - .map(|ps| ps.ensure_newline) - .unwrap_or(false); - let text = if ensure_newline && !text.ends_with('\n') { - text + "\n" - } else { - text - }; - has_newline = text.ends_with("\n"); + text_ends_with_newline = text.ends_with("\n"); buffer.edit([(position..position, text)], None, cx) }); @@ -1963,7 +1950,7 @@ impl Context { let start = pending_section.start; let end = buffer.anchor_before(position); - if !has_newline && ensure_trailing_newline { + if !text_ends_with_newline && ensure_trailing_newline { buffer.edit([(position..position, "\n")], None, cx); } diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 349f7833338d2..74cee9f7e76eb 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -1090,7 +1090,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std icon: IconName::Ai, label: "section".into(), metadata: None, - ensure_newline: false, }); events.push(SlashCommandEvent::Content { text: output_text[section_start..section_end].to_string(), @@ -1443,7 +1442,6 @@ impl SlashCommand for FakeSlashCommand { icon: IconName::Ai, label: "Fake Command".into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content { text: format!("Executed fake command: {}", self.0), diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 1d9e61b34b937..d7d47bc323bdd 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -503,7 +503,6 @@ pub fn buffer_to_output( icon: IconName::File, label: label.into(), metadata, - ensure_newline: true, }); let mut code_content = String::new(); diff --git a/crates/assistant/src/slash_command/cargo_workspace_command.rs b/crates/assistant/src/slash_command/cargo_workspace_command.rs index 22aac18e30696..05b8ed2ab1d69 100644 --- a/crates/assistant/src/slash_command/cargo_workspace_command.rs +++ b/crates/assistant/src/slash_command/cargo_workspace_command.rs @@ -144,7 +144,6 @@ impl SlashCommand for CargoWorkspaceSlashCommand { icon: IconName::FileTree, label: "Project".into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 62ef2cacfe758..a59f46212a4e0 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -161,7 +161,6 @@ impl SlashCommand for ContextServerSlashCommand { icon: IconName::Ai, label: "".into(), metadata: None, - ensure_newline: false, }); match message.content { diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 35fd1a00d4a06..875977a23dab1 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -76,7 +76,6 @@ impl SlashCommand for DefaultSlashCommand { icon: IconName::Library, label: "Default".into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index e7a8b3261d7e5..0a8432d5f675e 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -302,7 +302,6 @@ fn collect_diagnostics( icon: IconName::File, label: file_path.clone().into(), metadata: None, - ensure_newline: false, }); events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: format!("{}\n", file_path), @@ -356,7 +355,6 @@ fn collect_diagnostics( icon: IconName::Warning, label: label.into(), metadata: None, - ensure_newline: false, }, ); @@ -440,7 +438,6 @@ fn collect_diagnostic( icon, label: entry.diagnostic.message.clone().into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index 665e240ed19f1..e596301fdc2a7 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -351,7 +351,6 @@ impl SlashCommand for DocsSlashCommand { icon: IconName::FileDoc, label: format!("docs ({provider})").into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 1c28bf00e8e55..9139e05e7d6b5 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -165,7 +165,6 @@ impl SlashCommand for FetchSlashCommand { icon: IconName::AtSign, label: format!("fetch {}", url).into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 6a0daa44ec29d..defe23006a525 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -290,7 +290,6 @@ fn collect_files( icon: IconName::Folder, label: dirname.clone().into(), metadata: None, - ensure_newline: true, }); events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: dirname, diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 54253104aafb7..ebcf5078674b4 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -59,7 +59,6 @@ impl SlashCommand for NowSlashCommand { icon: IconName::CountdownTimer, label: now.to_rfc2822().into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index fceeee0726f06..2dbfbc036434f 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -130,7 +130,6 @@ impl SlashCommand for ProjectSlashCommand { icon: IconName::Book, label: "Project context".into(), metadata: None, - ensure_newline: false, }); let output = "Project context:\n".to_string(); @@ -145,7 +144,6 @@ impl SlashCommand for ProjectSlashCommand { icon: IconName::MagnifyingGlass, label: query.clone().into(), metadata: None, - ensure_newline: false, }); let mut section_text = format!("Results for {query}:\n"); diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index a4cb8721ce1bb..c7f6915818b66 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -100,7 +100,6 @@ impl SlashCommand for PromptSlashCommand { icon: IconName::Library, label: title, metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text: prompt, diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 6e1fb34e7a683..c26938cf1df05 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -114,7 +114,6 @@ impl SlashCommand for SearchSlashCommand { icon: IconName::MagnifyingGlass, label: SharedString::from(format!("Search results for {query}:")), metadata: None, - ensure_newline: false, }); for loaded_result in loaded_results { @@ -152,7 +151,6 @@ pub fn add_search_result_section( icon: IconName::File, label: path_str.into(), metadata: None, - ensure_newline: false, }); events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index d289e0e4f12f9..1fd610fa4d11d 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -82,7 +82,6 @@ impl SlashCommand for OutlineSlashCommand { icon: IconName::ListTree, label: path.to_string_lossy().to_string().into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text: outline_text, diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 0be38a48cd149..1f839ae225961 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -93,7 +93,6 @@ impl SlashCommand for TerminalSlashCommand { icon: IconName::Terminal, label: "Terminal".into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index 33914a7b1eafe..5740601181490 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/crates/assistant/src/slash_command/workflow_command.rs @@ -70,7 +70,6 @@ impl SlashCommand for WorkflowSlashCommand { icon: IconName::Route, label: "Workflow".into(), metadata: None, - ensure_newline: false, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text, diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 0833738e47dc6..4b534d60ad8d4 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -109,6 +109,7 @@ pub enum SlashCommandContentType { image: Image, }, } + pub enum SlashCommandEvent { StartMessage { role: Role, @@ -118,7 +119,6 @@ pub enum SlashCommandEvent { icon: IconName, label: SharedString, metadata: Option, - ensure_newline: bool, }, Content(SlashCommandContentType), Progress { diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 5820a60f3d09b..e349f2c698f08 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -120,7 +120,6 @@ impl SlashCommand for ExtensionSlashCommand { icon: IconName::Code, label: "Code Output".into(), metadata: None, - ensure_newline: true, }, SlashCommandEvent::Content(SlashCommandContentType::Text { text: "let x = 42;\nprintln!(\"The answer is {}\", x);".to_string(), From 6884ddb4a3900470b1a36ae86b76734c51d919e2 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Wed, 16 Oct 2024 14:23:40 +0100 Subject: [PATCH 15/49] assistant: Make file_command stream --- crates/assistant/src/context.rs | 5 ++- crates/assistant/src/context/context_tests.rs | 21 ++++++---- .../src/slash_command/file_command.rs | 42 ++++++++++++------- .../src/assistant_slash_command.rs | 2 + 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 91b78382fe904..7f0af85dce204 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1846,8 +1846,8 @@ impl Context { this.buffer.update(cx, |buffer, cx| { let start = command_range.start.to_offset(buffer); let end = command_range.end.to_offset(buffer); - buffer.edit([(start..end, "")], None, cx); - buffer.anchor_after(command_range.end) + buffer.edit([(end..end, "\n")], None, cx); + buffer.anchor_after(end + 1) }) })?; let mut finished_sections: Vec> = @@ -1858,6 +1858,7 @@ impl Context { let mut last_role: Option = None; while let Some(event) = stream.next().await { + dbg!(&event); match event { SlashCommandEvent::StartMessage { role, diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 74cee9f7e76eb..7bcf41611271e 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -6,8 +6,8 @@ use crate::{ }; use anyhow::Result; use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandEvent, SlashCommandOutputSection, - SlashCommandRegistry, SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContentType, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, }; use collections::HashSet; use fs::FakeFs; @@ -1080,7 +1080,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std .take(10) .collect::(); - let mut events = vec![SlashCommandEvent::StartMessage { role: Role::User }]; + let mut events = vec![SlashCommandEvent::StartMessage { + role: Role::User, + merge_same_roles: true, + }]; let num_sections = rng.gen_range(0..=3); let mut section_start = 0; @@ -1091,19 +1094,19 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std label: "section".into(), metadata: None, }); - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: output_text[section_start..section_end].to_string(), run_commands_in_text: false, - }); + })); events.push(SlashCommandEvent::EndSection { metadata: None }); section_start = section_end; } if section_start < output_text.len() { - events.push(SlashCommandEvent::Content { + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { text: output_text[section_start..].to_string(), run_commands_in_text: false, - }); + })); } log::info!( @@ -1443,10 +1446,10 @@ impl SlashCommand for FakeSlashCommand { label: "Fake Command".into(), metadata: None, }, - SlashCommandEvent::Content { + SlashCommandEvent::Content(SlashCommandContentType::Text { text: format!("Executed fake command: {}", self.0), run_commands_in_text: false, - }, + }), SlashCommandEvent::EndSection { metadata: None }, ]) .boxed())) diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index defe23006a525..4b9d0acc6c937 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -5,7 +5,10 @@ use super::{ // use super::diagnostics_command::collect_buffer_diagnostics; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandContentType}; -use futures::stream::{self, StreamExt}; +use futures::{ + channel::mpsc, + stream::{self, StreamExt}, +}; use fuzzy::PathMatch; use gpui::{AppContext, Model, Task, View, WeakView}; use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate}; @@ -219,8 +222,8 @@ fn collect_files( .map(|worktree| worktree.read(cx).snapshot()) .collect::>(); + let (events_tx, events_rx) = mpsc::unbounded(); cx.spawn(|mut cx| async move { - let mut events = Vec::new(); for snapshot in snapshots { let worktree_id = snapshot.id(); let mut folded_directory_names = Vec::new(); @@ -252,7 +255,7 @@ fn collect_files( { log::info!("end dir"); directory_stack.pop(); - events.push(SlashCommandEvent::EndSection { metadata: None }); + events_tx.unbounded_send(SlashCommandEvent::EndSection { metadata: None })?; } if entry.is_dir() { @@ -286,15 +289,17 @@ fn collect_files( } else { format!("{}/{}", prefix_paths, &filename) }; - events.push(SlashCommandEvent::StartSection { + events_tx.unbounded_send(SlashCommandEvent::StartSection { icon: IconName::Folder, label: dirname.clone().into(), metadata: None, - }); - events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { - text: dirname, - run_commands_in_text: false, - })); + })?; + events_tx.unbounded_send(SlashCommandEvent::Content( + SlashCommandContentType::Text { + text: dirname, + run_commands_in_text: false, + }, + ))?; directory_stack.push(entry.path.clone()); } else if entry.is_file() { let open_buffer_task = project_handle @@ -307,9 +312,11 @@ fn collect_files( }; if let Some(buffer) = open_buffer_task.await.log_err() { let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; - let mut events_from_buffer = + let events_from_buffer = buffer_to_output(&snapshot, Some(&path_including_worktree_name))?; - events.append(&mut events_from_buffer); + for event in events_from_buffer { + events_tx.unbounded_send(event)?; + } } } } @@ -318,11 +325,13 @@ fn collect_files( while !directory_stack.is_empty() { log::info!("end dir"); directory_stack.pop(); - events.push(SlashCommandEvent::EndSection { metadata: None }); + events_tx.unbounded_send(SlashCommandEvent::EndSection { metadata: None })?; } } - Ok(stream::iter(events).boxed()) + anyhow::Ok(()) }) + .detach(); + Task::ready(Ok(events_rx.boxed())) } /// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix @@ -561,7 +570,12 @@ mod test { // Check content is included in the events let content_events: Vec<_> = events .iter() - .filter(|e| matches!(e, SlashCommandEvent::Content { text, .. })) + .filter(|e| { + matches!( + e, + SlashCommandEvent::Content(SlashCommandContentType::Text { .. }) + ) + }) .collect(); let content = content_events.iter().fold(String::new(), |mut acc, e| { if let SlashCommandEvent::Content(SlashCommandContentType::Text { text, .. }) = e { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 4b534d60ad8d4..384ec3d931fa9 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -100,6 +100,7 @@ pub type RenderFoldPlaceholder = Arc< + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, >; +#[derive(Debug)] pub enum SlashCommandContentType { Text { text: String, @@ -110,6 +111,7 @@ pub enum SlashCommandContentType { }, } +#[derive(Debug)] pub enum SlashCommandEvent { StartMessage { role: Role, From dc80b1b2db0f41b7ba5ed090ff140ad1d01c1ad9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Oct 2024 16:14:49 +0200 Subject: [PATCH 16/49] Don't reparse slash command when inserting a newline at the end of it Co-Authored-By: David --- crates/assistant/src/context.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 7f0af85dce204..d666bd01baf12 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1278,10 +1278,23 @@ impl Context { .edits_since_last_parse .consume() .into_iter() - .map(|edit| { - let start_row = buffer.offset_to_point(edit.new.start).row; - let end_row = buffer.offset_to_point(edit.new.end).row + 1; - start_row..end_row + .filter_map(|edit| { + let new_start = buffer.offset_to_point(edit.new.start); + let new_end = buffer.offset_to_point(edit.new.end); + let mut start_row = new_start.row; + let end_row = new_end.row + 1; + if edit.old_len() == 0 + && new_start.column == buffer.line_len(new_start.row) + && buffer.chars_at(new_start).next() == Some('\n') + { + start_row += 1; + } + + if start_row <= end_row { + Some(start_row..end_row) + } else { + None + } }) .peekable(); @@ -1373,7 +1386,7 @@ impl Context { .last() .map_or(command_line.name.end, |argument| argument.end); let source_range = - buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); + buffer.anchor_after(start_ix)..buffer.anchor_before(end_ix); let pending_command = PendingSlashCommand { name: name.to_string(), arguments, From f2873f779d5877849d9da36844993c006de4985f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Oct 2024 16:15:54 +0200 Subject: [PATCH 17/49] Always insert a trailing newline after a section ends Co-Authored-By: David --- crates/assistant/src/context.rs | 4 +--- crates/assistant/src/slash_command/file_command.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index d666bd01baf12..250cb342e8c66 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1964,9 +1964,7 @@ impl Context { let start = pending_section.start; let end = buffer.anchor_before(position); - if !text_ends_with_newline && ensure_trailing_newline { - buffer.edit([(position..position, "\n")], None, cx); - } + buffer.edit([(position..position, "\n")], None, cx); log::info!("Slash command output section end: {:?}", end); diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 4b9d0acc6c937..0ed0e5a1e5393 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -330,7 +330,7 @@ fn collect_files( } anyhow::Ok(()) }) - .detach(); + .detach_and_log_err(cx); Task::ready(Ok(events_rx.boxed())) } From 932b3334aab1ee693666f8487ed36d9ef33c0ff4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Oct 2024 18:34:32 +0200 Subject: [PATCH 18/49] Show a block for invoked slash commands that haven't finished yet Co-Authored-By: Marshall --- crates/assistant/src/assistant_panel.rs | 157 +++++++----- crates/assistant/src/context.rs | 225 +++++++++++------- crates/assistant/src/context/context_tests.rs | 2 +- .../slash_command/context_server_command.rs | 7 +- .../src/assistant_slash_command.rs | 10 +- .../extension/src/extension_slash_command.rs | 6 +- 6 files changed, 240 insertions(+), 167 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 88a612dcf946c..34776108735f0 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -13,10 +13,11 @@ use crate::{ terminal_inline_assistant::TerminalInlineAssistant, Assist, CacheStatus, ConfirmCommand, Content, Context, ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, DeployPromptLibrary, - InlineAssistId, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, Message, MessageId, - MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, - PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, - SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepResolution, + InlineAssistId, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, + InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus, + ModelPickerDelegate, ModelSelector, NewContext, ParsedSlashCommand, PendingSlashCommandStatus, + QuoteSelection, RemoteContextMetadata, SavedContextMetadata, SlashCommandId, Split, + ToggleFocus, ToggleModelSelector, WorkflowStepResolution, }; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; @@ -41,7 +42,7 @@ use gpui::{ Context as _, Empty, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, - Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext, + Transformation, UpdateGlobal, View, VisualContext, WeakModel, WeakView, WindowContext, }; use indexed_docs::IndexedDocsStore; use language::{ @@ -1522,7 +1523,7 @@ pub struct ContextEditor { scroll_position: Option, remote_id: Option, pending_slash_command_creases: HashMap, CreaseId>, - pending_slash_command_blocks: HashMap, CustomBlockId>, + invoked_slash_command_blocks: HashMap, pending_tool_use_creases: HashMap, CreaseId>, _subscriptions: Vec, workflow_steps: HashMap, WorkflowStepViewState>, @@ -1593,7 +1594,7 @@ impl ContextEditor { workspace, project, pending_slash_command_creases: HashMap::default(), - pending_slash_command_blocks: HashMap::default(), + invoked_slash_command_blocks: HashMap::default(), pending_tool_use_creases: HashMap::default(), _subscriptions, workflow_steps: HashMap::default(), @@ -1618,7 +1619,7 @@ impl ContextEditor { }); let command = self.context.update(cx, |context, cx| { context.reparse(cx); - context.pending_slash_commands()[0].clone() + context.parsed_slash_commands()[0].clone() }); self.run_command( command.source_range, @@ -2071,11 +2072,10 @@ impl ContextEditor { ContextEvent::WorkflowStepsUpdated { removed, updated } => { self.workflow_steps_updated(removed, updated, cx); } - ContextEvent::PendingSlashCommandsUpdated { removed, updated } => { + ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { self.editor.update(cx, |editor, cx| { let buffer = editor.buffer().read(cx).snapshot(cx); - let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); - let excerpt_id = *excerpt_id; + let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); editor.remove_creases( removed @@ -2084,16 +2084,6 @@ impl ContextEditor { cx, ); - editor.remove_blocks( - HashSet::from_iter( - removed.iter().filter_map(|range| { - self.pending_slash_command_blocks.remove(range) - }), - ), - None, - cx, - ); - let crease_ids = editor.insert_creases( updated.iter().map(|command| { let workspace = self.workspace.clone(); @@ -2161,46 +2151,17 @@ impl ContextEditor { cx, ); - let block_ids = editor.insert_blocks( - updated - .iter() - .filter_map(|command| match &command.status { - PendingSlashCommandStatus::Error(error) => { - Some((command, error.clone())) - } - _ => None, - }) - .map(|(command, error_message)| BlockProperties { - style: BlockStyle::Fixed, - position: Anchor { - buffer_id: Some(buffer_id), - excerpt_id, - text_anchor: command.source_range.start, - }, - height: 1, - disposition: BlockDisposition::Below, - render: slash_command_error_block_renderer(error_message), - priority: 0, - }), - None, - cx, - ); - self.pending_slash_command_creases.extend( updated .iter() .map(|command| command.source_range.clone()) .zip(crease_ids), ); - - self.pending_slash_command_blocks.extend( - updated - .iter() - .map(|command| command.source_range.clone()) - .zip(block_ids), - ); }) } + ContextEvent::InvokedSlashCommandChanged { command_id } => { + self.update_invoked_slash_command(*command_id, cx); + } ContextEvent::SlashCommandFinished { output_range: _output_range, sections, @@ -2309,6 +2270,57 @@ impl ContextEditor { } } + fn update_invoked_slash_command( + &mut self, + command_id: SlashCommandId, + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + if let Some(invoked_slash_command) = + self.context.read(cx).invoked_slash_command(&command_id) + { + if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + editor.remove_blocks( + HashSet::from_iter(self.invoked_slash_command_blocks.remove(&command_id)), + None, + cx, + ) + } else if self.invoked_slash_command_blocks.contains_key(&command_id) { + cx.notify(); + } else { + let buffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); + let context = self.context.downgrade(); + let block_ids = editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Fixed, + position: Anchor { + buffer_id: Some(buffer_id), + excerpt_id, + text_anchor: invoked_slash_command.position, + }, + height: 1, + disposition: BlockDisposition::Above, + render: invoked_slash_command_renderer(command_id, context), + priority: 0, + }], + None, + cx, + ); + + self.invoked_slash_command_blocks + .insert(command_id, block_ids[0]); + } + } else { + editor.remove_blocks( + HashSet::from_iter(self.invoked_slash_command_blocks.remove(&command_id)), + None, + cx, + ) + }; + }); + } + fn workflow_steps_updated( &mut self, removed: &Vec>, @@ -5567,7 +5579,7 @@ fn render_pending_slash_command_gutter_decoration( fn render_docs_slash_command_trailer( row: MultiBufferRow, - command: PendingSlashCommand, + command: ParsedSlashCommand, cx: &mut WindowContext, ) -> AnyElement { if command.arguments.is_empty() { @@ -5651,16 +5663,33 @@ fn make_lsp_adapter_delegate( }) } -fn slash_command_error_block_renderer(message: String) -> RenderBlock { - Box::new(move |_| { - div() - .pl_6() - .child( - Label::new(format!("error: {}", message)) - .single_line() - .color(Color::Error), - ) - .into_any() +fn invoked_slash_command_renderer( + command_id: SlashCommandId, + context: WeakModel, +) -> RenderBlock { + Box::new(move |cx| { + let Some(context) = context.upgrade() else { + return Empty.into_any(); + }; + + let Some(command) = context.read(cx).invoked_slash_command(&command_id) else { + return Empty.into_any(); + }; + + match &command.status { + InvokedSlashCommandStatus::Running(_) => { + div().pl_6().child("Running command").into_any() + } + InvokedSlashCommandStatus::Error(message) => div() + .pl_6() + .child( + Label::new(format!("error: {}", message)) + .single_line() + .color(Color::Error), + ) + .into_any_element(), + InvokedSlashCommandStatus::Finished => Empty.into_any(), + } }) } diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 250cb342e8c66..5017128f81939 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -343,9 +343,12 @@ pub enum ContextEvent { removed: Vec>, updated: Vec>, }, - PendingSlashCommandsUpdated { + InvokedSlashCommandChanged { + command_id: SlashCommandId, + }, + ParsedSlashCommandsUpdated { removed: Vec>, - updated: Vec, + updated: Vec, }, SlashCommandFinished { output_range: Range, @@ -509,7 +512,8 @@ pub struct Context { pending_ops: Vec, operations: Vec, buffer: Model, - pending_slash_commands: Vec, + parsed_slash_commands: Vec, + invoked_slash_commands: HashMap, edits_since_last_parse: language::Subscription, finished_slash_commands: HashSet, slash_command_output_sections: Vec>, @@ -539,7 +543,7 @@ trait ContextAnnotation { fn range(&self) -> &Range; } -impl ContextAnnotation for PendingSlashCommand { +impl ContextAnnotation for ParsedSlashCommand { fn range(&self) -> &Range { &self.source_range } @@ -611,7 +615,8 @@ impl Context { message_anchors: Default::default(), contents: Default::default(), messages_metadata: Default::default(), - pending_slash_commands: Vec::new(), + parsed_slash_commands: Vec::new(), + invoked_slash_commands: HashMap::default(), finished_slash_commands: HashSet::default(), pending_tool_uses_by_id: HashMap::default(), slash_command_output_sections: Vec::new(), @@ -1013,8 +1018,15 @@ impl Context { .binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer)) } - pub fn pending_slash_commands(&self) -> &[PendingSlashCommand] { - &self.pending_slash_commands + pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] { + &self.parsed_slash_commands + } + + pub fn invoked_slash_command( + &self, + command_id: &SlashCommandId, + ) -> Option<&InvokedSlashCommand> { + self.invoked_slash_commands.get(command_id) } pub fn slash_command_output_sections(&self) -> &[SlashCommandOutputSection] { @@ -1335,7 +1347,7 @@ impl Context { } if !updated_slash_commands.is_empty() || !removed_slash_command_ranges.is_empty() { - cx.emit(ContextEvent::PendingSlashCommandsUpdated { + cx.emit(ContextEvent::ParsedSlashCommandsUpdated { removed: removed_slash_command_ranges, updated: updated_slash_commands, }); @@ -1353,7 +1365,7 @@ impl Context { &mut self, range: Range, buffer: &BufferSnapshot, - updated: &mut Vec, + updated: &mut Vec, removed: &mut Vec>, cx: &AppContext, ) { @@ -1387,7 +1399,7 @@ impl Context { .map_or(command_line.name.end, |argument| argument.end); let source_range = buffer.anchor_after(start_ix)..buffer.anchor_before(end_ix); - let pending_command = PendingSlashCommand { + let pending_command = ParsedSlashCommand { name: name.to_string(), arguments, source_range, @@ -1402,7 +1414,7 @@ impl Context { offset = lines.offset(); } - let removed_commands = self.pending_slash_commands.splice(old_range, new_commands); + let removed_commands = self.parsed_slash_commands.splice(old_range, new_commands); removed.extend(removed_commands.map(|command| command.source_range)); } @@ -1775,15 +1787,15 @@ impl Context { &mut self, position: language::Anchor, cx: &mut ModelContext, - ) -> Option<&mut PendingSlashCommand> { + ) -> Option<&mut ParsedSlashCommand> { let buffer = self.buffer.read(cx); match self - .pending_slash_commands + .parsed_slash_commands .binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer)) { - Ok(ix) => Some(&mut self.pending_slash_commands[ix]), + Ok(ix) => Some(&mut self.parsed_slash_commands[ix]), Err(ix) => { - let cmd = self.pending_slash_commands.get_mut(ix)?; + let cmd = self.parsed_slash_commands.get_mut(ix)?; if position.cmp(&cmd.source_range.start, buffer).is_ge() && position.cmp(&cmd.source_range.end, buffer).is_le() { @@ -1799,9 +1811,9 @@ impl Context { &self, range: Range, cx: &AppContext, - ) -> &[PendingSlashCommand] { + ) -> &[ParsedSlashCommand] { let range = self.pending_command_indices_for_range(range, cx); - &self.pending_slash_commands[range] + &self.parsed_slash_commands[range] } fn pending_command_indices_for_range( @@ -1809,7 +1821,7 @@ impl Context { range: Range, cx: &AppContext, ) -> Range { - self.indices_intersecting_buffer_range(&self.pending_slash_commands, range, cx) + self.indices_intersecting_buffer_range(&self.parsed_slash_commands, range, cx) } fn indices_intersecting_buffer_range( @@ -1841,11 +1853,16 @@ impl Context { expand_result: bool, cx: &mut ModelContext, ) { + let command_id = SlashCommandId(self.next_timestamp()); + + let insert_position = self.buffer.update(cx, |buffer, cx| { + buffer.edit([(command_range.clone(), "")], None, cx); + command_range.end.bias_right(buffer) + }); self.reparse(cx); - let insert_output_task = cx.spawn(|this, mut cx| { - let command_range = command_range.clone(); - async move { + let insert_output_task = cx.spawn(|this, mut cx| async move { + let run_command = async { let mut stream = output.await?; struct PendingSection { @@ -1855,14 +1872,6 @@ impl Context { metadata: Option, } - let position = this.update(&mut cx, |this, cx| { - this.buffer.update(cx, |buffer, cx| { - let start = command_range.start.to_offset(buffer); - let end = command_range.end.to_offset(buffer); - buffer.edit([(end..end, "\n")], None, cx); - buffer.anchor_after(end + 1) - }) - })?; let mut finished_sections: Vec> = Vec::new(); let mut pending_section_stack: Vec = Vec::new(); @@ -1879,9 +1888,9 @@ impl Context { } => { if !merge_same_roles && Some(role) != last_role { this.update(&mut cx, |this, cx| { - let offset = this - .buffer - .read_with(cx, |buffer, _cx| position.to_offset(buffer)); + let offset = this.buffer.read_with(cx, |buffer, _cx| { + insert_position.to_offset(buffer) + }); this.insert_message_at_offset( offset, role, @@ -1900,9 +1909,12 @@ impl Context { } => { this.read_with(&cx, |this, cx| { let buffer = this.buffer.read(cx); - log::info!("Slash command output section start: {:?}", position); + log::info!( + "Slash command output section start: {:?}", + insert_position + ); pending_section_stack.push(PendingSection { - start: buffer.anchor_before(position), + start: buffer.anchor_before(insert_position), icon, label, metadata, @@ -1921,7 +1933,7 @@ impl Context { LanguageModelImage::from_image(image, cx).shared(); this.insert_content( Content::Image { - anchor: position, + anchor: insert_position, image_id, image: image_task, render_image, @@ -1935,14 +1947,18 @@ impl Context { run_commands_in_text, } => { this.update(&mut cx, |this, cx| { - let start = this.buffer.read(cx).anchor_before(position); + let start = this.buffer.read(cx).anchor_before(insert_position); let result = this.buffer.update(cx, |buffer, cx| { text_ends_with_newline = text.ends_with("\n"); - buffer.edit([(position..position, text)], None, cx) + buffer.edit( + [(insert_position..insert_position, text)], + None, + cx, + ) }); - let end = this.buffer.read(cx).anchor_before(position); + let end = this.buffer.read(cx).anchor_before(insert_position); if run_commands_in_text { run_commands_in_ranges.push(start..end); } @@ -1962,9 +1978,13 @@ impl Context { this.update(&mut cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { let start = pending_section.start; - let end = buffer.anchor_before(position); + let end = buffer.anchor_before(insert_position); - buffer.edit([(position..position, "\n")], None, cx); + buffer.edit( + [(insert_position..insert_position, "\n")], + None, + cx, + ); log::info!("Slash command output section end: {:?}", end); @@ -1986,58 +2006,76 @@ impl Context { } } assert!(pending_section_stack.is_empty()); - this.update(&mut cx, |this, cx| { - let command_id = SlashCommandId(this.next_timestamp()); - this.finished_slash_commands.insert(command_id); - let version = this.version.clone(); - let (op, ev) = this.buffer.update(cx, |buffer, _cx| { - let start = command_range.start; - let output_range = start..position; + anyhow::Ok(()) + }; - this.slash_command_output_sections - .sort_by(|a, b| a.range.cmp(&b.range, buffer)); - finished_sections.sort_by(|a, b| a.range.cmp(&b.range, buffer)); - - // Remove the command range from the buffer - ( - ContextOperation::SlashCommandFinished { - id: command_id, - output_range: output_range.clone(), - sections: finished_sections.clone(), - version, - }, - ContextEvent::SlashCommandFinished { - output_range, - sections: finished_sections, - run_commands_in_ranges, - expand_result, - }, - ) - }); + let command_result = run_command.await; - this.push_op(op, cx); - cx.emit(ev); - }) - } - }); + this.update(&mut cx, |this, cx| { + let Some(invoked_slash_command) = this.invoked_slash_commands.get_mut(&command_id) + else { + return; + }; + match command_result { + Ok(()) => { + invoked_slash_command.status = InvokedSlashCommandStatus::Finished; + } + Err(error) => { + invoked_slash_command.status = + InvokedSlashCommandStatus::Error(error.to_string().into()); + } + } - let insert_output_task = cx.background_executor().spawn(async move { - if let Err(error) = insert_output_task.await { - log::error!("failed to run command: {:?}", error) - } + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + }) + .ok(); + + // todo!("make inserting sections and emitting operations streaming") + // async move { + // this.update(&mut cx, |this, cx| { + // this.finished_slash_commands.insert(command_id); + + // let version = this.version.clone(); + // let (op, ev) = this.buffer.update(cx, |buffer, _cx| { + // let start = command_range.start; + // let output_range = start..insert_position; + + // this.slash_command_output_sections + // .sort_by(|a, b| a.range.cmp(&b.range, buffer)); + // finished_sections.sort_by(|a, b| a.range.cmp(&b.range, buffer)); + + // // Remove the command range from the buffer + // ( + // ContextOperation::SlashCommandFinished { + // id: command_id, + // output_range: output_range.clone(), + // sections: finished_sections.clone(), + // version, + // }, + // ContextEvent::SlashCommandFinished { + // output_range, + // sections: finished_sections, + // run_commands_in_ranges, + // expand_result, + // }, + // ) + // }); + + // this.push_op(op, cx); + // cx.emit(ev); + // }) + // } }); - // We are inserting a pending command and update it. - if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) { - pending_command.status = PendingSlashCommandStatus::Running { - _task: insert_output_task.shared(), - }; - cx.emit(ContextEvent::PendingSlashCommandsUpdated { - removed: vec![pending_command.source_range.clone()], - updated: vec![pending_command.clone()], - }); - } + self.invoked_slash_commands.insert( + command_id, + InvokedSlashCommand { + position: command_range.start, + status: InvokedSlashCommandStatus::Running(insert_output_task), + }, + ); + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); } pub fn insert_tool_output( @@ -3003,13 +3041,26 @@ impl ContextVersion { } #[derive(Debug, Clone)] -pub struct PendingSlashCommand { +pub struct ParsedSlashCommand { pub name: String, pub arguments: SmallVec<[String; 3]>, pub status: PendingSlashCommandStatus, pub source_range: Range, } +#[derive(Debug)] +pub struct InvokedSlashCommand { + pub position: language::Anchor, + pub status: InvokedSlashCommandStatus, +} + +#[derive(Debug)] +pub enum InvokedSlashCommandStatus { + Running(Task<()>), + Error(SharedString), + Finished, +} + #[derive(Debug, Clone)] pub enum PendingSlashCommandStatus { Idle, diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 7bcf41611271e..957671bb27df7 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -387,7 +387,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { cx.subscribe(&context, { let ranges = output_ranges.clone(); move |_, _, event, _| match event { - ContextEvent::PendingSlashCommandsUpdated { removed, updated } => { + ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { for range in removed { ranges.borrow_mut().remove(range); } diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index a59f46212a4e0..16cc63d184da3 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -1,14 +1,15 @@ use super::create_label_for_command; use anyhow::{anyhow, Result}; use assistant_slash_command::{ - as_stream_vec, AfterCompletion, ArgumentCompletion, Role, SlashCommand, - SlashCommandContentType, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, + AfterCompletion, ArgumentCompletion, Role, SlashCommand, SlashCommandContentType, + SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, }; use collections::HashMap; use context_servers::{ manager::{ContextServer, ContextServerManager}, types::{Prompt, SamplingContent, SamplingRole}, }; +use futures::StreamExt; use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use std::sync::atomic::AtomicBool; @@ -185,7 +186,7 @@ impl SlashCommand for ContextServerSlashCommand { events.push(SlashCommandEvent::EndSection { metadata: None }); } - Ok(as_stream_vec(events)) + Ok(futures::stream::iter(events).boxed()) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 384ec3d931fa9..94ba7b86f5a78 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -1,7 +1,7 @@ mod slash_command_registry; use anyhow::Result; -use futures::stream::{BoxStream, StreamExt}; +use futures::stream::BoxStream; use gpui::{AnyElement, AppContext, ElementId, Image, SharedString, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; @@ -132,14 +132,6 @@ pub enum SlashCommandEvent { }, } -pub fn as_stream(event: SlashCommandEvent) -> BoxStream<'static, SlashCommandEvent> { - futures::stream::once(async { event }).boxed() -} - -pub fn as_stream_vec(events: Vec) -> BoxStream<'static, SlashCommandEvent> { - futures::stream::iter(events.into_iter()).boxed() -} - #[derive(Debug, Default, PartialEq)] pub struct SlashCommandMessage { pub role: Option, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index e349f2c698f08..cb094d0a5e6d5 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -1,9 +1,9 @@ use anyhow::{anyhow, Result}; use assistant_slash_command::{ - as_stream_vec, ArgumentCompletion, SlashCommand, SlashCommandOutputSection, SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandOutputSection, SlashCommandResult, }; use assistant_slash_command::{SlashCommandContentType, SlashCommandEvent}; -use futures::FutureExt; +use futures::{FutureExt, StreamExt}; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; @@ -128,7 +128,7 @@ impl SlashCommand for ExtensionSlashCommand { SlashCommandEvent::EndSection { metadata: None }, ]; - return Ok(as_stream_vec(events)); + return Ok(futures::stream::iter(events).boxed()); }) } } From 813e1069620f8542481e5a4022489c324c7e1195 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 13:03:18 -0400 Subject: [PATCH 19/49] Remove TODO Co-authored-by: Antonio --- crates/assistant/src/context.rs | 37 --------------------------------- 1 file changed, 37 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 5017128f81939..319cfd7fc198d 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,40 +1,3 @@ -// todo!() -// - fix extension -// - When slash command wants to insert a message, but it wants to insert it after a message that has the same Role and it emits a `StartMessage { merge_same_roles: bool (name TBD) }`, we should ignore it -// - When a section ends, we should run the following code: -// // this.slash_command_output_sections -// // .extend(sections.iter().cloned()); -// // this.slash_command_output_sections -// // .sort_by(|a, b| a.range.cmp(&b.range, buffer)); - -// // let output_range = -// // buffer.anchor_after(start)..buffer.anchor_before(new_end); -// // this.finished_slash_commands.insert(command_id); - -// // ( -// // ContextOperation::SlashCommandFinished { -// // id: command_id, -// // output_range: output_range.clone(), -// // sections: sections.clone(), -// // version, -// // }, -// // ContextEvent::SlashCommandFinished { -// // output_range, -// // sections, -// // run_commands_in_output: output.run_commands_in_text, -// // expand_result, -// // }, -// // ) -// - Animation example: -// Icon::new(IconName::ArrowCircle) -// .size(IconSize::Small) -// .with_animation( -// "arrow-circle", -// Animation::new(Duration::from_secs(2)).repeat(), -// |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), -// ) -// .into_any_element(), - #[cfg(test)] mod context_tests; From e8407f8603b45149ab5fb74ea4bc46a128e61220 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 13:39:32 -0400 Subject: [PATCH 20/49] Remove image support --- Cargo.lock | 2 -- crates/assistant/Cargo.toml | 2 -- crates/assistant/src/context.rs | 20 ------------------- .../src/slash_command/file_command.rs | 16 --------------- .../src/assistant_slash_command.rs | 5 +---- 5 files changed, 1 insertion(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7af47bccaf9a..ebf2e81b27760 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,7 +375,6 @@ dependencies = [ "assistant_slash_command", "assistant_tool", "async-watch", - "base64 0.22.1", "cargo_toml", "chrono", "client", @@ -397,7 +396,6 @@ dependencies = [ "heed", "html_to_markdown", "http_client", - "image", "indexed_docs", "indoc", "language", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 19bed8f4f0c33..21153b6fcc39d 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -27,7 +27,6 @@ assets.workspace = true assistant_slash_command.workspace = true assistant_tool.workspace = true async-watch.workspace = true -base64.workspace = true cargo_toml.workspace = true chrono.workspace = true client.workspace = true @@ -47,7 +46,6 @@ handlebars.workspace = true heed.workspace = true html_to_markdown.workspace = true http_client.workspace = true -image.workspace = true indexed_docs.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index d959e363159ca..dec2dad2dcdc9 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1778,26 +1778,6 @@ impl Context { })?; } SlashCommandEvent::Content(content) => match content { - SlashCommandContentType::Image { image } => { - this.update(&mut cx, |this, cx| { - let Some(render_image) = image.to_image_data(cx).log_err() - else { - return; - }; - let image_id = image.id(); - let image_task = - LanguageModelImage::from_image(image, cx).shared(); - this.insert_content( - Content::Image { - anchor: insert_position, - image_id, - image: image_task, - render_image, - }, - cx, - ); - })?; - } SlashCommandContentType::Text { text, run_commands_in_text, diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index c3e9830323031..305333ffc1e86 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -628,22 +628,6 @@ mod test { // Check content of events with pattern matching let mut i = 0; - // Check we get all expected events - let events_str = events - .iter() - .map(|e| match e { - SlashCommandEvent::StartSection { label, .. } => format!("StartSection: {}", label), - SlashCommandEvent::Content(SlashCommandContentType::Text { text, .. }) => { - format!("Content: {}", text) - } - SlashCommandEvent::Content(SlashCommandContentType::Image { .. }) => { - "Content: Image".to_string() - } - SlashCommandEvent::EndSection { .. } => "EndSection".to_string(), - _ => "Unknown event".to_string(), - }) - .collect::>() - .join("\n"); for event in &events { match event { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 94ba7b86f5a78..7ef41447b1aa9 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -2,7 +2,7 @@ mod slash_command_registry; use anyhow::Result; use futures::stream::BoxStream; -use gpui::{AnyElement, AppContext, ElementId, Image, SharedString, Task, WeakView, WindowContext}; +use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; use serde::{Deserialize, Serialize}; @@ -106,9 +106,6 @@ pub enum SlashCommandContentType { text: String, run_commands_in_text: bool, }, - Image { - image: Image, - }, } #[derive(Debug)] From 1963c81a994e528121c7598e98c0c6ddab722888 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 13:52:33 -0400 Subject: [PATCH 21/49] Map extension slash command output to streaming interface --- .../extension/src/extension_slash_command.rs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index cb094d0a5e6d5..fff43b218af4c 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -1,3 +1,5 @@ +use std::sync::{atomic::AtomicBool, Arc}; + use anyhow::{anyhow, Result}; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutputSection, SlashCommandResult, @@ -6,7 +8,6 @@ use assistant_slash_command::{SlashCommandContentType, SlashCommandEvent}; use futures::{FutureExt, StreamExt}; use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; -use std::sync::{atomic::AtomicBool, Arc}; use ui::prelude::*; use wasmtime_wasi::WasiView; use workspace::Workspace; @@ -113,22 +114,27 @@ impl SlashCommand for ExtensionSlashCommand { .await }); cx.foreground_executor().spawn(async move { - let _output = output.await?; + let output = output.await?; + let mut events = Vec::new(); - let events = vec![ - SlashCommandEvent::StartSection { + for section in output.sections { + events.push(SlashCommandEvent::StartSection { icon: IconName::Code, - label: "Code Output".into(), + label: section.label.into(), metadata: None, - }, - SlashCommandEvent::Content(SlashCommandContentType::Text { - text: "let x = 42;\nprintln!(\"The answer is {}\", x);".to_string(), + }); + events.push(SlashCommandEvent::Content(SlashCommandContentType::Text { + text: output + .text + .get(section.range.start as usize..section.range.end as usize) + .unwrap_or_default() + .to_string(), run_commands_in_text: false, - }), - SlashCommandEvent::EndSection { metadata: None }, - ]; + })); + events.push(SlashCommandEvent::EndSection { metadata: None }); + } - return Ok(futures::stream::iter(events).boxed()); + Ok(futures::stream::iter(events).boxed()) }) } } From af73caadde6e24456f6ae60ff2123c492cced9fd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 14:57:52 -0400 Subject: [PATCH 22/49] WIP: Make slash commands return a stream of events --- Cargo.lock | 1 + crates/assistant/src/context/context_tests.rs | 6 ++- .../src/slash_command/auto_command.rs | 3 +- .../slash_command/cargo_workspace_command.rs | 3 +- .../slash_command/context_server_command.rs | 3 +- .../src/slash_command/default_command.rs | 3 +- .../src/slash_command/delta_command.rs | 2 +- .../src/slash_command/diagnostics_command.rs | 6 ++- .../src/slash_command/docs_command.rs | 3 +- .../src/slash_command/fetch_command.rs | 3 +- .../src/slash_command/file_command.rs | 2 +- .../src/slash_command/now_command.rs | 3 +- .../src/slash_command/project_command.rs | 3 +- .../src/slash_command/prompt_command.rs | 3 +- .../src/slash_command/search_command.rs | 1 + .../src/slash_command/symbols_command.rs | 3 +- .../src/slash_command/tab_command.rs | 2 +- .../src/slash_command/terminal_command.rs | 3 +- .../src/slash_command/workflow_command.rs | 2 +- crates/assistant_slash_command/Cargo.toml | 1 + .../src/assistant_slash_command.rs | 53 ++++++++++++++++++- .../extension/src/extension_slash_command.rs | 3 +- 22 files changed, 92 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 845cdb227f954..f9d7947751f75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,6 +453,7 @@ dependencies = [ "anyhow", "collections", "derive_more", + "futures 0.3.30", "gpui", "language", "parking_lot", diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 4d866b4d8b9bb..e1b7448738693 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -1097,7 +1097,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std text: output_text, sections, run_commands_in_text: false, - })), + } + .to_event_stream())), true, false, cx, @@ -1421,6 +1422,7 @@ impl SlashCommand for FakeSlashCommand { text: format!("Executed fake command: {}", self.0), sections: vec![], run_commands_in_text: false, - })) + } + .to_event_stream())) } } diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index 352b5a3ac917e..cc73f36ebf391 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -147,7 +147,8 @@ impl SlashCommand for AutoCommand { text: prompt, sections: Vec::new(), run_commands_in_text: true, - }) + } + .to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/cargo_workspace_command.rs b/crates/assistant/src/slash_command/cargo_workspace_command.rs index 04fa408717bf3..968238d36e0f5 100644 --- a/crates/assistant/src/slash_command/cargo_workspace_command.rs +++ b/crates/assistant/src/slash_command/cargo_workspace_command.rs @@ -147,7 +147,8 @@ impl SlashCommand for CargoWorkspaceSlashCommand { metadata: None, }], run_commands_in_text: false, - }) + } + .to_event_stream()) }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index b749f9e4cd9a5..5b22e76bf87a2 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -185,7 +185,8 @@ impl SlashCommand for ContextServerSlashCommand { }], text: prompt, run_commands_in_text: false, - }) + } + .to_event_stream()) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 2c956f8ca66d9..4d9c9e2ae425e 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -78,7 +78,8 @@ impl SlashCommand for DefaultSlashCommand { }], text, run_commands_in_text: true, - }) + } + .to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs index a17c5d739c68f..b9f6752ccb32e 100644 --- a/crates/assistant/src/slash_command/delta_command.rs +++ b/crates/assistant/src/slash_command/delta_command.rs @@ -104,7 +104,7 @@ impl SlashCommand for DeltaSlashCommand { } } - Ok(output) + Ok(output.to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index 54be2219fff0b..c7475445ce253 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -180,7 +180,11 @@ impl SlashCommand for DiagnosticsSlashCommand { let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx); - cx.spawn(move |_| async move { task.await?.ok_or_else(|| anyhow!("No diagnostics found")) }) + cx.spawn(move |_| async move { + task.await? + .map(|output| output.to_event_stream()) + .ok_or_else(|| anyhow!("No diagnostics found")) + }) } } diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index 92c3cd1977b9b..b54f708e32011 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -356,7 +356,8 @@ impl SlashCommand for DocsSlashCommand { }) .collect(), run_commands_in_text: false, - }) + } + .to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 9b61c547dbd65..4d38bb20a7baa 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -167,7 +167,8 @@ impl SlashCommand for FetchSlashCommand { metadata: None, }], run_commands_in_text: false, - }) + } + .to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 51d0b33ba2624..239fe7d93ec06 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -342,7 +342,7 @@ fn collect_files( } } } - Ok(output) + Ok(output.to_event_stream()) }) } diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index 40bc29f27ddff..cf81bec9265bb 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/crates/assistant/src/slash_command/now_command.rs @@ -63,6 +63,7 @@ impl SlashCommand for NowSlashCommand { metadata: None, }], run_commands_in_text: false, - })) + } + .to_event_stream())) } } diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index e55699b026826..d14cb310ad10d 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -162,7 +162,8 @@ impl SlashCommand for ProjectSlashCommand { text: output, sections, run_commands_in_text: true, - }) + } + .to_event_stream()) }) .await }) diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index dc803293823fb..079d1425af098 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -102,7 +102,8 @@ impl SlashCommand for PromptSlashCommand { metadata: None, }], run_commands_in_text: true, - }) + } + .to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 999fe252becc7..9c4938ce9342b 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -130,6 +130,7 @@ impl SlashCommand for SearchSlashCommand { sections, run_commands_in_text: false, } + .to_event_stream() }) .await; diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index d28b53c1a1389..468c8d7126437 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -85,7 +85,8 @@ impl SlashCommand for OutlineSlashCommand { }], text: outline_text, run_commands_in_text: false, - }) + } + .to_event_stream()) }) }); diff --git a/crates/assistant/src/slash_command/tab_command.rs b/crates/assistant/src/slash_command/tab_command.rs index 23c3b64b38505..771c0765eea7f 100644 --- a/crates/assistant/src/slash_command/tab_command.rs +++ b/crates/assistant/src/slash_command/tab_command.rs @@ -150,7 +150,7 @@ impl SlashCommand for TabSlashCommand { for (full_path, buffer, _) in tab_items_search.await? { append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err(); } - Ok(output) + Ok(output.to_event_stream()) }) } } diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 7516b275ac8b8..2ca1d4041b872 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -97,7 +97,8 @@ impl SlashCommand for TerminalSlashCommand { metadata: None, }], run_commands_in_text: false, - })) + } + .to_event_stream())) } } diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index 1379eb5e803a6..596b37f7a4e22 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/crates/assistant/src/slash_command/workflow_command.rs @@ -75,7 +75,7 @@ impl SlashCommand for WorkflowSlashCommand { metadata: None, }], run_commands_in_text: false, - }) + }.to_event_stream()) }) } } diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index a58a84312fc3e..07543d32f872b 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -15,6 +15,7 @@ path = "src/assistant_slash_command.rs" anyhow.workspace = true collections.workspace = true derive_more.workspace = true +futures.workspace = true gpui.workspace = true language.workspace = true parking_lot.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 90e47690a83d9..b45f972b0949d 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -1,6 +1,8 @@ mod slash_command_registry; use anyhow::Result; +use futures::stream::{self, BoxStream}; +use futures::StreamExt; use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; use serde::{Deserialize, Serialize}; @@ -56,7 +58,7 @@ pub struct ArgumentCompletion { pub replace_previous_arguments: bool, } -pub type SlashCommandResult = Result; +pub type SlashCommandResult = Result>>; pub trait SlashCommand: 'static + Send + Sync { fn name(&self) -> String; @@ -98,6 +100,27 @@ pub type RenderFoldPlaceholder = Arc< + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, >; +#[derive(Debug)] +pub enum SlashCommandContent { + Text { + text: String, + run_commands_in_text: bool, + }, +} + +#[derive(Debug)] +pub enum SlashCommandEvent { + StartSection { + icon: IconName, + label: SharedString, + metadata: Option, + }, + Content(SlashCommandContent), + EndSection { + metadata: Option, + }, +} + #[derive(Debug, Default, PartialEq)] pub struct SlashCommandOutput { pub text: String, @@ -105,6 +128,34 @@ pub struct SlashCommandOutput { pub run_commands_in_text: bool, } +impl SlashCommandOutput { + /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. + pub fn to_event_stream(self) -> BoxStream<'static, Result> { + let mut events = Vec::new(); + + for section in self.sections { + events.push(Ok(SlashCommandEvent::StartSection { + icon: section.icon, + label: section.label, + metadata: section.metadata.clone(), + })); + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + text: self + .text + .get(section.range.start as usize..section.range.end as usize) + .unwrap_or_default() + .to_string(), + run_commands_in_text: self.run_commands_in_text, + }))); + events.push(Ok(SlashCommandEvent::EndSection { + metadata: section.metadata, + })); + } + + stream::iter(events).boxed() + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SlashCommandOutputSection { pub range: Range, diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index e9725f1ae423c..0a10e9e1a25fe 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -128,7 +128,8 @@ impl SlashCommand for ExtensionSlashCommand { }) .collect(), run_commands_in_text: false, - }) + } + .to_event_stream()) }) } } From df0cf4d7d7b3ba78e58477b12ebf3265f9253e50 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 16:46:32 -0400 Subject: [PATCH 23/49] Reassemble `SlashCommandOutput` from events --- Cargo.lock | 1 + crates/assistant/src/context.rs | 6 +- .../src/slash_command/delta_command.rs | 29 +- .../src/slash_command/file_command.rs | 12 + crates/assistant_slash_command/Cargo.toml | 5 + .../src/assistant_slash_command.rs | 321 +++++++++++++++++- 6 files changed, 356 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9d7947751f75..915fdf60e9f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,6 +457,7 @@ dependencies = [ "gpui", "language", "parking_lot", + "pretty_assertions", "serde", "serde_json", "workspace", diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index d2b80ca22491a..f0e3661a051bb 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -7,7 +7,7 @@ use crate::{ }; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ - SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, + SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, }; use assistant_tool::ToolRegistry; use client::{self, proto, telemetry::Telemetry}; @@ -1688,6 +1688,10 @@ impl Context { let command_range = command_range.clone(); async move { let output = output.await; + let output = match output { + Ok(output) => SlashCommandOutput::from_event_stream(output).await, + Err(err) => Err(err), + }; this.update(&mut cx, |this, cx| match output { Ok(mut output) => { // Ensure section ranges are valid. diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs index b9f6752ccb32e..a37d33e2af561 100644 --- a/crates/assistant/src/slash_command/delta_command.rs +++ b/crates/assistant/src/slash_command/delta_command.rs @@ -86,19 +86,22 @@ impl SlashCommand for DeltaSlashCommand { .zip(file_command_new_outputs) { if let Ok(new_output) = new_output { - if let Some(file_command_range) = new_output.sections.first() { - let new_text = &new_output.text[file_command_range.range.clone()]; - if old_text.chars().ne(new_text.chars()) { - output.sections.extend(new_output.sections.into_iter().map( - |section| SlashCommandOutputSection { - range: output.text.len() + section.range.start - ..output.text.len() + section.range.end, - icon: section.icon, - label: section.label, - metadata: section.metadata, - }, - )); - output.text.push_str(&new_output.text); + if let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await + { + if let Some(file_command_range) = new_output.sections.first() { + let new_text = &new_output.text[file_command_range.range.clone()]; + if old_text.chars().ne(new_text.chars()) { + output.sections.extend(new_output.sections.into_iter().map( + |section| SlashCommandOutputSection { + range: output.text.len() + section.range.start + ..output.text.len() + section.range.end, + icon: section.icon, + label: section.label, + metadata: section.metadata, + }, + )); + output.text.push_str(&new_output.text); + } } } } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 239fe7d93ec06..8c6cbd14904a7 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -528,6 +528,7 @@ pub fn append_buffer_to_output( #[cfg(test)] mod test { + use assistant_slash_command::SlashCommandOutput; use fs::FakeFs; use gpui::TestAppContext; use project::Project; @@ -577,6 +578,11 @@ mod test { .update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx)) .await .unwrap(); + let result_1 = SlashCommandOutput::from_event_stream(result_1) + .await + .unwrap(); + + dbg!(&result_1); assert!(result_1.text.starts_with("root/dir")); // 4 files + 2 directories @@ -586,6 +592,9 @@ mod test { .update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx)) .await .unwrap(); + let result_2 = SlashCommandOutput::from_event_stream(result_2) + .await + .unwrap(); assert_eq!(result_1, result_2); @@ -593,6 +602,7 @@ mod test { .update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx)) .await .unwrap(); + let result = SlashCommandOutput::from_event_stream(result).await.unwrap(); assert!(result.text.starts_with("root/dir")); // 5 files + 2 directories @@ -639,6 +649,7 @@ mod test { .update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx)) .await .unwrap(); + let result = SlashCommandOutput::from_event_stream(result).await.unwrap(); // Sanity check assert!(result.text.starts_with("zed/assets/themes\n")); @@ -700,6 +711,7 @@ mod test { .update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx)) .await .unwrap(); + let result = SlashCommandOutput::from_event_stream(result).await.unwrap(); assert!(result.text.starts_with("zed/assets/themes\n")); assert_eq!(result.sections[0].label, "zed/assets/themes/LICENSE"); diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index 07543d32f872b..8ec5b729c9360 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -22,3 +22,8 @@ parking_lot.workspace = true serde.workspace = true serde_json.workspace = true workspace.workspace = true + +[dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index b45f972b0949d..920036876529e 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -100,7 +100,7 @@ pub type RenderFoldPlaceholder = Arc< + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, >; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum SlashCommandContent { Text { text: String, @@ -108,7 +108,7 @@ pub enum SlashCommandContent { }, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum SlashCommandEvent { StartSection { icon: IconName, @@ -121,7 +121,7 @@ pub enum SlashCommandEvent { }, } -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Clone)] pub struct SlashCommandOutput { pub text: String, pub sections: Vec>, @@ -132,8 +132,20 @@ impl SlashCommandOutput { /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. pub fn to_event_stream(self) -> BoxStream<'static, Result> { let mut events = Vec::new(); + let mut last_section_end = 0; for section in self.sections { + if last_section_end < section.range.start { + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + text: self + .text + .get(last_section_end..section.range.start) + .unwrap_or_default() + .to_string(), + run_commands_in_text: self.run_commands_in_text, + }))); + } + events.push(Ok(SlashCommandEvent::StartSection { icon: section.icon, label: section.label, @@ -142,7 +154,7 @@ impl SlashCommandOutput { events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { text: self .text - .get(section.range.start as usize..section.range.end as usize) + .get(section.range.start..section.range.end) .unwrap_or_default() .to_string(), run_commands_in_text: self.run_commands_in_text, @@ -150,10 +162,71 @@ impl SlashCommandOutput { events.push(Ok(SlashCommandEvent::EndSection { metadata: section.metadata, })); + + last_section_end = section.range.end; + } + + if last_section_end < self.text.len() { + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + text: self.text[last_section_end..].to_string(), + run_commands_in_text: self.run_commands_in_text, + }))); } stream::iter(events).boxed() } + + pub async fn from_event_stream( + mut events: BoxStream<'static, Result>, + ) -> Result { + let mut output = SlashCommandOutput::default(); + let mut current_section = None; + + while let Some(event) = events.next().await { + match event? { + SlashCommandEvent::StartSection { + icon, + label, + metadata, + } => { + if let Some(section) = current_section.take() { + output.sections.push(section); + } + + let start = output.text.len(); + current_section = Some(SlashCommandOutputSection { + range: start..start, + icon, + label, + metadata, + }); + } + SlashCommandEvent::Content(SlashCommandContent::Text { + text, + run_commands_in_text, + }) => { + output.text.push_str(&text); + output.run_commands_in_text = run_commands_in_text; + + if let Some(section) = current_section.as_mut() { + section.range.end = output.text.len(); + } + } + SlashCommandEvent::EndSection { metadata } => { + if let Some(mut section) = current_section.take() { + section.metadata = metadata; + output.sections.push(section); + } + } + } + } + + if let Some(section) = current_section { + output.sections.push(section); + } + + Ok(output) + } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -169,3 +242,243 @@ impl SlashCommandOutputSection { self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty() } } + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use serde_json::json; + + use super::*; + + #[gpui::test] + async fn test_slash_command_output_to_events_round_trip() { + // Test basic output consisting of a single section. + { + let text = "Hello, world!".to_string(); + let range = 0..text.len(); + let output = SlashCommandOutput { + text, + sections: vec![SlashCommandOutputSection { + range, + icon: IconName::Code, + label: "Section 1".into(), + metadata: None, + }], + run_commands_in_text: false, + }; + + let events = output.clone().to_event_stream().collect::>().await; + let events = events + .into_iter() + .filter_map(|event| event.ok()) + .collect::>(); + + assert_eq!( + events, + vec![ + SlashCommandEvent::StartSection { + icon: IconName::Code, + label: "Section 1".into(), + metadata: None + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Hello, world!".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { metadata: None } + ] + ); + + let new_output = + SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + .await + .unwrap(); + + assert_eq!(new_output, output); + } + + // Test output where the sections do not comprise all of the text. + { + let text = "Apple\nCucumber\nBanana\n".to_string(); + let output = SlashCommandOutput { + text, + sections: vec![ + SlashCommandOutputSection { + range: 0..6, + icon: IconName::Check, + label: "Fruit".into(), + metadata: None, + }, + SlashCommandOutputSection { + range: 15..22, + icon: IconName::Check, + label: "Fruit".into(), + metadata: None, + }, + ], + run_commands_in_text: false, + }; + + let events = output.clone().to_event_stream().collect::>().await; + let events = events + .into_iter() + .filter_map(|event| event.ok()) + .collect::>(); + + assert_eq!( + events, + vec![ + SlashCommandEvent::StartSection { + icon: IconName::Check, + label: "Fruit".into(), + metadata: None + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Apple\n".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { metadata: None }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Cucumber\n".into(), + run_commands_in_text: false + }), + SlashCommandEvent::StartSection { + icon: IconName::Check, + label: "Fruit".into(), + metadata: None + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Banana\n".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { metadata: None } + ] + ); + + let new_output = + SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + .await + .unwrap(); + + assert_eq!(new_output, output); + } + + // Test output consisting of multiple sections. + { + let text = "Line 1\nLine 2\nLine 3\nLine 4\n".to_string(); + let output = SlashCommandOutput { + text, + sections: vec![ + SlashCommandOutputSection { + range: 0..6, + icon: IconName::FileCode, + label: "Section 1".into(), + metadata: Some(json!({ "a": true })), + }, + SlashCommandOutputSection { + range: 7..13, + icon: IconName::FileDoc, + label: "Section 2".into(), + metadata: Some(json!({ "b": true })), + }, + SlashCommandOutputSection { + range: 14..20, + icon: IconName::FileGit, + label: "Section 3".into(), + metadata: Some(json!({ "c": true })), + }, + SlashCommandOutputSection { + range: 21..27, + icon: IconName::FileToml, + label: "Section 4".into(), + metadata: Some(json!({ "d": true })), + }, + ], + run_commands_in_text: false, + }; + + let events = output.clone().to_event_stream().collect::>().await; + let events = events + .into_iter() + .filter_map(|event| event.ok()) + .collect::>(); + + assert_eq!( + events, + vec![ + SlashCommandEvent::StartSection { + icon: IconName::FileCode, + label: "Section 1".into(), + metadata: Some(json!({ "a": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Line 1".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { + metadata: Some(json!({ "a": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false + }), + SlashCommandEvent::StartSection { + icon: IconName::FileDoc, + label: "Section 2".into(), + metadata: Some(json!({ "b": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Line 2".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { + metadata: Some(json!({ "b": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false + }), + SlashCommandEvent::StartSection { + icon: IconName::FileGit, + label: "Section 3".into(), + metadata: Some(json!({ "c": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Line 3".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { + metadata: Some(json!({ "c": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false + }), + SlashCommandEvent::StartSection { + icon: IconName::FileToml, + label: "Section 4".into(), + metadata: Some(json!({ "d": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "Line 4".into(), + run_commands_in_text: false + }), + SlashCommandEvent::EndSection { + metadata: Some(json!({ "d": true })) + }, + SlashCommandEvent::Content(SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false + }), + ] + ); + + let new_output = + SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + .await + .unwrap(); + + assert_eq!(new_output, output); + } + } +} From 320af9cb47e893d6ec335e30a81cce1727e09396 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 17:04:39 -0400 Subject: [PATCH 24/49] Fix failing randomized test --- crates/assistant/src/context.rs | 12 +----------- .../src/assistant_slash_command.rs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f0e3661a051bb..78237e51b2165 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1694,17 +1694,7 @@ impl Context { }; this.update(&mut cx, |this, cx| match output { Ok(mut output) => { - // Ensure section ranges are valid. - for section in &mut output.sections { - section.range.start = section.range.start.min(output.text.len()); - section.range.end = section.range.end.min(output.text.len()); - while !output.text.is_char_boundary(section.range.start) { - section.range.start -= 1; - } - while !output.text.is_char_boundary(section.range.end) { - section.range.end += 1; - } - } + output.ensure_valid_section_ranges(); // Ensure there is a newline after the last section. if ensure_trailing_newline { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 920036876529e..83f3dd2147a87 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -129,8 +129,23 @@ pub struct SlashCommandOutput { } impl SlashCommandOutput { + pub fn ensure_valid_section_ranges(&mut self) { + for section in &mut self.sections { + section.range.start = section.range.start.min(self.text.len()); + section.range.end = section.range.end.min(self.text.len()); + while !self.text.is_char_boundary(section.range.start) { + section.range.start -= 1; + } + while !self.text.is_char_boundary(section.range.end) { + section.range.end += 1; + } + } + } + /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. - pub fn to_event_stream(self) -> BoxStream<'static, Result> { + pub fn to_event_stream(mut self) -> BoxStream<'static, Result> { + self.ensure_valid_section_ranges(); + let mut events = Vec::new(); let mut last_section_end = 0; From 2e182bec9d30aea8c6dacd434cbd853a41f04a3f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 21:39:57 -0400 Subject: [PATCH 25/49] Remove unused struct --- .../src/assistant_slash_command.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index fca280d6c15e5..1862db2b6e1b9 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -130,14 +130,6 @@ pub enum SlashCommandEvent { }, } -#[derive(Debug, Default, PartialEq)] -pub struct SlashCommandMessage { - pub role: Option, - pub text: String, - pub sections: Vec>, - pub run_commands_in_text: bool, -} - #[derive(Debug, Default, PartialEq, Clone)] pub struct SlashCommandOutput { pub text: String, From 6642fc52b68cde0e01b6a4d0c27db28efc8bf864 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Oct 2024 21:42:00 -0400 Subject: [PATCH 26/49] Remove some of the diff from `main` --- crates/assistant/src/slash_command.rs | 2 +- crates/assistant/src/slash_command/project_command.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 1310e67c579b1..e430e35622a22 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -1,7 +1,7 @@ use crate::assistant_panel::ContextEditor; use anyhow::Result; use assistant_slash_command::AfterCompletion; -pub use assistant_slash_command::{SlashCommand, SlashCommandRegistry}; +pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry}; use editor::{CompletionProvider, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext}; diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 8aacbb38deabe..d14cb310ad10d 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -1,9 +1,10 @@ -use super::{create_label_for_command, search_command::add_search_result_section, SlashCommand}; +use super::{ + create_label_for_command, search_command::add_search_result_section, SlashCommand, + SlashCommandOutput, +}; use crate::PromptBuilder; use anyhow::{anyhow, Result}; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, -}; +use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult}; use feature_flags::FeatureFlag; use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{Anchor, CodeLabel, LspAdapterDelegate}; From a8bcb3d90b348fa099aa2a6b10c69f43927609b4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 24 Oct 2024 11:39:54 -0400 Subject: [PATCH 27/49] Fixed streaming slash command output insertion Co-authored-by: Antonio --- crates/assistant/src/assistant_panel.rs | 3 + crates/assistant/src/context.rs | 88 ++++++++++++++++++------- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 3b2962c1e7a96..6bfc9ee7a9738 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1999,6 +1999,9 @@ impl ContextEditor { ContextEvent::InvokedSlashCommandChanged { command_id } => { self.update_invoked_slash_command(*command_id, cx); } + ContextEvent::SlashCommandOutputSectionAdded { section } => { + self.insert_slash_command_output_sections([section.clone()], false, cx); + } ContextEvent::SlashCommandFinished { output_range: _output_range, sections, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 3ab294fe4a32b..f186be845d96a 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -310,6 +310,9 @@ pub enum ContextEvent { removed: Vec>, updated: Vec, }, + SlashCommandOutputSectionAdded { + section: SlashCommandOutputSection, + }, SlashCommandFinished { output_range: Range, sections: Vec>, @@ -1728,12 +1731,11 @@ impl Context { metadata: Option, } - let mut finished_sections: Vec> = - Vec::new(); let mut pending_section_stack: Vec = Vec::new(); let mut run_commands_in_ranges: Vec> = Vec::new(); let mut text_ends_with_newline = false; let mut last_role: Option = None; + let mut last_section_range = None; while let Some(event) = stream.next().await { let event = event?; @@ -1812,36 +1814,56 @@ impl Context { SlashCommandEvent::EndSection { metadata } => { if let Some(pending_section) = pending_section_stack.pop() { this.update(&mut cx, |this, cx| { - this.buffer.update(cx, |buffer, cx| { - let start = pending_section.start; - let end = buffer.anchor_before(insert_position); - - buffer.edit( - [(insert_position..insert_position, "\n")], - None, - cx, - ); - - log::info!("Slash command output section end: {:?}", end); - - let slash_command_output_section = + let range = pending_section.start + ..insert_position.bias_left(this.buffer.read(cx)); + + let offset_range = range.to_offset(this.buffer.read(cx)); + if !offset_range.is_empty() { + this.buffer.update(cx, |buffer, cx| { + if !buffer.contains_str_at(offset_range.end - 1, "\n") { + buffer.edit( + [(offset_range.end..offset_range.end, "\n")], + None, + cx, + ); + } + }); + this.insert_slash_command_output_section( SlashCommandOutputSection { - range: start..end, + range: range.clone(), icon: pending_section.icon, label: pending_section.label, metadata: metadata.or(pending_section.metadata), - }; - finished_sections - .push(slash_command_output_section.clone()); - this.slash_command_output_sections - .push(slash_command_output_section); - }); + }, + cx, + ); + last_section_range = Some(range); + } })?; } } } } - assert!(pending_section_stack.is_empty()); + + if ensure_trailing_newline { + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + let offset = insert_position.to_offset(buffer); + let newline_offset = offset.saturating_sub(1); + if !buffer.contains_str_at(newline_offset, "\n") + || last_section_range.map_or(false, |last_section_range| { + last_section_range + .to_offset(buffer) + .contains(&newline_offset) + }) + { + buffer.edit([(offset..offset, "\n")], None, cx); + } + }); + })?; + } + + debug_assert!(pending_section_stack.is_empty()); anyhow::Ok(()) }; @@ -1914,6 +1936,26 @@ impl Context { cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); } + fn insert_slash_command_output_section( + &mut self, + section: SlashCommandOutputSection, + cx: &mut ModelContext, + ) { + let buffer = self.buffer.read(cx); + let insertion_ix = match self + .slash_command_output_sections + .binary_search_by(|probe| probe.range.cmp(§ion.range, buffer)) + { + Ok(ix) | Err(ix) => ix, + }; + self.slash_command_output_sections + .insert(insertion_ix, section.clone()); + cx.emit(ContextEvent::SlashCommandOutputSectionAdded { + section: section.clone(), + }); + // todo!("emit an operation that creates a section for collaborators") + } + pub fn insert_tool_output( &mut self, tool_use_id: Arc, From 24cf9c4a45f0daa7a49444e1bc65e4ade08b236d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 24 Oct 2024 11:45:56 -0400 Subject: [PATCH 28/49] Revert changes to parsing to fix slash command running Co-authored-by: Antonio --- crates/assistant/src/context.rs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f186be845d96a..76a8d7cc47c4a 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1253,23 +1253,10 @@ impl Context { .edits_since_last_parse .consume() .into_iter() - .filter_map(|edit| { - let new_start = buffer.offset_to_point(edit.new.start); - let new_end = buffer.offset_to_point(edit.new.end); - let mut start_row = new_start.row; - let end_row = new_end.row + 1; - if edit.old_len() == 0 - && new_start.column == buffer.line_len(new_start.row) - && buffer.chars_at(new_start).next() == Some('\n') - { - start_row += 1; - } - - if start_row <= end_row { - Some(start_row..end_row) - } else { - None - } + .map(|edit| { + let start_row = buffer.offset_to_point(edit.new.start).row; + let end_row = buffer.offset_to_point(edit.new.end).row + 1; + start_row..end_row }) .peekable(); @@ -1361,7 +1348,7 @@ impl Context { .last() .map_or(command_line.name.end, |argument| argument.end); let source_range = - buffer.anchor_after(start_ix)..buffer.anchor_before(end_ix); + buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); let pending_command = ParsedSlashCommand { name: name.to_string(), arguments, From 583fbf772521dd8edd9f5292c173aa164de380df Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 24 Oct 2024 12:52:30 -0400 Subject: [PATCH 29/49] Pass command information to block renderer Co-authored-by: Antonio --- crates/assistant/src/assistant_panel.rs | 32 +++++++++++-------- crates/assistant/src/context.rs | 6 ++++ crates/assistant/src/context/context_tests.rs | 2 ++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 6bfc9ee7a9738..3161e7f4c4581 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1781,6 +1781,8 @@ impl ContextEditor { self.context.update(cx, |context, cx| { context.insert_command_output( command_range, + name, + arguments, output, ensure_trailing_newline, expand_result, @@ -4988,20 +4990,22 @@ fn invoked_slash_command_renderer( return Empty.into_any(); }; - match &command.status { - InvokedSlashCommandStatus::Running(_) => { - div().pl_6().child("Running command").into_any() - } - InvokedSlashCommandStatus::Error(message) => div() - .pl_6() - .child( - Label::new(format!("error: {}", message)) - .single_line() - .color(Color::Error), - ) - .into_any_element(), - InvokedSlashCommandStatus::Finished => Empty.into_any(), - } + h_flex() + .pl_6() + .gap_2() + .child(h_flex().child("/").child(command.name.clone())) + .child(match &command.status { + InvokedSlashCommandStatus::Running(_) => div().child("Running command").into_any(), + InvokedSlashCommandStatus::Error(message) => div() + .child( + Label::new(format!("error: {}", message)) + .single_line() + .color(Color::Error), + ) + .into_any_element(), + InvokedSlashCommandStatus::Finished => Empty.into_any(), + }) + .into_any_element() }) } diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 76a8d7cc47c4a..6593cc24ea784 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1694,6 +1694,8 @@ impl Context { pub fn insert_command_output( &mut self, command_range: Range, + name: &str, + arguments: &[String], output: Task, ensure_trailing_newline: bool, expand_result: bool, @@ -1916,6 +1918,8 @@ impl Context { self.invoked_slash_commands.insert( command_id, InvokedSlashCommand { + name: name.to_string().into(), + arguments: arguments.iter().cloned().map(SharedString::from).collect(), position: command_range.start, status: InvokedSlashCommandStatus::Running(insert_output_task), }, @@ -2934,6 +2938,8 @@ pub struct ParsedSlashCommand { #[derive(Debug)] pub struct InvokedSlashCommand { + pub name: SharedString, + pub arguments: SmallVec<[SharedString; 3]>, pub position: language::Anchor, pub status: InvokedSlashCommandStatus, } diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index cd6e01ec86600..c917f39793a36 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -1109,6 +1109,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std ..context.buffer.read(cx).anchor_after(command_range.end); context.insert_command_output( command_range, + "/command", + &[], Task::ready(Ok(stream::iter(events).boxed())), true, false, From 673c3afedce6879dc4570320bcc081a7b5e2c62f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 24 Oct 2024 16:59:24 -0400 Subject: [PATCH 30/49] Style invoked slash commands a bit more --- crates/assistant/src/assistant_panel.rs | 31 +++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 044b5cb4f2414..eae44fab82a36 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -4991,19 +4991,26 @@ fn invoked_slash_command_renderer( }; h_flex() - .pl_6() + .px_1() + .ml_6() .gap_2() - .child(h_flex().child("/").child(command.name.clone())) - .child(match &command.status { - InvokedSlashCommandStatus::Running(_) => div().child("Running command").into_any(), - InvokedSlashCommandStatus::Error(message) => div() - .child( - Label::new(format!("error: {}", message)) - .single_line() - .color(Color::Error), - ) - .into_any_element(), - InvokedSlashCommandStatus::Finished => Empty.into_any(), + .bg(cx.theme().colors().surface_background) + .rounded_md() + .child(Label::new(format!("/{}", command.name.clone()))) + .map(|parent| match &command.status { + InvokedSlashCommandStatus::Running(_) => { + parent.child(Icon::new(IconName::ArrowCircle).with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(4)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + )) + } + InvokedSlashCommandStatus::Error(message) => parent.child( + Label::new(format!("error: {message}")) + .single_line() + .color(Color::Error), + ), + InvokedSlashCommandStatus::Finished => parent, }) .into_any_element() }) From df50cbedd31aee17532e049626859a7cff0bda26 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 24 Oct 2024 17:35:11 -0400 Subject: [PATCH 31/49] Add button to dismiss errors for slash commands --- crates/assistant/src/assistant_panel.rs | 47 +++++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index eae44fab82a36..80fe048c9dc0e 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -73,13 +73,13 @@ use std::{ }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use text::SelectionGoal; -use ui::TintColor; use ui::{ prelude::*, utils::{format_distance_from_now, DateTimeType}, Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip, }; +use ui::{IconButtonShape, TintColor}; use util::{maybe, ResultExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -2117,6 +2117,7 @@ impl ContextEditor { command_id: SlashCommandId, cx: &mut ViewContext, ) { + let context_editor = cx.view().downgrade(); self.editor.update(cx, |editor, cx| { if let Some(invoked_slash_command) = self.context.read(cx).invoked_slash_command(&command_id) @@ -2143,7 +2144,11 @@ impl ContextEditor { }, height: 1, disposition: BlockDisposition::Above, - render: invoked_slash_command_renderer(command_id, context), + render: invoked_slash_command_renderer( + command_id, + context, + context_editor, + ), priority: 0, }], None, @@ -4980,6 +4985,7 @@ fn make_lsp_adapter_delegate( fn invoked_slash_command_renderer( command_id: SlashCommandId, context: WeakModel, + context_editor: WeakView, ) -> RenderBlock { Box::new(move |cx| { let Some(context) = context.upgrade() else { @@ -5005,11 +5011,38 @@ fn invoked_slash_command_renderer( |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), )) } - InvokedSlashCommandStatus::Error(message) => parent.child( - Label::new(format!("error: {message}")) - .single_line() - .color(Color::Error), - ), + InvokedSlashCommandStatus::Error(message) => parent + .child( + Label::new(format!("error: {message}")) + .single_line() + .color(Color::Error), + ) + .child( + IconButton::new("dismiss-error", IconName::Close) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click({ + let context_editor = context_editor.clone(); + move |_event, cx| { + context_editor + .update(cx, |context_editor, cx| { + context_editor.editor.update(cx, |editor, cx| { + editor.remove_blocks( + HashSet::from_iter( + context_editor + .invoked_slash_command_blocks + .remove(&command_id), + ), + None, + cx, + ); + }) + }) + .log_err(); + } + }), + ), InvokedSlashCommandStatus::Finished => parent, }) .into_any_element() From 25f6a1e61fb28d967828c434bf706af7ff9bec9f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Oct 2024 11:00:26 -0400 Subject: [PATCH 32/49] Fix error after merge --- crates/assistant/src/assistant_panel.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 297765502a799..bec99a351b52d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -76,10 +76,9 @@ use text::SelectionGoal; use ui::{ prelude::*, utils::{format_distance_from_now, DateTimeType}, - Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, - ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, + Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, IconButtonShape, KeyBinding, + ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, }; -use ui::{IconButtonShape, TintColor}; use util::{maybe, ResultExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, From 2da529010de2a3fe4f72ec767f05a4182b8dfc82 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Oct 2024 12:09:43 -0400 Subject: [PATCH 33/49] WIP: Begin the switch to creases --- crates/assistant/src/assistant_panel.rs | 172 ++++++++++++------------ crates/assistant/src/context.rs | 4 +- 2 files changed, 89 insertions(+), 87 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index bec99a351b52d..85b2037467fcb 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1479,7 +1479,7 @@ pub struct ContextEditor { scroll_position: Option, remote_id: Option, pending_slash_command_creases: HashMap, CreaseId>, - invoked_slash_command_blocks: HashMap, + invoked_slash_command_creases: HashMap, pending_tool_use_creases: HashMap, CreaseId>, _subscriptions: Vec, patches: HashMap, PatchViewState>, @@ -1550,7 +1550,7 @@ impl ContextEditor { workspace, project, pending_slash_command_creases: HashMap::default(), - invoked_slash_command_blocks: HashMap::default(), + invoked_slash_command_creases: HashMap::default(), pending_tool_use_creases: HashMap::default(), _subscriptions, patches: HashMap::default(), @@ -2162,46 +2162,44 @@ impl ContextEditor { self.context.read(cx).invoked_slash_command(&command_id) { if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - editor.remove_blocks( - HashSet::from_iter(self.invoked_slash_command_blocks.remove(&command_id)), - None, + editor.remove_creases( + HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), cx, ) - } else if self.invoked_slash_command_blocks.contains_key(&command_id) { + } else if self.invoked_slash_command_creases.contains_key(&command_id) { cx.notify(); } else { let buffer = editor.buffer().read(cx).snapshot(cx); - let (&excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); + let (&excerpt_id, buffer_id, buffer_snapshot) = buffer.as_singleton().unwrap(); let context = self.context.downgrade(); - let block_ids = editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Fixed, - placement: BlockPlacement::Above(Anchor { - buffer_id: Some(buffer_id), - excerpt_id, - text_anchor: invoked_slash_command.position, - }), - height: 1, - render: invoked_slash_command_renderer( + let crease_ids = editor.insert_creases( + [Crease::new( + buffer.anchor_before( + invoked_slash_command.range.start.to_offset(buffer_snapshot), + ) + ..buffer.anchor_after( + invoked_slash_command.range.start.to_offset(buffer_snapshot), + ), + invoked_slash_command_fold_placeholder( command_id, context, context_editor, ), - priority: 0, - }], - None, + fold_toggle("invoked-slash-command"), + |_row, _folded, _cx| Empty.into_any(), + )], cx, ); - self.invoked_slash_command_blocks - .insert(command_id, block_ids[0]); + self.invoked_slash_command_creases + .insert(command_id, crease_ids[0]); } } else { - editor.remove_blocks( - HashSet::from_iter(self.invoked_slash_command_blocks.remove(&command_id)), - None, + editor.remove_creases( + HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), cx, - ) + ); + cx.notify(); }; }); } @@ -5103,71 +5101,75 @@ fn make_lsp_adapter_delegate( }) } -fn invoked_slash_command_renderer( +fn invoked_slash_command_fold_placeholder( command_id: SlashCommandId, context: WeakModel, context_editor: WeakView, -) -> RenderBlock { - Box::new(move |cx| { - let Some(context) = context.upgrade() else { - return Empty.into_any(); - }; +) -> FoldPlaceholder { + FoldPlaceholder { + constrain_width: false, + merge_adjacent: false, + render: Arc::new(move |fold_id, fold_range, cx| { + let Some(context) = context.upgrade() else { + return Empty.into_any(); + }; - let Some(command) = context.read(cx).invoked_slash_command(&command_id) else { - return Empty.into_any(); - }; + let Some(command) = context.read(cx).invoked_slash_command(&command_id) else { + return Empty.into_any(); + }; - h_flex() - .px_1() - .ml_6() - .gap_2() - .bg(cx.theme().colors().surface_background) - .rounded_md() - .child(Label::new(format!("/{}", command.name.clone()))) - .map(|parent| match &command.status { - InvokedSlashCommandStatus::Running(_) => { - parent.child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) - } - InvokedSlashCommandStatus::Error(message) => parent - .child( - Label::new(format!("error: {message}")) - .single_line() - .color(Color::Error), - ) - .child( - IconButton::new("dismiss-error", IconName::Close) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click({ - let context_editor = context_editor.clone(); - move |_event, cx| { - context_editor - .update(cx, |context_editor, cx| { - context_editor.editor.update(cx, |editor, cx| { - editor.remove_blocks( - HashSet::from_iter( - context_editor - .invoked_slash_command_blocks - .remove(&command_id), - ), - None, - cx, - ); + h_flex() + .id(fold_id) + .px_1() + .ml_6() + .gap_2() + .bg(cx.theme().colors().surface_background) + .rounded_md() + .child(Label::new(format!("/{}", command.name.clone()))) + .map(|parent| match &command.status { + InvokedSlashCommandStatus::Running(_) => { + parent.child(Icon::new(IconName::ArrowCircle).with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(4)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + )) + } + InvokedSlashCommandStatus::Error(message) => parent + .child( + Label::new(format!("error: {message}")) + .single_line() + .color(Color::Error), + ) + .child( + IconButton::new("dismiss-error", IconName::Close) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click({ + let context_editor = context_editor.clone(); + move |_event, cx| { + context_editor + .update(cx, |context_editor, cx| { + context_editor.editor.update(cx, |editor, cx| { + editor.remove_creases( + HashSet::from_iter( + context_editor + .invoked_slash_command_creases + .remove(&command_id), + ), + cx, + ); + }) }) - }) - .log_err(); - } - }), - ), - InvokedSlashCommandStatus::Finished => parent, - }) - .into_any_element() - }) + .log_err(); + } + }), + ), + InvokedSlashCommandStatus::Finished => parent, + }) + .into_any_element() + }), + } } enum TokenState { diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 30851fc58ee65..851274eb66e52 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1943,7 +1943,7 @@ impl Context { InvokedSlashCommand { name: name.to_string().into(), arguments: arguments.iter().cloned().map(SharedString::from).collect(), - position: command_range.start, + range: command_range, status: InvokedSlashCommandStatus::Running(insert_output_task), }, ); @@ -2990,7 +2990,7 @@ pub struct ParsedSlashCommand { pub struct InvokedSlashCommand { pub name: SharedString, pub arguments: SmallVec<[SharedString; 3]>, - pub position: language::Anchor, + pub range: Range, pub status: InvokedSlashCommandStatus, } From d9a22892867c5b98fc26df70704308f2bb1582e5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Oct 2024 13:18:27 -0400 Subject: [PATCH 34/49] WIP: Fix some issues with streaming in content into a crease Co-authored-by: Max Co-authored-by: Antonio --- crates/assistant/src/assistant_panel.rs | 37 ++++++++++++++++--------- crates/assistant/src/context.rs | 13 ++++++--- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 85b2037467fcb..a93822645acf4 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2162,35 +2162,46 @@ impl ContextEditor { self.context.read(cx).invoked_slash_command(&command_id) { if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + let buffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, _buffer_id, _buffer_snapshot) = + buffer.as_singleton().unwrap(); + + let start = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) + .unwrap(); + editor.unfold_ranges([start..start], true, false, cx); + editor.remove_creases( HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), cx, - ) + ); } else if self.invoked_slash_command_creases.contains_key(&command_id) { cx.notify(); } else { let buffer = editor.buffer().read(cx).snapshot(cx); - let (&excerpt_id, buffer_id, buffer_snapshot) = buffer.as_singleton().unwrap(); + let (&excerpt_id, _buffer_id, _buffer_snapshot) = + buffer.as_singleton().unwrap(); let context = self.context.downgrade(); + let crease_start = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) + .unwrap(); + let crease_end = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) + .unwrap(); + let fold_placeholder = + invoked_slash_command_fold_placeholder(command_id, context, context_editor); let crease_ids = editor.insert_creases( [Crease::new( - buffer.anchor_before( - invoked_slash_command.range.start.to_offset(buffer_snapshot), - ) - ..buffer.anchor_after( - invoked_slash_command.range.start.to_offset(buffer_snapshot), - ), - invoked_slash_command_fold_placeholder( - command_id, - context, - context_editor, - ), + crease_start..crease_end, + fold_placeholder.clone(), fold_toggle("invoked-slash-command"), |_row, _folded, _cx| Empty.into_any(), )], cx, ); + editor.fold_ranges([(crease_start..crease_end, fold_placeholder)], false, cx); + self.invoked_slash_command_creases .insert(command_id, crease_ids[0]); } diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 851274eb66e52..9944794e60bd9 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1716,7 +1716,7 @@ impl Context { pub fn insert_command_output( &mut self, - command_range: Range, + command_source_range: Range, name: &str, arguments: &[String], output: Task, @@ -1726,9 +1726,14 @@ impl Context { ) { let command_id = SlashCommandId(self.next_timestamp()); - let insert_position = self.buffer.update(cx, |buffer, cx| { - buffer.edit([(command_range.clone(), "")], None, cx); - command_range.end.bias_right(buffer) + let (insert_position, command_range) = self.buffer.update(cx, |buffer, cx| { + let command_source_range = command_source_range.to_offset(buffer); + buffer.edit([(command_source_range.clone(), "\n\n")], None, cx); + let insert_position = + buffer.anchor_after(command_source_range.start + 1); + let range = buffer.anchor_before(command_source_range.start) + ..buffer.anchor_after(command_source_range.start + 2); + (insert_position, range) }); self.reparse(cx); From 1406f5ffea9a06ed5d89ba9c02e6266725474458 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Oct 2024 13:36:27 -0400 Subject: [PATCH 35/49] Remove extra newlines from example streaming command --- .../slash_command/streaming_example_command.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/assistant/src/slash_command/streaming_example_command.rs b/crates/assistant/src/slash_command/streaming_example_command.rs index ae805669d2460..28efda4bdda9f 100644 --- a/crates/assistant/src/slash_command/streaming_example_command.rs +++ b/crates/assistant/src/slash_command/streaming_example_command.rs @@ -75,12 +75,6 @@ impl SlashCommand for StreamingExampleSlashCommand { }, )))?; events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; - events_tx.unbounded_send(Ok(SlashCommandEvent::Content( - SlashCommandContent::Text { - text: "\n".into(), - run_commands_in_text: false, - }, - )))?; Timer::after(Duration::from_secs(1)).await; @@ -96,12 +90,6 @@ impl SlashCommand for StreamingExampleSlashCommand { }, )))?; events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; - events_tx.unbounded_send(Ok(SlashCommandEvent::Content( - SlashCommandContent::Text { - text: "\n".into(), - run_commands_in_text: false, - }, - )))?; for n in 1..=10 { Timer::after(Duration::from_secs(1)).await; @@ -119,12 +107,6 @@ impl SlashCommand for StreamingExampleSlashCommand { )))?; events_tx .unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; - events_tx.unbounded_send(Ok(SlashCommandEvent::Content( - SlashCommandContent::Text { - text: "\n".into(), - run_commands_in_text: false, - }, - )))?; } anyhow::Ok(()) From 8ddffd72b92d4b062b9b957359c4e7ecbca469f7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Oct 2024 13:40:13 -0400 Subject: [PATCH 36/49] Leave the slash command text in until the end --- crates/assistant/src/context.rs | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 9944794e60bd9..e5c0a5fe00410 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1726,15 +1726,19 @@ impl Context { ) { let command_id = SlashCommandId(self.next_timestamp()); - let (insert_position, command_range) = self.buffer.update(cx, |buffer, cx| { - let command_source_range = command_source_range.to_offset(buffer); - buffer.edit([(command_source_range.clone(), "\n\n")], None, cx); - let insert_position = - buffer.anchor_after(command_source_range.start + 1); - let range = buffer.anchor_before(command_source_range.start) - ..buffer.anchor_after(command_source_range.start + 2); - (insert_position, range) - }); + let (insert_position, command_source_range, command_range) = + self.buffer.update(cx, |buffer, cx| { + let command_source_range = command_source_range.to_offset(buffer); + buffer.edit( + [(command_source_range.end..command_source_range.end, "\n\n")], + None, + cx, + ); + let insert_position = buffer.anchor_after(command_source_range.end + 1); + let output_range = buffer.anchor_before(command_source_range.start) + ..buffer.anchor_after(command_source_range.end + 2); + (insert_position, command_source_range, output_range) + }); self.reparse(cx); let insert_output_task = cx.spawn(|this, mut cx| async move { @@ -1862,9 +1866,11 @@ impl Context { } } - if ensure_trailing_newline { - this.update(&mut cx, |this, cx| { - this.buffer.update(cx, |buffer, cx| { + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(command_source_range, "")], None, cx); + + if ensure_trailing_newline { let offset = insert_position.to_offset(buffer); let newline_offset = offset.saturating_sub(1); if !buffer.contains_str_at(newline_offset, "\n") @@ -1876,9 +1882,9 @@ impl Context { { buffer.edit([(offset..offset, "\n")], None, cx); } - }); - })?; - } + } + }); + })?; debug_assert!(pending_section_stack.is_empty()); From 5c10a5a692aff36e1d68b0228b90b4cc9cf70072 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Nov 2024 16:45:00 -0500 Subject: [PATCH 37/49] WIP: Add collaboration Co-authored-by: Max --- crates/assistant/src/assistant_panel.rs | 32 -- crates/assistant/src/context.rs | 337 +++++++++++------- crates/assistant/src/context/context_tests.rs | 5 +- .../src/assistant_slash_command.rs | 6 +- crates/proto/proto/zed.proto | 23 +- 5 files changed, 228 insertions(+), 175 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index c6b1ebbde119d..a5f49f06bd85a 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2005,31 +2005,6 @@ impl ContextEditor { cx, ); - // todo!(): Added on `main`, do we still need? - // let block_ids = editor.insert_blocks( - // updated - // .iter() - // .filter_map(|command| match &command.status { - // PendingSlashCommandStatus::Error(error) => { - // Some((command, error.clone())) - // } - // _ => None, - // }) - // .map(|(command, error_message)| BlockProperties { - // style: BlockStyle::Fixed, - // height: 1, - // placement: BlockPlacement::Below(Anchor { - // buffer_id: Some(buffer_id), - // excerpt_id, - // text_anchor: command.source_range.start, - // }), - // render: slash_command_error_block_renderer(error_message), - // priority: 0, - // }), - // None, - // cx, - // ); - self.pending_slash_command_creases.extend( updated .iter() @@ -2046,16 +2021,9 @@ impl ContextEditor { } ContextEvent::SlashCommandFinished { output_range: _output_range, - sections, run_commands_in_ranges, expand_result, } => { - self.insert_slash_command_output_sections( - sections.iter().cloned(), - *expand_result, - cx, - ); - for range in run_commands_in_ranges { let commands = self.context.update(cx, |context, cx| { context.reparse(cx); diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 9c81f52dee316..2e60f4c727ead 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -94,10 +94,21 @@ pub enum ContextOperation { summary: ContextSummary, version: clock::Global, }, - SlashCommandFinished { + SlashCommandStarted { id: SlashCommandId, output_range: Range, - sections: Vec>, + name: String, + version: clock::Global, + }, + SlashCommandFinished { + id: SlashCommandId, + timestamp: clock::Lamport, + error_message: Option, + version: clock::Global, + }, + SlashCommandOutputSectionAdded { + timestamp: clock::Lamport, + section: SlashCommandOutputSection, version: clock::Global, }, BufferOperation(language::Operation), @@ -154,31 +165,47 @@ impl ContextOperation { }, version: language::proto::deserialize_version(&update.version), }), - proto::context_operation::Variant::SlashCommandFinished(finished) => { - Ok(Self::SlashCommandFinished { + proto::context_operation::Variant::SlashCommandStarted(message) => { + Ok(Self::SlashCommandStarted { id: SlashCommandId(language::proto::deserialize_timestamp( - finished.id.context("invalid id")?, + message.id.context("invalid id")?, )), output_range: language::proto::deserialize_anchor_range( - finished.output_range.context("invalid range")?, + message.output_range.context("invalid range")?, )?, - sections: finished - .sections - .into_iter() - .map(|section| { - Ok(SlashCommandOutputSection { - range: language::proto::deserialize_anchor_range( - section.range.context("invalid range")?, - )?, - icon: section.icon_name.parse()?, - label: section.label.into(), - metadata: section - .metadata - .and_then(|metadata| serde_json::from_str(&metadata).log_err()), - }) - }) - .collect::>>()?, - version: language::proto::deserialize_version(&finished.version), + name: message.name, + version: language::proto::deserialize_version(&message.version), + }) + } + proto::context_operation::Variant::SlashCommandOutputSectionAdded(message) => { + let section = message.section.context("missing section")?; + Ok(Self::SlashCommandOutputSectionAdded { + timestamp: language::proto::deserialize_timestamp( + message.timestamp.context("missing timestamp")?, + ), + section: SlashCommandOutputSection { + range: language::proto::deserialize_anchor_range( + section.range.context("invalid range")?, + )?, + icon: section.icon_name.parse()?, + label: section.label.into(), + metadata: section + .metadata + .and_then(|metadata| serde_json::from_str(&metadata).log_err()), + }, + version: language::proto::deserialize_version(&message.version), + }) + } + proto::context_operation::Variant::SlashCommandFinished(message) => { + Ok(Self::SlashCommandFinished { + id: SlashCommandId(language::proto::deserialize_timestamp( + message.id.context("invalid id")?, + )), + timestamp: language::proto::deserialize_timestamp( + message.timestamp.context("missing timestamp")?, + ), + error_message: message.error_message, + version: language::proto::deserialize_version(&message.version), }) } proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation( @@ -233,21 +260,33 @@ impl ContextOperation { }, )), }, - Self::SlashCommandFinished { + Self::SlashCommandStarted { id, output_range, - sections, + name, version, } => proto::ContextOperation { - variant: Some(proto::context_operation::Variant::SlashCommandFinished( - proto::context_operation::SlashCommandFinished { + variant: Some(proto::context_operation::Variant::SlashCommandStarted( + proto::context_operation::SlashCommandStarted { id: Some(language::proto::serialize_timestamp(id.0)), output_range: Some(language::proto::serialize_anchor_range( output_range.clone(), )), - sections: sections - .iter() - .map(|section| { + name: name.clone(), + version: language::proto::serialize_version(version), + }, + )), + }, + Self::SlashCommandOutputSectionAdded { + timestamp, + section, + version, + } => proto::ContextOperation { + variant: Some( + proto::context_operation::Variant::SlashCommandOutputSectionAdded( + proto::context_operation::SlashCommandOutputSectionAdded { + timestamp: Some(language::proto::serialize_timestamp(*timestamp)), + section: Some({ let icon_name: &'static str = section.icon.into(); proto::SlashCommandOutputSection { range: Some(language::proto::serialize_anchor_range( @@ -259,8 +298,23 @@ impl ContextOperation { serde_json::to_string(metadata).log_err() }), } - }) - .collect(), + }), + version: language::proto::serialize_version(version), + }, + ), + ), + }, + Self::SlashCommandFinished { + id, + timestamp, + error_message, + version, + } => proto::ContextOperation { + variant: Some(proto::context_operation::Variant::SlashCommandFinished( + proto::context_operation::SlashCommandFinished { + id: Some(language::proto::serialize_timestamp(id.0)), + timestamp: Some(language::proto::serialize_timestamp(*timestamp)), + error_message: error_message.clone(), version: language::proto::serialize_version(version), }, )), @@ -280,7 +334,9 @@ impl ContextOperation { Self::InsertMessage { anchor, .. } => anchor.id.0, Self::UpdateMessage { metadata, .. } => metadata.timestamp, Self::UpdateSummary { summary, .. } => summary.timestamp, - Self::SlashCommandFinished { id, .. } => id.0, + Self::SlashCommandStarted { id, .. } => id.0, + Self::SlashCommandOutputSectionAdded { timestamp, .. } + | Self::SlashCommandFinished { timestamp, .. } => *timestamp, Self::BufferOperation(_) => { panic!("reading the timestamp of a buffer operation is not supported") } @@ -293,6 +349,8 @@ impl ContextOperation { Self::InsertMessage { version, .. } | Self::UpdateMessage { version, .. } | Self::UpdateSummary { version, .. } + | Self::SlashCommandStarted { version, .. } + | Self::SlashCommandOutputSectionAdded { version, .. } | Self::SlashCommandFinished { version, .. } => version, Self::BufferOperation(_) => { panic!("reading the version of a buffer operation is not supported") @@ -325,7 +383,6 @@ pub enum ContextEvent { }, SlashCommandFinished { output_range: Range, - sections: Vec>, run_commands_in_ranges: Vec>, expand_result: bool, }, @@ -837,24 +894,49 @@ impl Context { summary_changed = true; } } - ContextOperation::SlashCommandFinished { + ContextOperation::SlashCommandStarted { id, output_range, - sections, + name, .. } => { - if self.finished_slash_commands.insert(id) { - let buffer = self.buffer.read(cx); - self.slash_command_output_sections - .extend(sections.iter().cloned()); + self.invoked_slash_commands.insert( + id, + InvokedSlashCommand { + name: name.into(), + range: output_range, + status: InvokedSlashCommandStatus::Running(Task::ready(())), + }, + ); + } + ContextOperation::SlashCommandOutputSectionAdded { section, .. } => { + let buffer = self.buffer.read(cx); + if let Err(ix) = self + .slash_command_output_sections + .binary_search_by(|probe| probe.range.cmp(§ion.range, buffer)) + { self.slash_command_output_sections - .sort_by(|a, b| a.range.cmp(&b.range, buffer)); - cx.emit(ContextEvent::SlashCommandFinished { - output_range, - sections, - expand_result: false, - run_commands_in_ranges: vec![], - }); + .insert(ix, section.clone()); + cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section }); + } + } + ContextOperation::SlashCommandFinished { + id, error_message, .. + } => { + if self.finished_slash_commands.insert(id) { + if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) { + match error_message { + Some(message) => { + slash_command.status = + InvokedSlashCommandStatus::Error(message.into()); + } + None => { + slash_command.status = InvokedSlashCommandStatus::Finished; + } + } + } + + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } } ContextOperation::BufferOperation(_) => unreachable!(), @@ -892,32 +974,34 @@ impl Context { self.messages_metadata.contains_key(message_id) } ContextOperation::UpdateSummary { .. } => true, - ContextOperation::SlashCommandFinished { - output_range, - sections, - .. - } => { - let version = &self.buffer.read(cx).version; - sections - .iter() - .map(|section| §ion.range) - .chain([output_range]) - .all(|range| { - let observed_start = range.start == language::Anchor::MIN - || range.start == language::Anchor::MAX - || version.observed(range.start.timestamp); - let observed_end = range.end == language::Anchor::MIN - || range.end == language::Anchor::MAX - || version.observed(range.end.timestamp); - observed_start && observed_end - }) + ContextOperation::SlashCommandStarted { output_range, .. } => { + self.has_received_operations_for_anchor_range(output_range.clone(), cx) } + ContextOperation::SlashCommandOutputSectionAdded { section, .. } => { + self.has_received_operations_for_anchor_range(section.range.clone(), cx) + } + ContextOperation::SlashCommandFinished { .. } => true, ContextOperation::BufferOperation(_) => { panic!("buffer operations should always be applied") } } } + fn has_received_operations_for_anchor_range( + &self, + range: Range, + cx: &AppContext, + ) -> bool { + let version = &self.buffer.read(cx).version; + let observed_start = range.start == language::Anchor::MIN + || range.start == language::Anchor::MAX + || version.observed(range.start.timestamp); + let observed_end = range.end == language::Anchor::MIN + || range.end == language::Anchor::MAX + || version.observed(range.end.timestamp); + observed_start && observed_end + } + fn push_op(&mut self, op: ContextOperation, cx: &mut ModelContext) { self.operations.push(op.clone()); cx.emit(ContextEvent::Operation(op)); @@ -1727,6 +1811,7 @@ impl Context { expand_result: bool, cx: &mut ModelContext, ) { + let version = self.version.clone(); let command_id = SlashCommandId(self.next_timestamp()); let (insert_position, command_source_range, command_range) = @@ -1829,12 +1914,6 @@ impl Context { })?; } }, - SlashCommandEvent::Progress { - message: _, - complete: _, - } => { - todo!() - } SlashCommandEvent::EndSection { metadata } => { if let Some(pending_section) = pending_section_stack.pop() { this.update(&mut cx, |this, cx| { @@ -1897,71 +1976,57 @@ impl Context { let command_result = run_command.await; this.update(&mut cx, |this, cx| { + let version = this.version.clone(); + let timestamp = this.next_timestamp(); let Some(invoked_slash_command) = this.invoked_slash_commands.get_mut(&command_id) else { return; }; + let mut error_message = None; match command_result { Ok(()) => { invoked_slash_command.status = InvokedSlashCommandStatus::Finished; } Err(error) => { + let message = error.to_string(); invoked_slash_command.status = - InvokedSlashCommandStatus::Error(error.to_string().into()); + InvokedSlashCommandStatus::Error(message.clone().into()); + error_message = Some(message); } } cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + this.push_op( + ContextOperation::SlashCommandFinished { + id: command_id, + timestamp, + error_message, + version, + }, + cx, + ); }) .ok(); - - // todo!("make inserting sections and emitting operations streaming") - // async move { - // this.update(&mut cx, |this, cx| { - // this.finished_slash_commands.insert(command_id); - - // let version = this.version.clone(); - // let (op, ev) = this.buffer.update(cx, |buffer, _cx| { - // let start = command_range.start; - // let output_range = start..insert_position; - - // this.slash_command_output_sections - // .sort_by(|a, b| a.range.cmp(&b.range, buffer)); - // finished_sections.sort_by(|a, b| a.range.cmp(&b.range, buffer)); - - // // Remove the command range from the buffer - // ( - // ContextOperation::SlashCommandFinished { - // id: command_id, - // output_range: output_range.clone(), - // sections: finished_sections.clone(), - // version, - // }, - // ContextEvent::SlashCommandFinished { - // output_range, - // sections: finished_sections, - // run_commands_in_ranges, - // expand_result, - // }, - // ) - // }); - - // this.push_op(op, cx); - // cx.emit(ev); - // }) - // } }); self.invoked_slash_commands.insert( command_id, InvokedSlashCommand { name: name.to_string().into(), - arguments: arguments.iter().cloned().map(SharedString::from).collect(), - range: command_range, + range: command_range.clone(), status: InvokedSlashCommandStatus::Running(insert_output_task), }, ); cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + self.push_op( + ContextOperation::SlashCommandStarted { + id: command_id, + output_range: command_range, + name: name.to_string(), + version, + }, + cx, + ); } fn insert_slash_command_output_section( @@ -1981,7 +2046,16 @@ impl Context { cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section: section.clone(), }); - // todo!("emit an operation that creates a section for collaborators") + let version = self.version.clone(); + let timestamp = self.next_timestamp(); + self.push_op( + ContextOperation::SlashCommandOutputSectionAdded { + timestamp, + section, + version, + }, + cx, + ); } pub fn insert_tool_output( @@ -3009,7 +3083,6 @@ pub struct ParsedSlashCommand { #[derive(Debug)] pub struct InvokedSlashCommand { pub name: SharedString, - pub arguments: SmallVec<[SharedString; 3]>, pub range: Range, pub status: InvokedSlashCommandStatus, } @@ -3160,27 +3233,23 @@ impl SavedContext { version.observe(timestamp); } - let timestamp = next_timestamp.tick(); - operations.push(ContextOperation::SlashCommandFinished { - id: SlashCommandId(timestamp), - output_range: language::Anchor::MIN..language::Anchor::MAX, - sections: self - .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, - metadata: section.metadata, - } - }) - .collect(), - version: version.clone(), - }); - version.observe(timestamp); + let buffer = buffer.read(cx); + for section in self.slash_command_output_sections { + let timestamp = next_timestamp.tick(); + operations.push(ContextOperation::SlashCommandOutputSectionAdded { + timestamp, + section: SlashCommandOutputSection { + range: buffer.anchor_after(section.range.start) + ..buffer.anchor_before(section.range.end), + icon: section.icon, + label: section.label, + metadata: section.metadata, + }, + version: version.clone(), + }); + + version.observe(timestamp); + } let timestamp = next_timestamp.tick(); operations.push(ContextOperation::UpdateSummary { diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 7409d897c0f06..adcd74043ed79 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -1077,7 +1077,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let num_sections = rng.gen_range(0..=3); let mut section_start = 0; for _ in 0..num_sections { - let section_end = rng.gen_range(section_start..=output_text.len()); + let mut section_end = rng.gen_range(section_start..=output_text.len()); + while !output_text.is_char_boundary(section_end) { + section_end += 1; + } events.push(Ok(SlashCommandEvent::StartSection { icon: IconName::Ai, label: "section".into(), diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 69ed91105265d..dc1f423d1cda8 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -123,10 +123,6 @@ pub enum SlashCommandEvent { label: SharedString, metadata: Option, }, - Progress { - message: SharedString, - complete: f32, - }, Content(SlashCommandContent), EndSection { metadata: Option, @@ -241,7 +237,7 @@ impl SlashCommandOutput { output.sections.push(section); } } - SlashCommandEvent::StartMessage { .. } | SlashCommandEvent::Progress { .. } => {} + SlashCommandEvent::StartMessage { .. } => {} } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index e43b622545e4b..fa327d529a221 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2251,10 +2251,14 @@ message ContextOperation { InsertMessage insert_message = 1; UpdateMessage update_message = 2; UpdateSummary update_summary = 3; - SlashCommandFinished slash_command_finished = 4; BufferOperation buffer_operation = 5; + SlashCommandStarted slash_command_started = 6; + SlashCommandOutputSectionAdded slash_command_output_section_added = 7; + SlashCommandFinished slash_command_finished = 8; } + reserved 4; + message InsertMessage { ContextMessage message = 1; repeated VectorClockEntry version = 2; @@ -2275,13 +2279,26 @@ message ContextOperation { repeated VectorClockEntry version = 4; } - message SlashCommandFinished { + message SlashCommandStarted { LamportTimestamp id = 1; AnchorRange output_range = 2; - repeated SlashCommandOutputSection sections = 3; + string name = 3; repeated VectorClockEntry version = 4; } + message SlashCommandOutputSectionAdded { + LamportTimestamp timestamp = 1; + SlashCommandOutputSection section = 2; + repeated VectorClockEntry version = 3; + } + + message SlashCommandFinished { + LamportTimestamp id = 1; + LamportTimestamp timestamp = 3; + optional string error_message = 4; + repeated VectorClockEntry version = 5; + } + message BufferOperation { Operation operation = 1; } From 6efa983cedc224beef0ca361c98c25d53db5fa00 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Nov 2024 16:23:23 -0800 Subject: [PATCH 38/49] Remove unused arguments parameter, remove noisy log --- crates/assistant/src/assistant_panel.rs | 1 - crates/assistant/src/context.rs | 5 ----- crates/assistant/src/context/context_tests.rs | 1 - 3 files changed, 7 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index a5f49f06bd85a..adc66d1062751 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1797,7 +1797,6 @@ impl ContextEditor { context.insert_command_output( command_range, name, - arguments, output, ensure_trailing_newline, expand_result, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 2e60f4c727ead..eb4c771191e21 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1805,7 +1805,6 @@ impl Context { &mut self, command_source_range: Range, name: &str, - arguments: &[String], output: Task, ensure_trailing_newline: bool, expand_result: bool, @@ -1876,10 +1875,6 @@ impl Context { } => { this.read_with(&cx, |this, cx| { let buffer = this.buffer.read(cx); - log::info!( - "Slash command output section start: {:?}", - insert_position - ); pending_section_stack.push(PendingSection { start: buffer.anchor_before(insert_position), icon, diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index adcd74043ed79..f6868bc2ce2af 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -1113,7 +1113,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std context.insert_command_output( command_range, "/command", - &[], Task::ready(Ok(stream::iter(events).boxed())), true, false, From 2f1c701bf37336ff43a5754b36b0d6d90337ad4c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Nov 2024 16:39:33 -0800 Subject: [PATCH 39/49] Add missing slash command update event when applying operation --- crates/assistant/src/context.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index eb4c771191e21..5b5e1c0599b10 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -908,6 +908,7 @@ impl Context { status: InvokedSlashCommandStatus::Running(Task::ready(())), }, ); + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } ContextOperation::SlashCommandOutputSectionAdded { section, .. } => { let buffer = self.buffer.read(cx); From bcf95bbd72c3eb2ff493b6ee25cd7bba2cf2e449 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Nov 2024 18:19:01 +0100 Subject: [PATCH 40/49] Invalidate creases for sections that have been deleted --- crates/assistant/src/context.rs | 5 +++-- .../src/assistant_slash_command.rs | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 5b5e1c0599b10..198800b1bca18 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1913,8 +1913,9 @@ impl Context { SlashCommandEvent::EndSection { metadata } => { if let Some(pending_section) = pending_section_stack.pop() { this.update(&mut cx, |this, cx| { - let range = pending_section.start - ..insert_position.bias_left(this.buffer.read(cx)); + let range = + pending_section.start.bias_right(this.buffer.read(cx)) + ..insert_position.bias_left(this.buffer.read(cx)); let offset_range = range.to_offset(this.buffer.read(cx)); if !offset_range.is_empty() { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index dc1f423d1cda8..fcac07bbd0381 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -112,6 +112,15 @@ pub enum SlashCommandContent { }, } +impl<'a> From<&'a str> for SlashCommandContent { + fn from(text: &'a str) -> Self { + Self::Text { + text: text.into(), + run_commands_in_text: false, + } + } +} + #[derive(Debug, PartialEq)] pub enum SlashCommandEvent { StartMessage { From 8346801dcd62fb5607f4871ad9cc8666a1d3300c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Nov 2024 18:48:43 +0100 Subject: [PATCH 41/49] Restructure test for slash commands --- crates/assistant/src/context/context_tests.rs | 239 +++++++++++++++--- 1 file changed, 209 insertions(+), 30 deletions(-) diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index f6868bc2ce2af..ced4c5b45d1b8 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -2,15 +2,19 @@ use super::{AssistantEdit, MessageCacheMetadata}; use crate::{ assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus, Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder, + SlashCommandId, }; use anyhow::Result; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, }; -use collections::HashSet; +use collections::{HashMap, HashSet}; use fs::FakeFs; -use futures::stream::{self, StreamExt}; +use futures::{ + channel::mpsc, + stream::{self, StreamExt}, +}; use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView}; use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate}; use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role}; @@ -28,7 +32,7 @@ use std::{ rc::Rc, sync::{atomic::AtomicBool, Arc}, }; -use text::{network::Network, OffsetRangeExt as _, ReplicaId}; +use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset}; use ui::{Context as _, IconName, WindowContext}; use unindent::Unindent; use util::{ @@ -382,20 +386,41 @@ async fn test_slash_commands(cx: &mut TestAppContext) { let context = cx.new_model(|cx| Context::local(registry.clone(), None, None, prompt_builder.clone(), cx)); - let output_ranges = Rc::new(RefCell::new(HashSet::default())); + #[derive(Default)] + struct ContextRanges { + parsed_commands: HashSet>, + command_outputs: HashMap>, + output_sections: HashSet>, + } + + let context_ranges = Rc::new(RefCell::new(ContextRanges::default())); context.update(cx, |_, cx| { cx.subscribe(&context, { - let ranges = output_ranges.clone(); - move |_, _, event, _| match event { - ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { - for range in removed { - ranges.borrow_mut().remove(range); + let context_ranges = context_ranges.clone(); + move |context, _, event, _| { + let mut context_ranges = context_ranges.borrow_mut(); + match event { + ContextEvent::InvokedSlashCommandChanged { command_id } => { + let command = context.invoked_slash_command(command_id).unwrap(); + context_ranges + .command_outputs + .insert(*command_id, command.range.clone()); + } + ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { + for range in removed { + context_ranges.parsed_commands.remove(range); + } + for command in updated { + context_ranges + .parsed_commands + .insert(command.source_range.clone()); + } } - for command in updated { - ranges.borrow_mut().insert(command.source_range.clone()); + ContextEvent::SlashCommandOutputSectionAdded { section } => { + context_ranges.output_sections.insert(section.range.clone()); } + _ => {} } - _ => {} } }) .detach(); @@ -407,9 +432,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { buffer.update(cx, |buffer, cx| { buffer.edit([(0..0, "/file src/lib.rs")], None, cx); }); - assert_text_and_output_ranges( + assert_text_and_context_ranges( &buffer, - &output_ranges.borrow(), + &context_ranges, " «/file src/lib.rs» " @@ -423,9 +448,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { let edit_offset = buffer.text().find("lib.rs").unwrap(); buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx); }); - assert_text_and_output_ranges( + assert_text_and_context_ranges( &buffer, - &output_ranges.borrow(), + &context_ranges, " «/file src/main.rs» " @@ -443,9 +468,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { cx, ); }); - assert_text_and_output_ranges( + assert_text_and_context_ranges( &buffer, - &output_ranges.borrow(), + &context_ranges, " /unknown src/main.rs " @@ -454,25 +479,179 @@ async fn test_slash_commands(cx: &mut TestAppContext) { cx, ); + // Undoing the insertion of an non-existant slash command resorts the previous one. + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + " + «/file src/main.rs» + " + .unindent() + .trim_end(), + cx, + ); + + let (command_output_tx, command_output_rx) = mpsc::unbounded(); + context.update(cx, |context, cx| { + let buffer = context.buffer.read(cx); + let command_source_range = buffer.anchor_after(0)..buffer.anchor_before(buffer.len()); + context.insert_command_output( + command_source_range, + "file", + Task::ready(Ok(command_output_rx.boxed())), + false, + false, + cx, + ); + }); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + " + ⟦«/file src/main.rs» + + ⟧ + " + .unindent() + .trim_end(), + cx, + ); + + command_output_tx + .unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::Ai, + label: "src/main.rs".into(), + metadata: None, + })) + .unwrap(); + command_output_tx + .unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs\n".into()))) + .unwrap(); + cx.run_until_parked(); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + " + ⟦«/file src/main.rs» + src/main.rs + + ⟧ + " + .unindent() + .trim_end(), + cx, + ); + + command_output_tx + .unbounded_send(Ok(SlashCommandEvent::Content("fn main() {}".into()))) + .unwrap(); + cx.run_until_parked(); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + " + ⟦«/file src/main.rs» + src/main.rs + fn main() {} + ⟧ + " + .unindent() + .trim_end(), + cx, + ); + + command_output_tx + .unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None })) + .unwrap(); + cx.run_until_parked(); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + " + ⟦«/file src/main.rs» + ⟪src/main.rs + fn main() {}⟫ + ⟧ + " + .unindent() + .trim_end(), + cx, + ); + + drop(command_output_tx); + cx.run_until_parked(); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + " + ⟦⟪src/main.rs + fn main() {}⟫ + ⟧ + " + .unindent() + .trim_end(), + cx, + ); + #[track_caller] - fn assert_text_and_output_ranges( + fn assert_text_and_context_ranges<'a>( buffer: &Model, - ranges: &HashSet>, + ranges: &RefCell, expected_marked_text: &str, cx: &mut TestAppContext, ) { - let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false); - let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| { - let mut ranges = ranges - .iter() - .map(|range| range.to_offset(buffer)) - .collect::>(); - ranges.sort_by_key(|a| a.start); - (buffer.text(), ranges) + let mut actual_marked_text = String::new(); + buffer.update(cx, |buffer, _| { + struct Endpoint { + offset: usize, + marker: char, + } + + let ranges = ranges.borrow(); + let mut endpoints = Vec::new(); + for range in ranges.command_outputs.values() { + endpoints.push(Endpoint { + offset: range.start.to_offset(buffer), + marker: '⟦', + }); + endpoints.push(Endpoint { + offset: range.end.to_offset(buffer), + marker: '⟧', + }); + } + for range in ranges.parsed_commands.iter() { + endpoints.push(Endpoint { + offset: range.start.to_offset(buffer), + marker: '«', + }); + endpoints.push(Endpoint { + offset: range.end.to_offset(buffer), + marker: '»', + }); + } + for range in ranges.output_sections.iter() { + endpoints.push(Endpoint { + offset: range.start.to_offset(buffer), + marker: '⟪', + }); + endpoints.push(Endpoint { + offset: range.end.to_offset(buffer), + marker: '⟫', + }); + } + + endpoints.sort_by_key(|endpoint| endpoint.offset); + let mut offset = 0; + for endpoint in endpoints { + actual_marked_text.extend(buffer.text_for_range(offset..endpoint.offset)); + actual_marked_text.push(endpoint.marker); + offset = endpoint.offset; + } + actual_marked_text.extend(buffer.text_for_range(offset..buffer.len())); }); - assert_eq!(actual_text, expected_text); - assert_eq!(actual_ranges, expected_ranges); + assert_eq!(actual_marked_text, expected_marked_text); } } From a52bc590a4485a5ce8e7ea6757fb86cc045df864 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Nov 2024 19:44:25 +0100 Subject: [PATCH 42/49] Make the slash command test pass Co-Authored-By: Max Co-Authored-By: Will --- crates/assistant/src/context.rs | 120 +++++++++--------- crates/assistant/src/context/context_tests.rs | 44 ++++--- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 198800b1bca18..f98d8e4dd00ba 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -48,7 +48,7 @@ use std::{ time::{Duration, Instant}, }; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; -use text::BufferSnapshot; +use text::{BufferSnapshot, ToPoint}; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::ui::IconName; @@ -1818,13 +1818,15 @@ impl Context { self.buffer.update(cx, |buffer, cx| { let command_source_range = command_source_range.to_offset(buffer); buffer.edit( - [(command_source_range.end..command_source_range.end, "\n\n")], + [(command_source_range.end..command_source_range.end, "\n")], None, cx, ); let insert_position = buffer.anchor_after(command_source_range.end + 1); - let output_range = buffer.anchor_before(command_source_range.start) - ..buffer.anchor_after(command_source_range.end + 2); + let output_range = + buffer.anchor_before(command_source_range.start)..insert_position; + let command_source_range = buffer.anchor_before(command_source_range.start) + ..buffer.anchor_before(command_source_range.end + 1); (insert_position, command_source_range, output_range) }); self.reparse(cx); @@ -1842,7 +1844,6 @@ impl Context { let mut pending_section_stack: Vec = Vec::new(); let mut run_commands_in_ranges: Vec> = Vec::new(); - let mut text_ends_with_newline = false; let mut last_role: Option = None; let mut last_section_range = None; @@ -1874,71 +1875,68 @@ impl Context { label, metadata, } => { - this.read_with(&cx, |this, cx| { - let buffer = this.buffer.read(cx); - pending_section_stack.push(PendingSection { - start: buffer.anchor_before(insert_position), - icon, - label, - metadata, + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + let insert_point = insert_position.to_point(buffer); + if insert_point.column > 0 { + buffer.edit([(insert_point..insert_point, "\n")], None, cx); + } + + pending_section_stack.push(PendingSection { + start: buffer.anchor_before(insert_position), + icon, + label, + metadata, + }); }); })?; } - SlashCommandEvent::Content(content) => match content { - SlashCommandContent::Text { - text, - run_commands_in_text, - } => { - this.update(&mut cx, |this, cx| { - let start = this.buffer.read(cx).anchor_before(insert_position); - - let result = this.buffer.update(cx, |buffer, cx| { - text_ends_with_newline = text.ends_with("\n"); - buffer.edit( - [(insert_position..insert_position, text)], - None, - cx, - ) - }); + SlashCommandEvent::Content(SlashCommandContent::Text { + text, + run_commands_in_text, + }) => { + this.update(&mut cx, |this, cx| { + let start = this.buffer.read(cx).anchor_before(insert_position); + + let result = this.buffer.update(cx, |buffer, cx| { + buffer.edit( + [(insert_position..insert_position, text)], + None, + cx, + ) + }); - let end = this.buffer.read(cx).anchor_before(insert_position); - if run_commands_in_text { - run_commands_in_ranges.push(start..end); - } + let end = this.buffer.read(cx).anchor_before(insert_position); + if run_commands_in_text { + run_commands_in_ranges.push(start..end); + } - result - })?; - } - }, + result + })?; + } SlashCommandEvent::EndSection { metadata } => { if let Some(pending_section) = pending_section_stack.pop() { this.update(&mut cx, |this, cx| { - let range = - pending_section.start.bias_right(this.buffer.read(cx)) - ..insert_position.bias_left(this.buffer.read(cx)); - - let offset_range = range.to_offset(this.buffer.read(cx)); - if !offset_range.is_empty() { - this.buffer.update(cx, |buffer, cx| { - if !buffer.contains_str_at(offset_range.end - 1, "\n") { - buffer.edit( - [(offset_range.end..offset_range.end, "\n")], - None, - cx, - ); - } - }); - this.insert_slash_command_output_section( - SlashCommandOutputSection { - range: range.clone(), - icon: pending_section.icon, - label: pending_section.label, - metadata: metadata.or(pending_section.metadata), - }, - cx, - ); - last_section_range = Some(range); + let offset_range = (pending_section.start..insert_position) + .to_offset(this.buffer.read(cx)); + if offset_range.is_empty() { + return; } + + let range = this.buffer.update(cx, |buffer, _cx| { + buffer.anchor_after(offset_range.start) + ..buffer.anchor_before(offset_range.end) + }); + this.insert_slash_command_output_section( + SlashCommandOutputSection { + range: range.clone(), + icon: pending_section.icon, + label: pending_section.label, + metadata: metadata.or(pending_section.metadata), + }, + cx, + ); + last_section_range = Some(range); })?; } } diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index ced4c5b45d1b8..05279105f3e2d 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -510,8 +510,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { &context_ranges, " ⟦«/file src/main.rs» - ⟧ + " .unindent() .trim_end(), @@ -526,7 +526,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { })) .unwrap(); command_output_tx - .unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs\n".into()))) + .unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs".into()))) .unwrap(); cx.run_until_parked(); assert_text_and_context_ranges( @@ -534,9 +534,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { &context_ranges, " ⟦«/file src/main.rs» - src/main.rs + src/main.rs⟧ - ⟧ " .unindent() .trim_end(), @@ -544,7 +543,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { ); command_output_tx - .unbounded_send(Ok(SlashCommandEvent::Content("fn main() {}".into()))) + .unbounded_send(Ok(SlashCommandEvent::Content("\nfn main() {}".into()))) .unwrap(); cx.run_until_parked(); assert_text_and_context_ranges( @@ -553,8 +552,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { " ⟦«/file src/main.rs» src/main.rs - fn main() {} - ⟧ + fn main() {}⟧ + " .unindent() .trim_end(), @@ -571,8 +570,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { " ⟦«/file src/main.rs» ⟪src/main.rs - fn main() {}⟫ - ⟧ + fn main() {}⟫⟧ + " .unindent() .trim_end(), @@ -586,8 +585,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { &context_ranges, " ⟦⟪src/main.rs - fn main() {}⟫ - ⟧ + fn main() {}⟫⟧ + " .unindent() .trim_end(), @@ -615,31 +614,38 @@ async fn test_slash_commands(cx: &mut TestAppContext) { offset: range.start.to_offset(buffer), marker: '⟦', }); - endpoints.push(Endpoint { - offset: range.end.to_offset(buffer), - marker: '⟧', - }); } for range in ranges.parsed_commands.iter() { endpoints.push(Endpoint { offset: range.start.to_offset(buffer), marker: '«', }); - endpoints.push(Endpoint { - offset: range.end.to_offset(buffer), - marker: '»', - }); } for range in ranges.output_sections.iter() { endpoints.push(Endpoint { offset: range.start.to_offset(buffer), marker: '⟪', }); + } + + for range in ranges.output_sections.iter() { endpoints.push(Endpoint { offset: range.end.to_offset(buffer), marker: '⟫', }); } + for range in ranges.parsed_commands.iter() { + endpoints.push(Endpoint { + offset: range.end.to_offset(buffer), + marker: '»', + }); + } + for range in ranges.command_outputs.values() { + endpoints.push(Endpoint { + offset: range.end.to_offset(buffer), + marker: '⟧', + }); + } endpoints.sort_by_key(|endpoint| endpoint.offset); let mut offset = 0; From 332a67933cd328597a7be2ef887fda950da95daf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 12:03:09 -0800 Subject: [PATCH 43/49] Add APIs for removing folds with specific ranges, fix unfolding of pending slash commands --- crates/assistant/src/assistant_panel.rs | 7 +- crates/editor/src/display_map.rs | 48 +++++++------ crates/editor/src/display_map/fold_map.rs | 61 ++++++++++------ crates/editor/src/editor.rs | 88 +++++++++++++++-------- crates/editor/src/items.rs | 4 +- crates/search/src/project_search.rs | 2 +- 6 files changed, 133 insertions(+), 77 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index c025061eb2fff..6bcebd14b411c 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2134,7 +2134,10 @@ impl ContextEditor { let start = buffer .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) .unwrap(); - editor.unfold_ranges([start..start], true, false, cx); + let end = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) + .unwrap(); + editor.remove_folds(&[start..end], false, cx); editor.remove_creases( HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), @@ -2310,7 +2313,7 @@ impl ContextEditor { } if should_refold { - editor.unfold_ranges([patch_start..patch_end], true, false, cx); + editor.unfold_ranges(&[patch_start..patch_end], true, false, cx); editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx); } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 1efb27ef7dd3c..a6f0d4bb91f0f 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -36,7 +36,7 @@ use block_map::{BlockRow, BlockSnapshot}; use collections::{HashMap, HashSet}; pub use crease_map::*; pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint}; -use fold_map::{FoldMap, FoldSnapshot}; +use fold_map::{FoldMap, FoldMapWriter, FoldOffset, FoldSnapshot}; use gpui::{ AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle, }; @@ -65,7 +65,7 @@ use std::{ }; use sum_tree::{Bias, TreeMap}; use tab_map::{TabMap, TabSnapshot}; -use text::LineIndent; +use text::{Edit, LineIndent}; use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext}; use unicode_segmentation::UnicodeSegmentation; use wrap_map::{WrapMap, WrapSnapshot}; @@ -206,34 +206,40 @@ impl DisplayMap { ); } + /// Creates folds for the given ranges. pub fn fold( &mut self, ranges: impl IntoIterator, FoldPlaceholder)>, cx: &mut ModelContext, ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - let edits = self.buffer_subscription.consume().into_inner(); - let tab_size = Self::tab_size(&self.buffer, cx); - let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); - let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); - let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); - let (snapshot, edits) = self - .wrap_map - .update(cx, |map, cx| map.sync(snapshot, edits, cx)); - self.block_map.read(snapshot, edits); - let (snapshot, edits) = fold_map.fold(ranges); - let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); - let (snapshot, edits) = self - .wrap_map - .update(cx, |map, cx| map.sync(snapshot, edits, cx)); - self.block_map.read(snapshot, edits); + self.update_fold_map(cx, |fold_map| fold_map.fold(ranges)) + } + + /// Removes any folds with the given ranges. + pub fn remove_folds( + &mut self, + ranges: impl IntoIterator>, + cx: &mut ModelContext, + ) { + self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges)) } - pub fn unfold( + /// Removes any folds whose ranges intersect any of the given ranges. + pub fn unfold_intersecting( &mut self, ranges: impl IntoIterator>, inclusive: bool, cx: &mut ModelContext, + ) { + self.update_fold_map(cx, |fold_map| { + fold_map.unfold_intersecting(ranges, inclusive) + }) + } + + fn update_fold_map( + &mut self, + cx: &mut ModelContext, + callback: impl FnOnce(&mut FoldMapWriter) -> (FoldSnapshot, Vec>), ) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); @@ -245,7 +251,7 @@ impl DisplayMap { .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); self.block_map.read(snapshot, edits); - let (snapshot, edits) = fold_map.unfold(ranges, inclusive); + let (snapshot, edits) = callback(&mut fold_map); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map @@ -1442,7 +1448,7 @@ pub mod tests { if rng.gen() && fold_count > 0 { log::info!("unfolding ranges: {:?}", ranges); map.update(cx, |map, cx| { - map.unfold(ranges, true, cx); + map.unfold_intersecting(ranges, true, cx); }); } else { log::info!("folding ranges: {:?}", ranges); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 2cfe4b41f5b8d..12e025d08ba71 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -173,31 +173,53 @@ impl<'a> FoldMapWriter<'a> { (self.0.snapshot.clone(), edits) } - pub(crate) fn unfold( + /// Removes any folds with the given ranges. + pub(crate) fn remove_folds( + &mut self, + ranges: impl IntoIterator>, + ) -> (FoldSnapshot, Vec) { + self.remove_folds_with(ranges, |range, fold_range| range == fold_range, false) + } + + /// Removes any folds whose ranges intersect the given ranges. + pub(crate) fn unfold_intersecting( &mut self, ranges: impl IntoIterator>, inclusive: bool, + ) -> (FoldSnapshot, Vec) { + self.remove_folds_with(ranges, |_, _| true, inclusive) + } + + /// Removes any folds that intersect the given ranges and for which the given predicate + /// returns true. + fn remove_folds_with( + &mut self, + ranges: impl IntoIterator>, + should_unfold: fn(&Range, &Range) -> bool, + inclusive: bool, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut fold_ixs_to_delete = Vec::new(); let snapshot = self.0.snapshot.inlay_snapshot.clone(); let buffer = &snapshot.buffer; for range in ranges.into_iter() { - // Remove intersecting folds and add their ranges to edits that are passed to sync. + let range = range.start.to_offset(buffer)..range.end.to_offset(buffer); let mut folds_cursor = - intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive); + intersecting_folds(&snapshot, &self.0.snapshot.folds, range.clone(), inclusive); while let Some(fold) = folds_cursor.item() { let offset_range = fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer); - if offset_range.end > offset_range.start { - let inlay_range = snapshot.to_inlay_offset(offset_range.start) - ..snapshot.to_inlay_offset(offset_range.end); - edits.push(InlayEdit { - old: inlay_range.clone(), - new: inlay_range, - }); + if should_unfold(&range, &offset_range) { + if offset_range.end > offset_range.start { + let inlay_range = snapshot.to_inlay_offset(offset_range.start) + ..snapshot.to_inlay_offset(offset_range.end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range, + }); + } + fold_ixs_to_delete.push(*folds_cursor.start()); } - fold_ixs_to_delete.push(*folds_cursor.start()); folds_cursor.next(buffer); } } @@ -665,6 +687,8 @@ impl FoldSnapshot { where T: ToOffset, { + let buffer = &self.inlay_snapshot.buffer; + let range = range.start.to_offset(buffer)..range.end.to_offset(buffer); let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); iter::from_fn(move || { let item = folds.item(); @@ -821,15 +845,12 @@ fn push_isomorphic(transforms: &mut SumTree, summary: TextSummary) { } } -fn intersecting_folds<'a, T>( +fn intersecting_folds<'a>( inlay_snapshot: &'a InlaySnapshot, folds: &'a SumTree, - range: Range, + range: Range, inclusive: bool, -) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> -where - T: ToOffset, -{ +) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> { let buffer = &inlay_snapshot.buffer; let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); @@ -1419,12 +1440,12 @@ mod tests { assert_eq!(snapshot4.text(), "123a⋯c123456eee"); let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false); + writer.unfold_intersecting(Some(Point::new(0, 4)..Point::new(0, 4)), false); let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot5.text(), "123a⋯c123456eee"); let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true); + writer.unfold_intersecting(Some(Point::new(0, 4)..Point::new(0, 4)), true); let (snapshot6, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee"); } @@ -1913,7 +1934,7 @@ mod tests { log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive); let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); snapshot_edits.push((snapshot, edits)); - let (snapshot, edits) = writer.unfold(to_unfold, inclusive); + let (snapshot, edits) = writer.unfold_intersecting(to_unfold, inclusive); snapshot_edits.push((snapshot, edits)); } _ => { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 205d94b5a9889..da4f5e7da576e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -75,8 +75,8 @@ use gpui::{ AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, - ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, - Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, + ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render, + SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext, }; @@ -1849,7 +1849,7 @@ impl Editor { editor .update(cx, |editor, cx| { editor.unfold_ranges( - [fold_range.start..fold_range.end], + &[fold_range.start..fold_range.end], true, false, cx, @@ -6810,7 +6810,7 @@ impl Editor { } self.transact(cx, |this, cx| { - this.unfold_ranges(unfold_ranges, true, true, cx); + this.unfold_ranges(&unfold_ranges, true, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([(range, text)], None, cx); @@ -6904,7 +6904,7 @@ impl Editor { } self.transact(cx, |this, cx| { - this.unfold_ranges(unfold_ranges, true, true, cx); + this.unfold_ranges(&unfold_ranges, true, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([(range, text)], None, cx); @@ -8256,7 +8256,7 @@ impl Editor { to_unfold.push(selection.start..selection.end); } } - self.unfold_ranges(to_unfold, true, true, cx); + self.unfold_ranges(&to_unfold, true, true, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(new_selection_ranges); }); @@ -8387,7 +8387,7 @@ impl Editor { auto_scroll: Option, cx: &mut ViewContext, ) { - this.unfold_ranges([range.clone()], false, true, cx); + this.unfold_ranges(&[range.clone()], false, true, cx); this.change_selections(auto_scroll, cx, |s| { if replace_newest { s.delete(s.newest_anchor().id); @@ -8598,7 +8598,10 @@ impl Editor { select_next_state.done = true; self.unfold_ranges( - new_selections.iter().map(|selection| selection.range()), + &new_selections + .iter() + .map(|selection| selection.range()) + .collect::>(), false, false, cx, @@ -8667,7 +8670,7 @@ impl Editor { } if let Some(next_selected_range) = next_selected_range { - self.unfold_ranges([next_selected_range.clone()], false, true, cx); + self.unfold_ranges(&[next_selected_range.clone()], false, true, cx); self.change_selections(Some(Autoscroll::newest()), cx, |s| { if action.replace_newest { s.delete(s.newest_anchor().id); @@ -8744,7 +8747,7 @@ impl Editor { } self.unfold_ranges( - selections.iter().map(|s| s.range()).collect::>(), + &selections.iter().map(|s| s.range()).collect::>(), false, true, cx, @@ -10986,7 +10989,7 @@ impl Editor { }) .collect::>(); - self.unfold_ranges(ranges, true, true, cx); + self.unfold_ranges(&ranges, true, true, cx); } pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext) { @@ -11004,7 +11007,7 @@ impl Editor { }) .collect::>(); - self.unfold_ranges(ranges, true, true, cx); + self.unfold_ranges(&ranges, true, true, cx); } pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext) { @@ -11022,13 +11025,13 @@ impl Editor { .iter() .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); - self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx) + self.unfold_ranges(&[intersection_range], true, autoscroll, cx) } pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.unfold_ranges( - [Point::zero()..display_map.max_point().to_point(&display_map)], + &[Point::zero()..display_map.max_point().to_point(&display_map)], true, true, cx, @@ -11104,39 +11107,62 @@ impl Editor { } } + /// Removes any folds whose ranges intersect any of the given ranges. pub fn unfold_ranges( &mut self, - ranges: impl IntoIterator>, + ranges: &[Range], inclusive: bool, auto_scroll: bool, cx: &mut ViewContext, ) { - let mut unfold_ranges = Vec::new(); + self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { + map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx) + }); + } + + /// Removes any folds with the given ranges. + pub fn remove_folds( + &mut self, + ranges: &[Range], + auto_scroll: bool, + cx: &mut ViewContext, + ) { + self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { + map.remove_folds(ranges.iter().cloned(), cx) + }); + } + + fn remove_folds_with( + &mut self, + ranges: &[Range], + auto_scroll: bool, + cx: &mut ViewContext, + update: impl FnOnce(&mut DisplayMap, &mut ModelContext), + ) { + if ranges.is_empty() { + return; + } + let mut buffers_affected = HashMap::default(); let multi_buffer = self.buffer().read(cx); for range in ranges { if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { buffers_affected.insert(buffer.read(cx).remote_id(), buffer); }; - unfold_ranges.push(range); } - let mut ranges = unfold_ranges.into_iter().peekable(); - if ranges.peek().is_some() { - self.display_map - .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); - if auto_scroll { - self.request_autoscroll(Autoscroll::fit(), cx); - } - - for buffer in buffers_affected.into_values() { - self.sync_expanded_diff_hunks(buffer, cx); - } + self.display_map.update(cx, update); + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } - cx.notify(); - self.scrollbar_marker_state.dirty = true; - self.active_indent_guides_state.dirty = true; + for buffer in buffers_affected.into_values() { + self.sync_expanded_diff_hunks(buffer, cx); } + + cx.notify(); + self.scrollbar_marker_state.dirty = true; + self.active_indent_guides_state.dirty = true; } pub fn default_fold_placeholder(&self, cx: &AppContext) -> FoldPlaceholder { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f309b2657d3f0..6ddf73cb00f5c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1280,7 +1280,7 @@ impl SearchableItem for Editor { matches: &[Range], cx: &mut ViewContext, ) { - self.unfold_ranges([matches[index].clone()], false, true, cx); + self.unfold_ranges(&[matches[index].clone()], false, true, cx); let range = self.range_for_match(&matches[index]); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); @@ -1288,7 +1288,7 @@ impl SearchableItem for Editor { } fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { - self.unfold_ranges(matches.to_vec(), false, false, cx); + self.unfold_ranges(matches, false, false, cx); let mut ranges = Vec::new(); for m in matches { ranges.push(self.range_for_match(m)) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 48f53b45f935f..40068ea5f919e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1067,7 +1067,7 @@ impl ProjectSearchView { let range_to_select = match_ranges[new_index].clone(); self.results_editor.update(cx, |editor, cx| { let range_to_select = editor.range_for_match(&range_to_select); - editor.unfold_ranges([range_to_select.clone()], false, true, cx); + editor.unfold_ranges(&[range_to_select.clone()], false, true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range_to_select]) }); From a5276131fe478b518856d06ec22c80c661e20508 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 14:45:34 -0800 Subject: [PATCH 44/49] Ensure slash command output is contained in pending fold while command is running Co-authored-by: Will --- crates/assistant/src/context.rs | 51 +++++++++---- crates/assistant/src/context/context_tests.rs | 75 +++++++------------ 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f98d8e4dd00ba..bc60cd8347d26 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1814,20 +1814,31 @@ impl Context { let version = self.version.clone(); let command_id = SlashCommandId(self.next_timestamp()); - let (insert_position, command_source_range, command_range) = + const PENDING_OUTPUT_END_MARKER: &str = "…"; + + let (command_range, command_source_range, insert_position) = self.buffer.update(cx, |buffer, cx| { let command_source_range = command_source_range.to_offset(buffer); + let mut insertion = format!("\n{PENDING_OUTPUT_END_MARKER}"); + if ensure_trailing_newline { + insertion.push('\n'); + } buffer.edit( - [(command_source_range.end..command_source_range.end, "\n")], + [( + command_source_range.end..command_source_range.end, + insertion, + )], None, cx, ); let insert_position = buffer.anchor_after(command_source_range.end + 1); - let output_range = - buffer.anchor_before(command_source_range.start)..insert_position; + let command_range = buffer.anchor_before(command_source_range.start) + ..buffer.anchor_after( + command_source_range.end + 1 + PENDING_OUTPUT_END_MARKER.len(), + ); let command_source_range = buffer.anchor_before(command_source_range.start) ..buffer.anchor_before(command_source_range.end + 1); - (insert_position, command_source_range, output_range) + (command_range, command_source_range, insert_position) }); self.reparse(cx); @@ -1945,21 +1956,33 @@ impl Context { this.update(&mut cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit([(command_source_range, "")], None, cx); - - if ensure_trailing_newline { - let offset = insert_position.to_offset(buffer); - let newline_offset = offset.saturating_sub(1); - if !buffer.contains_str_at(newline_offset, "\n") - || last_section_range.map_or(false, |last_section_range| { - last_section_range + let mut deletions = vec![(command_source_range.to_offset(buffer), "")]; + let insert_position = insert_position.to_offset(buffer); + let command_range_end = command_range.end.to_offset(buffer); + + if buffer.contains_str_at(insert_position, PENDING_OUTPUT_END_MARKER) { + deletions.push(( + insert_position..insert_position + PENDING_OUTPUT_END_MARKER.len(), + "", + )); + } + + if ensure_trailing_newline + && buffer.contains_str_at(command_range_end, "\n") + { + let newline_offset = insert_position.saturating_sub(1); + if buffer.contains_str_at(newline_offset, "\n") + && last_section_range.map_or(true, |last_section_range| { + !last_section_range .to_offset(buffer) .contains(&newline_offset) }) { - buffer.edit([(offset..offset, "\n")], None, cx); + deletions.push((command_range_end..command_range_end + 1, "")); } } + + buffer.edit(deletions, None, cx); }); })?; diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 05279105f3e2d..2a24e7278166a 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -435,11 +435,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " - «/file src/lib.rs» - " - .unindent() - .trim_end(), + &" + «/file src/lib.rs»" + .unindent(), cx, ); @@ -451,11 +449,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " - «/file src/main.rs» - " - .unindent() - .trim_end(), + &" + «/file src/main.rs»" + .unindent(), cx, ); @@ -471,11 +467,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " - /unknown src/main.rs - " - .unindent() - .trim_end(), + &" + /unknown src/main.rs" + .unindent(), cx, ); @@ -484,23 +478,20 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " - «/file src/main.rs» - " - .unindent() - .trim_end(), + &" + «/file src/main.rs»" + .unindent(), cx, ); let (command_output_tx, command_output_rx) = mpsc::unbounded(); context.update(cx, |context, cx| { - let buffer = context.buffer.read(cx); - let command_source_range = buffer.anchor_after(0)..buffer.anchor_before(buffer.len()); + let command_source_range = context.parsed_slash_commands[0].source_range.clone(); context.insert_command_output( command_source_range, "file", Task::ready(Ok(command_output_rx.boxed())), - false, + true, false, cx, ); @@ -508,13 +499,11 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " + &" ⟦«/file src/main.rs» - ⟧ - + …⟧ " - .unindent() - .trim_end(), + .unindent(), cx, ); @@ -532,13 +521,11 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " + &" ⟦«/file src/main.rs» - src/main.rs⟧ - + src/main.rs…⟧ " - .unindent() - .trim_end(), + .unindent(), cx, ); @@ -549,14 +536,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " + &" ⟦«/file src/main.rs» src/main.rs - fn main() {}⟧ - + fn main() {}…⟧ " - .unindent() - .trim_end(), + .unindent(), cx, ); @@ -567,14 +552,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " + &" ⟦«/file src/main.rs» ⟪src/main.rs - fn main() {}⟫⟧ - + fn main() {}⟫…⟧ " - .unindent() - .trim_end(), + .unindent(), cx, ); @@ -583,13 +566,11 @@ async fn test_slash_commands(cx: &mut TestAppContext) { assert_text_and_context_ranges( &buffer, &context_ranges, - " + &" ⟦⟪src/main.rs fn main() {}⟫⟧ - " - .unindent() - .trim_end(), + .unindent(), cx, ); From 798568e4d85290979c04dbf5c096a9d5abbc5ac3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 15:10:25 -0800 Subject: [PATCH 45/49] On slash command completion, remove pending output folds by its placeholder type tag Co-authored-by: Will --- crates/assistant/src/assistant_panel.rs | 31 ++++++++++++----------- crates/editor/src/display_map.rs | 5 ++-- crates/editor/src/display_map/fold_map.rs | 31 ++++++++++++++++++----- crates/editor/src/editor.rs | 6 +++-- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 6bcebd14b411c..334196f3f4706 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1866,8 +1866,7 @@ impl ContextEditor { IconName::PocketKnife, tool_use.name.clone().into(), ), - constrain_width: false, - merge_adjacent: false, + ..Default::default() }; let render_trailer = move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any(); @@ -1958,8 +1957,7 @@ impl ContextEditor { }); let placeholder = FoldPlaceholder { render: Arc::new(move |_, _, _| Empty.into_any()), - constrain_width: false, - merge_adjacent: false, + ..Default::default() }; let render_toggle = { let confirm_command = confirm_command.clone(); @@ -2077,8 +2075,7 @@ impl ContextEditor { IconName::PocketKnife, format!("Tool Result: {tool_use_id}").into(), ), - constrain_width: false, - merge_adjacent: false, + ..Default::default() }; let render_trailer = move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any(); @@ -2137,7 +2134,12 @@ impl ContextEditor { let end = buffer .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) .unwrap(); - editor.remove_folds(&[start..end], false, cx); + editor.remove_folds_with_type( + &[start..end], + TypeId::of::(), + false, + cx, + ); editor.remove_creases( HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), @@ -2254,8 +2256,7 @@ impl ContextEditor { .unwrap_or_else(|| Empty.into_any()) }) }, - constrain_width: false, - merge_adjacent: false, + ..Default::default() }; let should_refold; @@ -2359,8 +2360,7 @@ impl ContextEditor { section.icon, section.label.clone(), ), - constrain_width: false, - merge_adjacent: false, + ..Default::default() }, render_slash_command_output_toggle, |_, _, _| Empty.into_any_element(), @@ -3289,13 +3289,12 @@ impl ContextEditor { Crease::new( start..end, FoldPlaceholder { - constrain_width: false, render: render_fold_icon_button( weak_editor.clone(), metadata.crease.icon, metadata.crease.label.clone(), ), - merge_adjacent: false, + ..Default::default() }, render_slash_command_output_toggle, |_, _, _| Empty.into_any(), @@ -4961,8 +4960,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakView) -> .into_any_element() } }), - constrain_width: false, - merge_adjacent: false, + ..Default::default() } } @@ -5090,6 +5088,8 @@ fn make_lsp_adapter_delegate( }) } +enum PendingSlashCommand {} + fn invoked_slash_command_fold_placeholder( command_id: SlashCommandId, context: WeakModel, @@ -5158,6 +5158,7 @@ fn invoked_slash_command_fold_placeholder( }) .into_any_element() }), + type_tag: Some(TypeId::of::()), } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a6f0d4bb91f0f..8211862840840 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -216,12 +216,13 @@ impl DisplayMap { } /// Removes any folds with the given ranges. - pub fn remove_folds( + pub fn remove_folds_with_type( &mut self, ranges: impl IntoIterator>, + type_id: TypeId, cx: &mut ModelContext, ) { - self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges)) + self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges, type_id)) } /// Removes any folds whose ranges intersect any of the given ranges. diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 12e025d08ba71..c4bb4080e2613 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -6,12 +6,14 @@ use gpui::{AnyElement, ElementId, WindowContext}; use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary}; use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset}; use std::{ + any::TypeId, cmp::{self, Ordering}, fmt, iter, ops::{Add, AddAssign, Deref, DerefMut, Range, Sub}, sync::Arc, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary}; +use ui::IntoElement as _; use util::post_inc; #[derive(Clone)] @@ -22,17 +24,29 @@ pub struct FoldPlaceholder { pub constrain_width: bool, /// If true, merges the fold with an adjacent one. pub merge_adjacent: bool, + /// Category of the fold. Useful for carefully removing from overlapping folds. + pub type_tag: Option, +} + +impl Default for FoldPlaceholder { + fn default() -> Self { + Self { + render: Arc::new(|_, _, _| gpui::Empty.into_any_element()), + constrain_width: true, + merge_adjacent: true, + type_tag: None, + } + } } impl FoldPlaceholder { #[cfg(any(test, feature = "test-support"))] pub fn test() -> Self { - use gpui::IntoElement; - Self { render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()), constrain_width: true, merge_adjacent: true, + type_tag: None, } } } @@ -177,8 +191,13 @@ impl<'a> FoldMapWriter<'a> { pub(crate) fn remove_folds( &mut self, ranges: impl IntoIterator>, + type_id: TypeId, ) -> (FoldSnapshot, Vec) { - self.remove_folds_with(ranges, |range, fold_range| range == fold_range, false) + self.remove_folds_with( + ranges, + |fold| fold.placeholder.type_tag == Some(type_id), + false, + ) } /// Removes any folds whose ranges intersect the given ranges. @@ -187,7 +206,7 @@ impl<'a> FoldMapWriter<'a> { ranges: impl IntoIterator>, inclusive: bool, ) -> (FoldSnapshot, Vec) { - self.remove_folds_with(ranges, |_, _| true, inclusive) + self.remove_folds_with(ranges, |_| true, inclusive) } /// Removes any folds that intersect the given ranges and for which the given predicate @@ -195,7 +214,7 @@ impl<'a> FoldMapWriter<'a> { fn remove_folds_with( &mut self, ranges: impl IntoIterator>, - should_unfold: fn(&Range, &Range) -> bool, + should_unfold: impl Fn(&Fold) -> bool, inclusive: bool, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); @@ -209,7 +228,7 @@ impl<'a> FoldMapWriter<'a> { while let Some(fold) = folds_cursor.item() { let offset_range = fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer); - if should_unfold(&range, &offset_range) { + if should_unfold(fold) { if offset_range.end > offset_range.start { let inlay_range = snapshot.to_inlay_offset(offset_range.start) ..snapshot.to_inlay_offset(offset_range.end); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index da4f5e7da576e..345f436958b68 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1861,6 +1861,7 @@ impl Editor { .into_any() }), merge_adjacent: true, + ..Default::default() }; let display_map = cx.new_model(|cx| { DisplayMap::new( @@ -11121,14 +11122,15 @@ impl Editor { } /// Removes any folds with the given ranges. - pub fn remove_folds( + pub fn remove_folds_with_type( &mut self, ranges: &[Range], + type_id: TypeId, auto_scroll: bool, cx: &mut ViewContext, ) { self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { - map.remove_folds(ranges.iter().cloned(), cx) + map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx) }); } From 068d2d461566185b7cf03fc38d273e3502b64c94 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 15:31:11 -0800 Subject: [PATCH 46/49] Remove unused expand_result parameters and fields --- crates/assistant/src/assistant_panel.rs | 9 +-------- crates/assistant/src/context.rs | 2 -- crates/assistant/src/context/context_tests.rs | 2 -- crates/assistant/src/slash_command.rs | 2 -- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 334196f3f4706..d798e35627cd8 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1580,7 +1580,6 @@ impl ContextEditor { &command.name, &command.arguments, false, - false, self.workspace.clone(), cx, ); @@ -1753,7 +1752,6 @@ impl ContextEditor { &command.name, &command.arguments, true, - false, workspace.clone(), cx, ); @@ -1769,7 +1767,6 @@ impl ContextEditor { name: &str, arguments: &[String], ensure_trailing_newline: bool, - expand_result: bool, workspace: WeakView, cx: &mut ViewContext, ) { @@ -1796,7 +1793,6 @@ impl ContextEditor { name, output, ensure_trailing_newline, - expand_result, cx, ) }); @@ -1947,7 +1943,6 @@ impl ContextEditor { &command.name, &command.arguments, false, - false, workspace.clone(), cx, ); @@ -2016,7 +2011,6 @@ impl ContextEditor { ContextEvent::SlashCommandFinished { output_range: _output_range, run_commands_in_ranges, - expand_result, } => { for range in run_commands_in_ranges { let commands = self.context.update(cx, |context, cx| { @@ -2032,7 +2026,6 @@ impl ContextEditor { &command.name, &command.arguments, false, - false, self.workspace.clone(), cx, ); @@ -5098,7 +5091,7 @@ fn invoked_slash_command_fold_placeholder( FoldPlaceholder { constrain_width: false, merge_adjacent: false, - render: Arc::new(move |fold_id, fold_range, cx| { + render: Arc::new(move |fold_id, _, cx| { let Some(context) = context.upgrade() else { return Empty.into_any(); }; diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index bc60cd8347d26..3918b87a7b3d7 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -384,7 +384,6 @@ pub enum ContextEvent { SlashCommandFinished { output_range: Range, run_commands_in_ranges: Vec>, - expand_result: bool, }, UsePendingTools, ToolFinished { @@ -1808,7 +1807,6 @@ impl Context { name: &str, output: Task, ensure_trailing_newline: bool, - expand_result: bool, cx: &mut ModelContext, ) { let version = self.version.clone(); diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 2a24e7278166a..b105e35b86087 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -492,7 +492,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) { "file", Task::ready(Ok(command_output_rx.boxed())), true, - false, cx, ); }); @@ -1281,7 +1280,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std "/command", Task::ready(Ok(stream::iter(events).boxed())), true, - false, cx, ); }); diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index eec039d9429be..44641701b2246 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -127,7 +127,6 @@ impl SlashCommandCompletionProvider { &command_name, &[], true, - false, workspace.clone(), cx, ); @@ -212,7 +211,6 @@ impl SlashCommandCompletionProvider { &command_name, &completed_arguments, true, - false, workspace.clone(), cx, ); From 1b6627b3fb8d00bd9871b02e1aaef85f678edb24 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 15:37:06 -0800 Subject: [PATCH 47/49] Fix clippy warnings --- crates/assistant/src/assistant_panel.rs | 15 +++++++-------- crates/assistant/src/context/context_tests.rs | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d798e35627cd8..3d44a103f630d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -23,7 +23,7 @@ use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; use assistant_tool::ToolRegistry; use client::{proto, zed_urls, Client, Status}; -use collections::{BTreeSet, HashMap, HashSet}; +use collections::{hash_map, BTreeSet, HashMap, HashSet}; use editor::{ actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, display_map::{ @@ -2138,9 +2138,9 @@ impl ContextEditor { HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), cx, ); - } else if self.invoked_slash_command_creases.contains_key(&command_id) { - cx.notify(); - } else { + } else if let hash_map::Entry::Vacant(entry) = + self.invoked_slash_command_creases.entry(command_id) + { let buffer = editor.buffer().read(cx).snapshot(cx); let (&excerpt_id, _buffer_id, _buffer_snapshot) = buffer.as_singleton().unwrap(); @@ -2162,11 +2162,10 @@ impl ContextEditor { )], cx, ); - editor.fold_ranges([(crease_start..crease_end, fold_placeholder)], false, cx); - - self.invoked_slash_command_creases - .insert(command_id, crease_ids[0]); + entry.insert(crease_ids[0]); + } else { + cx.notify() } } else { editor.remove_creases( diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index b105e35b86087..571a20b8979f9 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -574,7 +574,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { ); #[track_caller] - fn assert_text_and_context_ranges<'a>( + fn assert_text_and_context_ranges( buffer: &Model, ranges: &RefCell, expected_marked_text: &str, From d4eac88f51ed2ba5d8674ec5600be8e4244fa4f2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 15:40:17 -0800 Subject: [PATCH 48/49] Fix spelling error --- crates/assistant/src/context/context_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 571a20b8979f9..fea022c88a9c4 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -473,7 +473,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { cx, ); - // Undoing the insertion of an non-existant slash command resorts the previous one. + // Undoing the insertion of an non-existent slash command resorts the previous one. buffer.update(cx, |buffer, cx| buffer.undo(cx)); assert_text_and_context_ranges( &buffer, From f6eee03d09f216674c602adba42aaff9bee33e8e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Nov 2024 15:47:57 -0800 Subject: [PATCH 49/49] Rename new slash command message to appease protobuf checker --- crates/assistant/src/context.rs | 6 +++--- crates/proto/proto/zed.proto | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 3918b87a7b3d7..c3a1d04ec9edd 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -196,7 +196,7 @@ impl ContextOperation { version: language::proto::deserialize_version(&message.version), }) } - proto::context_operation::Variant::SlashCommandFinished(message) => { + proto::context_operation::Variant::SlashCommandCompleted(message) => { Ok(Self::SlashCommandFinished { id: SlashCommandId(language::proto::deserialize_timestamp( message.id.context("invalid id")?, @@ -310,8 +310,8 @@ impl ContextOperation { error_message, version, } => proto::ContextOperation { - variant: Some(proto::context_operation::Variant::SlashCommandFinished( - proto::context_operation::SlashCommandFinished { + variant: Some(proto::context_operation::Variant::SlashCommandCompleted( + proto::context_operation::SlashCommandCompleted { id: Some(language::proto::serialize_timestamp(id.0)), timestamp: Some(language::proto::serialize_timestamp(*timestamp)), error_message: error_message.clone(), diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index fa327d529a221..c727296d6406f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2254,7 +2254,7 @@ message ContextOperation { BufferOperation buffer_operation = 5; SlashCommandStarted slash_command_started = 6; SlashCommandOutputSectionAdded slash_command_output_section_added = 7; - SlashCommandFinished slash_command_finished = 8; + SlashCommandCompleted slash_command_completed = 8; } reserved 4; @@ -2292,7 +2292,7 @@ message ContextOperation { repeated VectorClockEntry version = 3; } - message SlashCommandFinished { + message SlashCommandCompleted { LamportTimestamp id = 1; LamportTimestamp timestamp = 3; optional string error_message = 4;