1use collections::HashMap;
2use gpui::{AnyElement, IntoElement};
3use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
4use std::{cmp::Ordering, ops::Range, sync::Arc};
5use sum_tree::{Bias, SeekTarget, SumTree};
6use text::Point;
7use ui::WindowContext;
8
9use crate::FoldPlaceholder;
10
11#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
12pub struct FlapId(usize);
13
14#[derive(Default)]
15pub struct FlapMap {
16 snapshot: FlapSnapshot,
17 next_id: FlapId,
18 id_to_range: HashMap<FlapId, Range<Anchor>>,
19}
20
21#[derive(Clone, Default)]
22pub struct FlapSnapshot {
23 flaps: SumTree<FlapItem>,
24}
25
26impl FlapSnapshot {
27 /// Returns the first Flap starting on the specified buffer row.
28 pub fn query_row<'a>(
29 &'a self,
30 row: MultiBufferRow,
31 snapshot: &'a MultiBufferSnapshot,
32 ) -> Option<&'a Flap> {
33 let start = snapshot.anchor_before(Point::new(row.0, 0));
34 let mut cursor = self.flaps.cursor::<ItemSummary>();
35 cursor.seek(&start, Bias::Left, snapshot);
36 while let Some(item) = cursor.item() {
37 match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
38 Ordering::Less => cursor.next(snapshot),
39 Ordering::Equal => {
40 if item.flap.range.start.is_valid(snapshot) {
41 return Some(&item.flap);
42 } else {
43 cursor.next(snapshot);
44 }
45 }
46 Ordering::Greater => break,
47 }
48 }
49 return None;
50 }
51
52 pub fn flap_items_with_offsets(
53 &self,
54 snapshot: &MultiBufferSnapshot,
55 ) -> Vec<(FlapId, Range<Point>)> {
56 let mut cursor = self.flaps.cursor::<ItemSummary>();
57 let mut results = Vec::new();
58
59 cursor.next(snapshot);
60 while let Some(item) = cursor.item() {
61 let start_point = item.flap.range.start.to_point(snapshot);
62 let end_point = item.flap.range.end.to_point(snapshot);
63 results.push((item.id, start_point..end_point));
64 cursor.next(snapshot);
65 }
66
67 results
68 }
69}
70
71type RenderToggleFn = Arc<
72 dyn Send
73 + Sync
74 + Fn(
75 MultiBufferRow,
76 bool,
77 Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
78 &mut WindowContext,
79 ) -> AnyElement,
80>;
81type RenderTrailerFn =
82 Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
83
84#[derive(Clone)]
85pub struct Flap {
86 pub range: Range<Anchor>,
87 pub placeholder: FoldPlaceholder,
88 pub render_toggle: RenderToggleFn,
89 pub render_trailer: RenderTrailerFn,
90}
91
92impl Flap {
93 pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
94 range: Range<Anchor>,
95 placeholder: FoldPlaceholder,
96 render_toggle: RenderToggle,
97 render_trailer: RenderTrailer,
98 ) -> Self
99 where
100 RenderToggle: 'static
101 + Send
102 + Sync
103 + Fn(
104 MultiBufferRow,
105 bool,
106 Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
107 &mut WindowContext,
108 ) -> ToggleElement
109 + 'static,
110 ToggleElement: IntoElement,
111 RenderTrailer: 'static
112 + Send
113 + Sync
114 + Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement
115 + 'static,
116 TrailerElement: IntoElement,
117 {
118 Flap {
119 range,
120 placeholder,
121 render_toggle: Arc::new(move |row, folded, toggle, cx| {
122 render_toggle(row, folded, toggle, cx).into_any_element()
123 }),
124 render_trailer: Arc::new(move |row, folded, cx| {
125 render_trailer(row, folded, cx).into_any_element()
126 }),
127 }
128 }
129}
130
131impl std::fmt::Debug for Flap {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("Flap").field("range", &self.range).finish()
134 }
135}
136
137#[derive(Clone, Debug)]
138struct FlapItem {
139 id: FlapId,
140 flap: Flap,
141}
142
143impl FlapMap {
144 pub fn snapshot(&self) -> FlapSnapshot {
145 self.snapshot.clone()
146 }
147
148 pub fn insert(
149 &mut self,
150 flaps: impl IntoIterator<Item = Flap>,
151 snapshot: &MultiBufferSnapshot,
152 ) -> Vec<FlapId> {
153 let mut new_ids = Vec::new();
154 self.snapshot.flaps = {
155 let mut new_flaps = SumTree::new();
156 let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
157 for flap in flaps {
158 new_flaps.append(cursor.slice(&flap.range, Bias::Left, snapshot), snapshot);
159
160 let id = self.next_id;
161 self.next_id.0 += 1;
162 self.id_to_range.insert(id, flap.range.clone());
163 new_flaps.push(FlapItem { flap, id }, snapshot);
164 new_ids.push(id);
165 }
166 new_flaps.append(cursor.suffix(snapshot), snapshot);
167 new_flaps
168 };
169 new_ids
170 }
171
172 pub fn remove(
173 &mut self,
174 ids: impl IntoIterator<Item = FlapId>,
175 snapshot: &MultiBufferSnapshot,
176 ) {
177 let mut removals = Vec::new();
178 for id in ids {
179 if let Some(range) = self.id_to_range.remove(&id) {
180 removals.push((id, range.clone()));
181 }
182 }
183 removals.sort_unstable_by(|(a_id, a_range), (b_id, b_range)| {
184 AnchorRangeExt::cmp(a_range, b_range, snapshot).then(b_id.cmp(&a_id))
185 });
186
187 self.snapshot.flaps = {
188 let mut new_flaps = SumTree::new();
189 let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
190
191 for (id, range) in removals {
192 new_flaps.append(cursor.slice(&range, Bias::Left, snapshot), snapshot);
193 while let Some(item) = cursor.item() {
194 cursor.next(snapshot);
195 if item.id == id {
196 break;
197 } else {
198 new_flaps.push(item.clone(), snapshot);
199 }
200 }
201 }
202
203 new_flaps.append(cursor.suffix(snapshot), snapshot);
204 new_flaps
205 };
206 }
207}
208
209#[derive(Debug, Clone)]
210pub struct ItemSummary {
211 range: Range<Anchor>,
212}
213
214impl Default for ItemSummary {
215 fn default() -> Self {
216 Self {
217 range: Anchor::min()..Anchor::min(),
218 }
219 }
220}
221
222impl sum_tree::Summary for ItemSummary {
223 type Context = MultiBufferSnapshot;
224
225 fn add_summary(&mut self, other: &Self, _snapshot: &MultiBufferSnapshot) {
226 self.range = other.range.clone();
227 }
228}
229
230impl sum_tree::Item for FlapItem {
231 type Summary = ItemSummary;
232
233 fn summary(&self) -> Self::Summary {
234 ItemSummary {
235 range: self.flap.range.clone(),
236 }
237 }
238}
239
240/// Implements `SeekTarget` for `Range<Anchor>` to enable seeking within a `SumTree` of `FlapItem`s.
241impl SeekTarget<'_, ItemSummary, ItemSummary> for Range<Anchor> {
242 fn cmp(&self, cursor_location: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
243 AnchorRangeExt::cmp(self, &cursor_location.range, snapshot)
244 }
245}
246
247impl SeekTarget<'_, ItemSummary, ItemSummary> for Anchor {
248 fn cmp(&self, other: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
249 self.cmp(&other.range.start, snapshot)
250 }
251}
252
253#[cfg(test)]
254mod test {
255 use super::*;
256 use gpui::{div, AppContext};
257 use multi_buffer::MultiBuffer;
258
259 #[gpui::test]
260 fn test_insert_and_remove_flaps(cx: &mut AppContext) {
261 let text = "line1\nline2\nline3\nline4\nline5";
262 let buffer = MultiBuffer::build_simple(text, cx);
263 let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
264 let mut flap_map = FlapMap::default();
265
266 // Insert flaps
267 let flaps = [
268 Flap::new(
269 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
270 FoldPlaceholder::test(),
271 |_row, _folded, _toggle, _cx| div(),
272 |_row, _folded, _cx| div(),
273 ),
274 Flap::new(
275 snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
276 FoldPlaceholder::test(),
277 |_row, _folded, _toggle, _cx| div(),
278 |_row, _folded, _cx| div(),
279 ),
280 ];
281 let flap_ids = flap_map.insert(flaps, &snapshot);
282 assert_eq!(flap_ids.len(), 2);
283
284 // Verify flaps are inserted
285 let flap_snapshot = flap_map.snapshot();
286 assert!(flap_snapshot
287 .query_row(MultiBufferRow(1), &snapshot)
288 .is_some());
289 assert!(flap_snapshot
290 .query_row(MultiBufferRow(3), &snapshot)
291 .is_some());
292
293 // Remove flaps
294 flap_map.remove(flap_ids, &snapshot);
295
296 // Verify flaps are removed
297 let flap_snapshot = flap_map.snapshot();
298 assert!(flap_snapshot
299 .query_row(MultiBufferRow(1), &snapshot)
300 .is_none());
301 assert!(flap_snapshot
302 .query_row(MultiBufferRow(3), &snapshot)
303 .is_none());
304 }
305}