Skip to main content

editor/
anchor.rs

1//! Positions that outlive the edits under them.
2//!
3//! A comment or a highlight is anchored to a range of text, and that range has
4//! to move when the text around it does. What it *says* — a thread's replies,
5//! a highlight's note, who made either — is the app's, the way a link preview
6//! is. What is here is the range, its wash, and what keeps it over the same
7//! words.
8//!
9//! The anchors live in the editor rather than in the app because
10//! [`crate::History`] restores whole-document snapshots: an undo has no delta to
11//! map an anchor through, so the store has to sit where the history can carry
12//! it back.
13//!
14//! Mapping follows the **left-sticky** rule the marks already follow
15//! (`markdown::Text::insert`): text typed at the end of a range joins it, text
16//! typed at the start does not. Both ends take the same arithmetic, so there is
17//! no bias to pick per end and no second rule to keep in step with the first.
18
19use std::ops::Range;
20
21use markdown::{Annotation, Cursor, Part, Selection, Splice};
22
23/// The app's key for a comment or a highlight. Opaque — nothing here looks
24/// inside one.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub struct AnchorId(pub u64);
27
28/// Each anchor's range in byte offsets into the markdown, `None` where the
29/// markdown has no place for an end.
30pub type SourceRanges = Vec<(AnchorId, Option<Range<usize>>)>;
31
32/// A range in the document, and which wash it paints.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Anchor {
35    pub id: AnchorId,
36    pub range: Selection,
37    pub state: Annotation,
38}
39
40impl Anchor {
41    pub fn new(id: AnchorId, range: Selection) -> Self {
42        Self {
43            id,
44            range,
45            state: Annotation::default(),
46        }
47    }
48
49    /// Whether the words this pointed at are gone.
50    ///
51    /// Detached rather than dropped: whether that reads as "outdated" or as
52    /// "resolved" is the app's call, and deleting it here would take the choice
53    /// away.
54    pub fn detached(&self) -> bool {
55        self.range.is_collapsed()
56    }
57
58    pub(crate) fn map(&mut self, delta: &Delta) {
59        let (start, end) = self.range.ordered();
60        self.range = match (delta.cursor(start), delta.cursor(end)) {
61            (Some(start), Some(end)) => Selection::new(start.min(end), start.max(end)),
62            (surviving, other) => Selection::at(surviving.or(other).unwrap_or_default()),
63        };
64    }
65}
66
67/// What one step of a mutation did to the positions in a document.
68///
69/// Every path through [`crate::Editor::edit`] hands back a list of these, empty
70/// for an edit that moved nothing — so a block operation added later does not
71/// compile until it says what it did. Silent is the one thing it must not be:
72/// an anchor mapped through the wrong shift points at the wrong words and
73/// nothing complains.
74pub(crate) enum Delta {
75    CellsMoved {
76        block: usize,
77        from: crate::editor::Line,
78        to: usize,
79    },
80    /// Text went in, out, or both.
81    Spliced(Splice),
82    /// A run of blocks left `at`, and arrived at `to` unless it went away.
83    Moved { at: Range<usize>, to: Option<usize> },
84    /// Blocks appeared at `at`, pushing everything from there down.
85    Opened { at: usize, count: usize },
86    /// A row or a column appeared in table `block`, pushing the cells from
87    /// there on.
88    Cells {
89        block: usize,
90        row: Option<usize>,
91        column: Option<usize>,
92    },
93    /// A row or a column left table `block`, taking what was in it and
94    /// pulling the cells after it back.
95    CellsRemoved {
96        block: usize,
97        row: Option<usize>,
98        column: Option<usize>,
99    },
100}
101
102impl Delta {
103    /// Where `at` ends up, and `None` when the block under it went away.
104    pub(crate) fn cursor(&self, at: Cursor) -> Option<Cursor> {
105        match self {
106            Self::CellsMoved { block, from, to } => {
107                let mut at = at;
108                if at.block == *block
109                    && let Part::Cell { row, column } = &mut at.part
110                {
111                    let (index, from) = match from {
112                        crate::editor::Line::Row(from) => (row, *from),
113                        crate::editor::Line::Column(from) => (column, *from),
114                    };
115                    *index = if *index == from {
116                        *to
117                    } else if from < *index && *index <= *to {
118                        *index - 1
119                    } else if *to <= *index && *index < from {
120                        *index + 1
121                    } else {
122                        *index
123                    };
124                }
125                Some(at)
126            }
127            Self::Spliced(splice) => {
128                let (start, end) = splice.removed.ordered();
129                if at < start {
130                    return Some(at);
131                }
132                // Inside what went, the seam is the only place left to be.
133                if at <= end {
134                    return Some(splice.caret);
135                }
136                // Past it in the same text, the seam moved by what replaced it;
137                // further down the document, only the block count moved.
138                if at.block == end.block && at.part == end.part {
139                    return Some(Cursor {
140                        offset: splice.caret.offset + (at.offset - end.offset),
141                        ..splice.caret
142                    });
143                }
144                Some(Cursor {
145                    block: at.block.checked_add_signed(splice.blocks)?,
146                    ..at
147                })
148            }
149            Self::Moved { at: span, to } => {
150                if span.contains(&at.block) {
151                    return Some(Cursor {
152                        block: (*to)? + (at.block - span.start),
153                        ..at
154                    });
155                }
156                // Drained before it was put back, and `to` is already counted
157                // against the hole that left — so this is too.
158                let pulled = if at.block >= span.end {
159                    at.block - span.len()
160                } else {
161                    at.block
162                };
163                let block = match to {
164                    Some(to) if pulled >= *to => pulled + span.len(),
165                    _ => pulled,
166                };
167                Some(Cursor { block, ..at })
168            }
169            Self::Cells { block, row, column } => Some(match at.part {
170                Part::Cell { row: r, column: c } if at.block == *block => {
171                    let past = |line: usize, opened: Option<usize>| {
172                        line + usize::from(opened.is_some_and(|opened| line >= opened))
173                    };
174                    Cursor {
175                        part: Part::Cell {
176                            row: past(r, *row),
177                            column: past(c, *column),
178                        },
179                        ..at
180                    }
181                }
182                _ => at,
183            }),
184            Self::CellsRemoved { block, row, column } => match at.part {
185                Part::Cell { row: r, column: c } if at.block == *block => {
186                    if Some(r) == *row || Some(c) == *column {
187                        return None;
188                    }
189                    let past = |line: usize, removed: Option<usize>| {
190                        line - usize::from(removed.is_some_and(|removed| line > removed))
191                    };
192                    Some(Cursor {
193                        part: Part::Cell {
194                            row: past(r, *row),
195                            column: past(c, *column),
196                        },
197                        ..at
198                    })
199                }
200                _ => Some(at),
201            },
202            Self::Opened { at: opened, count } => Some(Cursor {
203                block: if at.block >= *opened {
204                    at.block + count
205                } else {
206                    at.block
207                },
208                ..at
209            }),
210        }
211    }
212}