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