Make slash command output streamable (#19632)

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>
This commit is contained in:
Marshall Bowers
2024-11-06 16:24:43 -08:00
committed by GitHub
co-authored by David Soria Parra Antonio Scandurra David Antonio Max Max Brunsfeld Will
parent f6fbf662b4
commit b129e18396
14 changed files with 1130 additions and 501 deletions
+28 -21
View File
@@ -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))
}
pub fn unfold<T: ToOffset>(
/// 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))
}
/// 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);
+62 -22
View File
@@ -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,9 +187,34 @@ 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();
@@ -183,21 +222,23 @@ impl<'a> FoldMapWriter<'a> {
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));
}
_ => {
+60 -32
View File
@@ -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);
}
cx.notify();
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
self.display_map.update(cx, update);
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
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 {
+2 -2
View File
@@ -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))