Cargo.lock 🔗
@@ -463,6 +463,7 @@ dependencies = [
"futures 0.3.30",
"gpui",
"language",
+ "language_model",
"parking_lot",
"pretty_assertions",
"serde",
Marshall Bowers , David Soria Parra , Antonio Scandurra , David , Antonio , Max , Max Brunsfeld , and Will created
This PR adds support for streaming output from slash commands
In this PR we are focused primarily on the interface of the
`SlashCommand` trait to support streaming the output. We will follow up
later with support for extensions and context servers to take advantage
of the streaming nature.
Release Notes:
- N/A
---------
Co-authored-by: David Soria Parra <davidsp@anthropic.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: David <david@anthropic.com>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Will <will@zed.dev>
Cargo.lock | 1
crates/assistant/src/assistant_panel.rs | 290
crates/assistant/src/context.rs | 745 ++
crates/assistant/src/context/context_tests.rs | 305
crates/assistant/src/slash_command.rs | 2
crates/assistant/src/slash_command/streaming_example_command.rs | 18
crates/assistant_slash_command/Cargo.toml | 1
crates/assistant_slash_command/src/assistant_slash_command.rs | 19
crates/editor/src/display_map.rs | 49
crates/editor/src/display_map/fold_map.rs | 84
crates/editor/src/editor.rs | 90
crates/editor/src/items.rs | 4
crates/proto/proto/zed.proto | 23
crates/search/src/project_search.rs | 2
14 files changed, 1,131 insertions(+), 502 deletions(-)
@@ -463,6 +463,7 @@ dependencies = [
"futures 0.3.30",
"gpui",
"language",
+ "language_model",
"parking_lot",
"pretty_assertions",
"serde",
@@ -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<ScrollPosition>,
remote_id: Option<workspace::ViewId>,
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
- pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
+ invoked_slash_command_creases: HashMap<SlashCommandId, CreaseId>,
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
_subscriptions: Vec<Subscription>,
patches: HashMap<Range<language::Anchor>, 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<Workspace>,
cx: &mut ViewContext<Self>,
) {
@@ -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<Self>,
+ ) {
+ 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::<PendingSlashCommand>(),
+ 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<Range<text::Anchor>>,
@@ -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(),
@@ -3275,13 +3292,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(),
@@ -4947,8 +4963,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) ->
.into_any_element()
}
}),
- constrain_width: false,
- merge_adjacent: false,
+ ..Default::default()
}
}
@@ -4992,7 +5007,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() {
@@ -5076,17 +5091,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>,
+ context_editor: WeakView<ContextEditor>,
+) -> 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::<PendingSlashCommand>()),
+ }
}
enum TokenState {
@@ -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<language::Anchor>,
- sections: Vec<SlashCommandOutputSection<language::Anchor>>,
+ name: String,
+ version: clock::Global,
+ },
+ SlashCommandFinished {
+ id: SlashCommandId,
+ timestamp: clock::Lamport,
+ error_message: Option<String>,
+ version: clock::Global,
+ },
+ SlashCommandOutputSectionAdded {
+ timestamp: clock::Lamport,
+ section: SlashCommandOutputSection<language::Anchor>,
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::<Result<Vec<_>>>()?,
- 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<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>,
},
- PendingSlashCommandsUpdated {
+ InvokedSlashCommandChanged {
+ command_id: SlashCommandId,
+ },
+ ParsedSlashCommandsUpdated {
removed: Vec<Range<language::Anchor>>,
- updated: Vec<PendingSlashCommand>,
+ updated: Vec<ParsedSlashCommand>,
+ },
+ SlashCommandOutputSectionAdded {
+ section: SlashCommandOutputSection<language::Anchor>,
},
SlashCommandFinished {
output_range: Range<language::Anchor>,
- sections: Vec<SlashCommandOutputSection<language::Anchor>>,
- run_commands_in_output: bool,
- expand_result: bool,
+ run_commands_in_ranges: Vec<Range<language::Anchor>>,
},
UsePendingTools,
ToolFinished {
@@ -478,7 +542,8 @@ pub struct Context {
pending_ops: Vec<ContextOperation>,
operations: Vec<ContextOperation>,
buffer: Model<Buffer>,
- pending_slash_commands: Vec<PendingSlashCommand>,
+ parsed_slash_commands: Vec<ParsedSlashCommand>,
+ invoked_slash_commands: HashMap<SlashCommandId, InvokedSlashCommand>,
edits_since_last_parse: language::Subscription,
finished_slash_commands: HashSet<SlashCommandId>,
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
@@ -508,7 +573,7 @@ trait ContextAnnotation {
fn range(&self) -> &Range<language::Anchor>;
}
-impl ContextAnnotation for PendingSlashCommand {
+impl ContextAnnotation for ParsedSlashCommand {
fn range(&self) -> &Range<language::Anchor> {
&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<text::Anchor>,
+ 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>) {
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<language::Anchor>] {
@@ -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<text::Anchor>,
buffer: &BufferSnapshot,
- updated: &mut Vec<PendingSlashCommand>,
+ updated: &mut Vec<ParsedSlashCommand>,
removed: &mut Vec<Range<text::Anchor>>,
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<Self>,
- ) -> 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<language::Anchor>,
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<language::Anchor>,
cx: &AppContext,
) -> Range<usize> {
- 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<T: ContextAnnotation>(
@@ -1702,112 +1803,275 @@ impl Context {
pub fn insert_command_output(
&mut self,
- command_range: Range<language::Anchor>,
+ command_source_range: Range<language::Anchor>,
+ name: &str,
output: Task<SlashCommandResult>,
ensure_trailing_newline: bool,
- expand_result: bool,
cx: &mut ModelContext<Self>,
) {
+ 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<serde_json::Value>,
+ }
+
+ let mut pending_section_stack: Vec<PendingSection> = Vec::new();
+ let mut run_commands_in_ranges: Vec<Range<language::Anchor>> = Vec::new();
+ let mut last_role: Option<Role> = 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::<Vec<_>>();
- 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<language::Anchor>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<Self>,
+ ) -> 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<Self>) {
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<language::Anchor>,
}
+#[derive(Debug)]
+pub struct InvokedSlashCommand {
+ pub name: SharedString,
+ pub range: Range<language::Anchor>,
+ 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 {
@@ -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<Range<language::Anchor>>,
+ command_outputs: HashMap<SlashCommandId, Range<language::Anchor>>,
+ output_sections: HashSet<Range<language::Anchor>>,
+ }
+
+ 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<Buffer>,
- ranges: &HashSet<Range<language::Anchor>>,
+ ranges: &RefCell<ContextRanges>,
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::<Vec<_>>();
- 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::<String>();
+ 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,
);
});
@@ -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,
);
@@ -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(())
@@ -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
@@ -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<dyn Fn(&mut WindowContext)>, &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 { .. } => {}
}
}
@@ -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<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
cx: &mut ModelContext<Self>,
) {
- 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<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ type_id: TypeId,
+ cx: &mut ModelContext<Self>,
+ ) {
+ self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges, type_id))
}
- pub fn unfold<T: ToOffset>(
+ /// Removes any folds whose ranges intersect any of the given ranges.
+ pub fn unfold_intersecting<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>,
+ ) {
+ self.update_fold_map(cx, |fold_map| {
+ fold_map.unfold_intersecting(ranges, inclusive)
+ })
+ }
+
+ fn update_fold_map(
+ &mut self,
+ cx: &mut ModelContext<Self>,
+ callback: impl FnOnce(&mut FoldMapWriter) -> (FoldSnapshot, Vec<Edit<FoldOffset>>),
) {
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);
@@ -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<TypeId>,
+}
+
+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<T: ToOffset>(
+ /// Removes any folds with the given ranges.
+ pub(crate) fn remove_folds<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ type_id: TypeId,
+ ) -> (FoldSnapshot, Vec<FoldEdit>) {
+ 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<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
+ ) -> (FoldSnapshot, Vec<FoldEdit>) {
+ 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<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ should_unfold: impl Fn(&Fold) -> bool,
+ inclusive: bool,
) -> (FoldSnapshot, Vec<FoldEdit>) {
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<Transform>, summary: TextSummary) {
}
}
-fn intersecting_folds<'a, T>(
+fn intersecting_folds<'a>(
inlay_snapshot: &'a InlaySnapshot,
folds: &'a SumTree<Fold>,
- range: Range<T>,
+ range: Range<usize>,
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));
}
_ => {
@@ -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<Autoscroll>,
cx: &mut ViewContext<Editor>,
) {
- 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::<Vec<_>>(),
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::<Vec<_>>(),
+ &selections.iter().map(|s| s.range()).collect::<Vec<_>>(),
false,
true,
cx,
@@ -10986,7 +10990,7 @@ impl Editor {
})
.collect::<Vec<_>>();
- self.unfold_ranges(ranges, true, true, cx);
+ self.unfold_ranges(&ranges, true, true, cx);
}
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
@@ -11004,7 +11008,7 @@ impl Editor {
})
.collect::<Vec<_>>();
- 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<Self>) {
@@ -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<Self>) {
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<T: ToOffset + Clone>(
&mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
+ ranges: &[Range<T>],
inclusive: bool,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
- 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<T: ToOffset + Clone>(
+ &mut self,
+ ranges: &[Range<T>],
+ type_id: TypeId,
+ auto_scroll: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<T: ToOffset + Clone>(
+ &mut self,
+ ranges: &[Range<T>],
+ auto_scroll: bool,
+ cx: &mut ViewContext<Self>,
+ update: impl FnOnce(&mut DisplayMap, &mut ModelContext<DisplayMap>),
+ ) {
+ 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 {
@@ -1280,7 +1280,7 @@ impl SearchableItem for Editor {
matches: &[Range<Anchor>],
cx: &mut ViewContext<Self>,
) {
- 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>) {
- 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))
@@ -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;
}
@@ -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])
});