diff --git a/Cargo.lock b/Cargo.lock index 2075d4b4ef58e..7bf98ef6fc632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,6 +463,7 @@ dependencies = [ "futures 0.3.30", "gpui", "language", + "language_model", "parking_lot", "pretty_assertions", "serde", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 23aeec839b641..3d44a103f630d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,3 +1,4 @@ +use crate::slash_command::file_command::codeblock_fence_for_path; use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, humanize_token_count, @@ -6,24 +7,23 @@ use crate::{ slash_command::{ 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, Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context, ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, - InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate, - ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, - RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus, - ToggleModelSelector, + InsertIntoEditor, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, + MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, ParsedSlashCommand, + PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, RequestType, + SavedContextMetadata, SlashCommandId, Split, ToggleFocus, ToggleModelSelector, }; 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::{ @@ -38,12 +38,12 @@ use editor::{display_map::CreaseId, FoldPlaceholder}; use fs::Fs; use futures::FutureExt; use gpui::{ - canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt, - AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem, - CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView, - FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage, - SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, - UpdateGlobal, View, VisualContext, WeakView, WindowContext, + canvas, div, img, percentage, point, prelude::*, pulsating_between, size, Action, Animation, + AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, + ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, + FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, + Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, + Task, Transformation, UpdateGlobal, View, WeakModel, WeakView, }; use indexed_docs::IndexedDocsStore; use language::{ @@ -77,8 +77,8 @@ 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 util::{maybe, ResultExt}; use workspace::{ @@ -1477,7 +1477,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_creases: HashMap, pending_tool_use_creases: HashMap, CreaseId>, _subscriptions: Vec, patches: HashMap, PatchViewState>, @@ -1548,7 +1548,7 @@ impl ContextEditor { workspace, project, pending_slash_command_creases: HashMap::default(), - pending_slash_command_blocks: HashMap::default(), + invoked_slash_command_creases: HashMap::default(), pending_tool_use_creases: HashMap::default(), _subscriptions, patches: HashMap::default(), @@ -1573,14 +1573,13 @@ 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, &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, ) { @@ -1793,9 +1790,9 @@ impl ContextEditor { self.context.update(cx, |context, cx| { context.insert_command_output( command_range, + name, output, ensure_trailing_newline, - expand_result, cx, ) }); @@ -1865,8 +1862,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(); @@ -1921,11 +1917,10 @@ impl ContextEditor { ContextEvent::PatchesUpdated { removed, updated } => { self.patches_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 @@ -1934,16 +1929,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(); @@ -1958,7 +1943,6 @@ impl ContextEditor { &command.name, &command.arguments, false, - false, workspace.clone(), cx, ); @@ -1968,8 +1952,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(); @@ -2011,62 +1994,29 @@ 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, - 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() .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::SlashCommandOutputSectionAdded { section } => { + self.insert_slash_command_output_sections([section.clone()], false, cx); + } ContextEvent::SlashCommandFinished { - output_range, - sections, - run_commands_in_output, - expand_result, + output_range: _output_range, + run_commands_in_ranges, } => { - self.insert_slash_command_output_sections( - sections.iter().cloned(), - *expand_result, - 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() }); @@ -2076,7 +2026,6 @@ impl ContextEditor { &command.name, &command.arguments, false, - false, self.workspace.clone(), cx, ); @@ -2119,8 +2068,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(); @@ -2158,6 +2106,77 @@ impl ContextEditor { } } + fn update_invoked_slash_command( + &mut self, + 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) + { + 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(); + let end = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) + .unwrap(); + 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)), + cx, + ); + } 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(); + 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( + 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); + entry.insert(crease_ids[0]); + } else { + cx.notify() + } + } else { + editor.remove_creases( + HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), + cx, + ); + cx.notify(); + }; + }); + } + fn patches_updated( &mut self, removed: &Vec>, @@ -2229,8 +2248,7 @@ impl ContextEditor { .unwrap_or_else(|| Empty.into_any()) }) }, - constrain_width: false, - merge_adjacent: false, + ..Default::default() }; let should_refold; @@ -2288,7 +2306,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); } } @@ -2334,8 +2352,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(), @@ -3264,13 +3281,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(), @@ -4936,8 +4952,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakView) -> .into_any_element() } }), - constrain_width: false, - merge_adjacent: false, + ..Default::default() } } @@ -4981,7 +4996,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() { @@ -5065,17 +5080,78 @@ 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() - }) +enum PendingSlashCommand {} + +fn invoked_slash_command_fold_placeholder( + command_id: SlashCommandId, + context: WeakModel, + context_editor: WeakView, +) -> FoldPlaceholder { + FoldPlaceholder { + constrain_width: false, + merge_adjacent: false, + render: Arc::new(move |fold_id, _, 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(); + }; + + 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() + }), + type_tag: Some(TypeId::of::()), + } } enum TokenState { diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 9290e59d85e2a..c3a1d04ec9edd 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -8,7 +8,8 @@ use crate::{ }; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ - SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, + SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry, + SlashCommandResult, }; use assistant_tool::ToolRegistry; use client::{self, proto, telemetry::Telemetry}; @@ -47,9 +48,10 @@ 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; #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct ContextId(String); @@ -92,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), @@ -152,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::SlashCommandCompleted(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( @@ -231,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( @@ -257,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::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(), version: language::proto::serialize_version(version), }, )), @@ -278,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") } @@ -291,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") @@ -311,15 +371,19 @@ pub enum ContextEvent { removed: Vec>, updated: Vec>, }, - PendingSlashCommandsUpdated { + InvokedSlashCommandChanged { + command_id: SlashCommandId, + }, + ParsedSlashCommandsUpdated { removed: Vec>, - updated: Vec, + updated: Vec, + }, + SlashCommandOutputSectionAdded { + section: SlashCommandOutputSection, }, SlashCommandFinished { output_range: Range, - sections: Vec>, - run_commands_in_output: bool, - expand_result: bool, + run_commands_in_ranges: Vec>, }, UsePendingTools, ToolFinished { @@ -478,7 +542,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>, @@ -508,7 +573,7 @@ trait ContextAnnotation { fn range(&self) -> &Range; } -impl ContextAnnotation for PendingSlashCommand { +impl ContextAnnotation for ParsedSlashCommand { fn range(&self) -> &Range { &self.source_range } @@ -580,7 +645,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(), @@ -827,24 +893,50 @@ 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(())), + }, + ); + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); + } + 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_output: false, - }); + .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!(), @@ -882,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)); @@ -983,8 +1077,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] { @@ -1306,7 +1407,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, }); @@ -1324,7 +1425,7 @@ impl Context { &mut self, range: Range, buffer: &BufferSnapshot, - updated: &mut Vec, + updated: &mut Vec, removed: &mut Vec>, cx: &AppContext, ) { @@ -1358,7 +1459,7 @@ impl Context { .map_or(command_line.name.end, |argument| argument.end); let source_range = buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); - let pending_command = PendingSlashCommand { + let pending_command = ParsedSlashCommand { name: name.to_string(), arguments, source_range, @@ -1373,7 +1474,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)); } @@ -1642,15 +1743,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() { @@ -1666,9 +1767,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( @@ -1676,7 +1777,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( @@ -1702,112 +1803,275 @@ impl Context { pub fn insert_command_output( &mut self, - command_range: Range, + command_source_range: Range, + name: &str, output: Task, ensure_trailing_newline: bool, - expand_result: bool, cx: &mut ModelContext, ) { + let version = self.version.clone(); + let command_id = SlashCommandId(self.next_timestamp()); + + 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, + insertion, + )], + None, + cx, + ); + let insert_position = buffer.anchor_after(command_source_range.end + 1); + 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); + (command_range, command_source_range, insert_position) + }); self.reparse(cx); - let insert_output_task = cx.spawn(|this, mut cx| { - 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) => { - output.ensure_valid_section_ranges(); + let insert_output_task = cx.spawn(|this, mut cx| async move { + let run_command = async { + let mut stream = output.await?; - // 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') + struct PendingSection { + start: language::Anchor, + icon: IconName, + label: SharedString, + metadata: Option, + } + + let mut pending_section_stack: Vec = Vec::new(); + let mut run_commands_in_ranges: Vec> = Vec::new(); + let mut last_role: Option = None; + let mut last_section_range = None; + + while let Some(event) = stream.next().await { + let event = event?; + match event { + 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| { + insert_position.to_offset(buffer) + }); + this.insert_message_at_offset( + offset, + role, + MessageStatus::Pending, + cx, + ); + })?; + } + + last_role = Some(role); + } + SlashCommandEvent::StartSection { + 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, + }); }); - if !has_newline_after_last_section { - output.text.push('\n'); + })?; + } + 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); + } + + result + })?; + } + SlashCommandEvent::EndSection { metadata } => { + if let Some(pending_section) = pending_section_stack.pop() { + this.update(&mut cx, |this, cx| { + 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); + })?; } } + } + } - 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, + this.update(&mut cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + 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) }) - .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, - }, - ) - }); + { + deletions.push((command_range_end..command_range_end + 1, "")); + } + } + + buffer.edit(deletions, None, cx); + }); + })?; + + debug_assert!(pending_section_stack.is_empty()); + + anyhow::Ok(()) + }; + + let command_result = run_command.await; - this.push_op(operation, cx); - cx.emit(event); + 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) => { - 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()], - }); - } + let message = error.to_string(); + invoked_slash_command.status = + InvokedSlashCommandStatus::Error(message.clone().into()); + error_message = Some(message); } - }) - .ok(); - } + } + + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + this.push_op( + ContextOperation::SlashCommandFinished { + id: command_id, + timestamp, + error_message, + version, + }, + cx, + ); + }) + .ok(); }); - 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 { + name: name.to_string().into(), + 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( + &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(), + }); + let version = self.version.clone(); + let timestamp = self.next_timestamp(); + self.push_op( + ContextOperation::SlashCommandOutputSectionAdded { + timestamp, + section, + version, + }, + cx, + ); } pub fn insert_tool_output( @@ -2312,43 +2576,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 @@ -2814,13 +3089,27 @@ 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 name: SharedString, + pub range: Range, + pub status: InvokedSlashCommandStatus, +} + +#[derive(Debug)] +pub enum InvokedSlashCommandStatus { + Running(Task<()>), + Error(SharedString), + Finished, +} + #[derive(Debug, Clone)] pub enum PendingSlashCommandStatus { Idle, @@ -2960,27 +3249,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 ecbe272693503..fea022c88a9c4 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -2,14 +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, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandRegistry, SlashCommandResult, + ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput, + SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, }; -use collections::HashSet; +use collections::{HashMap, HashSet}; use fs::FakeFs; +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}; @@ -27,8 +32,8 @@ use std::{ rc::Rc, sync::{atomic::AtomicBool, Arc}, }; -use text::{network::Network, OffsetRangeExt as _, ReplicaId}; -use ui::{Context as _, WindowContext}; +use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset}; +use ui::{Context as _, IconName, WindowContext}; use unindent::Unindent; use util::{ test::{generate_marked_text, marked_text_ranges}, @@ -381,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::PendingSlashCommandsUpdated { 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(); @@ -406,14 +432,12 @@ 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(), - " - «/file src/lib.rs» - " - .unindent() - .trim_end(), + &context_ranges, + &" + «/file src/lib.rs»" + .unindent(), cx, ); @@ -422,14 +446,12 @@ 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(), - " - «/file src/main.rs» - " - .unindent() - .trim_end(), + &context_ranges, + &" + «/file src/main.rs»" + .unindent(), cx, ); @@ -442,36 +464,180 @@ async fn test_slash_commands(cx: &mut TestAppContext) { cx, ); }); - assert_text_and_output_ranges( + assert_text_and_context_ranges( + &buffer, + &context_ranges, + &" + /unknown src/main.rs" + .unindent(), + cx, + ); + + // 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, + &context_ranges, + &" + «/file src/main.rs»" + .unindent(), + cx, + ); + + let (command_output_tx, command_output_rx) = mpsc::unbounded(); + context.update(cx, |context, cx| { + 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())), + true, + cx, + ); + }); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + &" + ⟦«/file src/main.rs» + …⟧ + " + .unindent(), + 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".into()))) + .unwrap(); + cx.run_until_parked(); + assert_text_and_context_ranges( + &buffer, + &context_ranges, + &" + ⟦«/file src/main.rs» + src/main.rs…⟧ + " + .unindent(), + cx, + ); + + command_output_tx + .unbounded_send(Ok(SlashCommandEvent::Content("\nfn 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(), + cx, + ); + + command_output_tx + .unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None })) + .unwrap(); + cx.run_until_parked(); + assert_text_and_context_ranges( &buffer, - &output_ranges.borrow(), + &context_ranges, + &" + ⟦«/file src/main.rs» + ⟪src/main.rs + fn main() {}⟫…⟧ " - /unknown src/main.rs + .unindent(), + 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(), + .unindent(), cx, ); #[track_caller] - fn assert_text_and_output_ranges( + fn assert_text_and_context_ranges( 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: '⟦', + }); + } + for range in ranges.parsed_commands.iter() { + endpoints.push(Endpoint { + offset: range.start.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; + 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); } } @@ -1063,44 +1229,57 @@ 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![Ok(SlashCommandEvent::StartMessage { + role: Role::User, + merge_same_roles: true, + })]; + 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 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(), metadata: None, - }); + })); + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + text: output_text[section_start..section_end].to_string(), + run_commands_in_text: false, + }))); + events.push(Ok(SlashCommandEvent::EndSection { metadata: None })); + section_start = section_end; + } + + if section_start < output_text.len() { + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + 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 { - text: output_text, - sections, - run_commands_in_text: false, - } - .to_event_stream())), + "/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, ); 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(()) diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index 8ec5b729c9360..9f862a3d26d10 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -18,6 +18,7 @@ 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 58f4fcb9b4ac8..fcac07bbd0381 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -5,6 +5,7 @@ use futures::stream::{self, BoxStream}; use futures::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::{ @@ -103,7 +104,7 @@ pub type RenderFoldPlaceholder = Arc< + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, >; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq)] pub enum SlashCommandContent { Text { text: String, @@ -111,8 +112,21 @@ pub enum SlashCommandContent { }, } -#[derive(Debug, PartialEq, Eq)] +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 { + role: Role, + merge_same_roles: bool, + }, StartSection { icon: IconName, label: SharedString, @@ -232,6 +246,7 @@ impl SlashCommandOutput { output.sections.push(section); } } + SlashCommandEvent::StartMessage { .. } => {} } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 1efb27ef7dd3c..8211862840840 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,41 @@ 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_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, type_id)) } - 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 +252,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 +1449,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..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, } } } @@ -173,31 +187,58 @@ 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>, + type_id: TypeId, + ) -> (FoldSnapshot, Vec) { + self.remove_folds_with( + ranges, + |fold| fold.placeholder.type_tag == Some(type_id), + 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: impl Fn(&Fold) -> 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(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); + 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 +706,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 +864,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 +1459,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 +1953,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..345f436958b68 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, @@ -1861,6 +1861,7 @@ impl Editor { .into_any() }), merge_adjacent: true, + ..Default::default() }; let display_map = cx.new_model(|cx| { DisplayMap::new( @@ -6810,7 +6811,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 +6905,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 +8257,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 +8388,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 +8599,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 +8671,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 +8748,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 +10990,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 +11008,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 +11026,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 +11108,63 @@ 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_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_with_type(ranges.iter().cloned(), type_id, 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/proto/proto/zed.proto b/crates/proto/proto/zed.proto index e43b622545e4b..c727296d6406f 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; + SlashCommandCompleted slash_command_completed = 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 SlashCommandCompleted { + LamportTimestamp id = 1; + LamportTimestamp timestamp = 3; + optional string error_message = 4; + repeated VectorClockEntry version = 5; + } + message BufferOperation { Operation operation = 1; } 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]) });