flap_map.rs

  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}