Skip to main content

markdown/
select.rs

1//! Positions and ranges in a [`Doc`].
2//!
3//! A [`Cursor`] is `(block, part, offset)` — which block, which of its texts,
4//! and how far into it. The part is a *coordinate*, not a path: a block has one
5//! kind of part and never a mix, so the model stays flat while a caret can still
6//! reach inside a code block or a table cell.
7//!
8//! Because the three fields order lexicographically, a [`Selection`] is just two
9//! cursors and `min`/`max` decides which end is which. That is what lets every
10//! delete, every paste and every keystroke be the same operation —
11//! [`Doc::replace`] over a range — rather than a special case per key.
12//!
13//! This half is pure, so the motion is testable without a window.
14
15use crate::doc::{Doc, Part};
16
17/// A caret: which block, which part of it, and how far into that part.
18///
19/// Byte offsets, like the marks — and like them, always on a character
20/// boundary.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
22pub struct Cursor {
23    pub block: usize,
24    pub part: Part,
25    pub offset: usize,
26}
27
28impl Cursor {
29    pub fn new(block: usize, part: Part, offset: usize) -> Self {
30        Self {
31            block,
32            part,
33            offset,
34        }
35    }
36
37    /// How much text this position's part holds, or `None` when the block has
38    /// no such part — an image and a rule have none at all.
39    pub fn len_in(self, doc: &Doc) -> Option<usize> {
40        doc.blocks
41            .get(self.block)?
42            .text_at(self.part)
43            .map(|text| text.text.len())
44    }
45
46    /// Pull the caret back onto a real position: a block that exists, a part
47    /// that block has, an offset inside it, and a character boundary.
48    pub fn clamp(self, doc: &Doc) -> Self {
49        if doc.blocks.is_empty() {
50            return Self::default();
51        }
52        let block = self.block.min(doc.blocks.len() - 1);
53        // The part can vanish under the caret — a table loses a row, a block
54        // changes kind — so fall back to the block's first, and to nothing at
55        // all for a block no caret can enter.
56        let part = match doc.blocks[block].text_at(self.part) {
57            Some(_) => self.part,
58            None => match doc.blocks[block].parts().first() {
59                Some(part) => *part,
60                None => return Self::new(block, Part::default(), 0),
61            },
62        };
63        let here = Self::new(block, part, self.offset);
64        let Some(text) = doc.blocks[block].text_at(part) else {
65            return here;
66        };
67        let mut offset = self.offset.min(text.text.len());
68        while offset > 0 && !text.text.is_char_boundary(offset) {
69            offset -= 1;
70        }
71        if let Some(mention) = text.mention_around(offset) {
72            offset = match offset - mention.start <= mention.end - offset {
73                true => mention.start,
74                false => mention.end,
75            };
76        }
77        Self { offset, ..here }
78    }
79
80    /// The first position at or after block `from` a caret can sit in.
81    fn next_editable(doc: &Doc, from: usize) -> Option<Self> {
82        (from..doc.blocks.len()).find_map(|ix| {
83            doc.blocks[ix]
84                .parts()
85                .first()
86                .map(|part| Self::new(ix, *part, 0))
87        })
88    }
89
90    /// The last position at or before block `from` a caret can sit in, at its
91    /// end — where stepping backwards into it lands.
92    fn previous_editable(doc: &Doc, from: usize) -> Option<Self> {
93        (0..=from.min(doc.blocks.len().saturating_sub(1)))
94            .rev()
95            .find_map(|ix| {
96                let part = *doc.blocks[ix].parts().last()?;
97                let at = Self::new(ix, part, 0);
98                Some(Self {
99                    offset: at.len_in(doc).unwrap_or(0),
100                    ..at
101                })
102            })
103    }
104
105    /// The neighbouring part within this block — the cells of a table are the
106    /// only case, and they are why this is not just "the next block".
107    fn step_part(self, doc: &Doc, by: isize) -> Option<Part> {
108        let parts = doc.blocks.get(self.block)?.parts();
109        let ix = parts.iter().position(|part| *part == self.part)?;
110        parts.get(ix.checked_add_signed(by)?).copied()
111    }
112
113    /// One character left, stepping into the part or block before it.
114    pub fn left(self, doc: &Doc) -> Self {
115        let here = self.clamp(doc);
116        if here.offset > 0
117            && let Some(text) = doc.blocks[here.block].text_at(here.part)
118        {
119            let mut offset = here.offset - 1;
120            while offset > 0 && !text.text.is_char_boundary(offset) {
121                offset -= 1;
122            }
123            let offset = text.mention_around(offset).map_or(offset, |m| m.start);
124            return Self { offset, ..here };
125        }
126        if let Some(part) = here.step_part(doc, -1) {
127            let at = Self { part, ..here };
128            return Self {
129                offset: at.len_in(doc).unwrap_or(0),
130                ..at
131            };
132        }
133        here.block
134            .checked_sub(1)
135            .and_then(|ix| Self::previous_editable(doc, ix))
136            .unwrap_or(here)
137    }
138
139    /// One character right, stepping into the part or block after it.
140    pub fn right(self, doc: &Doc) -> Self {
141        let here = self.clamp(doc);
142        let len = here.len_in(doc).unwrap_or(0);
143        if here.offset < len
144            && let Some(text) = doc.blocks[here.block].text_at(here.part)
145        {
146            let mut offset = here.offset + 1;
147            while offset < len && !text.text.is_char_boundary(offset) {
148                offset += 1;
149            }
150            let offset = text.mention_around(offset).map_or(offset, |m| m.end);
151            return Self { offset, ..here };
152        }
153        if let Some(part) = here.step_part(doc, 1) {
154            return Self {
155                part,
156                offset: 0,
157                ..here
158            };
159        }
160        Self::next_editable(doc, here.block + 1).unwrap_or(here)
161    }
162
163    /// The row above, keeping the offset where it fits.
164    ///
165    /// Within a table that is the cell above in the same column; everywhere
166    /// else it is the block above. A wrapped paragraph's visual lines are not
167    /// reachable from here — that needs the paint's layout, and the editor
168    /// resolves it there.
169    pub fn up(self, doc: &Doc) -> Self {
170        let here = self.clamp(doc);
171        if let Part::Cell { row, column } = here.part
172            && row > 0
173        {
174            return Self {
175                part: Part::Cell {
176                    row: row - 1,
177                    column,
178                },
179                ..here
180            }
181            .clamp(doc);
182        }
183        match here
184            .block
185            .checked_sub(1)
186            .and_then(|ix| Self::previous_editable(doc, ix))
187        {
188            Some(above) => Self {
189                offset: here.offset,
190                ..above
191            }
192            .clamp(doc),
193            None => Self { offset: 0, ..here },
194        }
195    }
196
197    /// The row below, keeping the offset where it fits.
198    pub fn down(self, doc: &Doc) -> Self {
199        let here = self.clamp(doc);
200        if let Part::Cell { row, column } = here.part {
201            let below = Self {
202                part: Part::Cell {
203                    row: row + 1,
204                    column,
205                },
206                ..here
207            };
208            if below.len_in(doc).is_some() {
209                return below.clamp(doc);
210            }
211        }
212        match Self::next_editable(doc, here.block + 1) {
213            Some(below) => Self {
214                offset: here.offset,
215                ..below
216            }
217            .clamp(doc),
218            None => Self {
219                offset: here.len_in(doc).unwrap_or(0),
220                ..here
221            },
222        }
223    }
224
225    pub fn home(self) -> Self {
226        Self { offset: 0, ..self }
227    }
228
229    pub fn end(self, doc: &Doc) -> Self {
230        let here = self.clamp(doc);
231        Self {
232            offset: here.len_in(doc).unwrap_or(0),
233            ..here
234        }
235    }
236
237    /// The start of the word at or before the caret — alt-left.
238    ///
239    /// Whitespace first, then the run of word characters, which is the rule
240    /// every platform's word-left follows and the one `ui::TextField` uses.
241    pub fn word_left(self, doc: &Doc) -> Self {
242        let here = self.clamp(doc);
243        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
244            return here.left(doc);
245        };
246        if here.offset == 0 {
247            return here.left(doc);
248        }
249        let head = &text.text[..here.offset];
250        let trimmed = head.trim_end_matches(|c: char| !c.is_alphanumeric());
251        let offset = trimmed.trim_end_matches(char::is_alphanumeric).len();
252        let offset = text.mention_around(offset).map_or(offset, |m| m.start);
253        Self { offset, ..here }
254    }
255
256    /// The end of the word at or after the caret — alt-right.
257    pub fn word_right(self, doc: &Doc) -> Self {
258        let here = self.clamp(doc);
259        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
260            return here.right(doc);
261        };
262        if here.offset >= text.text.len() {
263            return here.right(doc);
264        }
265        let tail = &text.text[here.offset..];
266        let skipped = tail.len()
267            - tail
268                .trim_start_matches(|c: char| !c.is_alphanumeric())
269                .len();
270        let rest = &tail[skipped..];
271        let word = rest.len() - rest.trim_start_matches(char::is_alphanumeric).len();
272        let offset = here.offset + skipped + word;
273        Self {
274            offset: text.mention_around(offset).map_or(offset, |m| m.end),
275            ..here
276        }
277    }
278}
279
280/// A range in the document: where the selection started, and where it is being
281/// dragged to.
282///
283/// The head is the end that moves — shift+arrow and a mouse drag both leave the
284/// anchor where it was. Collapsed (`anchor == head`) is an ordinary caret, so
285/// there is one position type in the editor rather than two.
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
287pub struct Selection {
288    pub anchor: Cursor,
289    pub head: Cursor,
290}
291
292impl From<Cursor> for Selection {
293    fn from(cursor: Cursor) -> Self {
294        Self::at(cursor)
295    }
296}
297
298impl Selection {
299    /// A collapsed selection — a plain caret.
300    pub fn at(cursor: Cursor) -> Self {
301        Self {
302            anchor: cursor,
303            head: cursor,
304        }
305    }
306
307    pub fn new(anchor: Cursor, head: Cursor) -> Self {
308        Self { anchor, head }
309    }
310
311    pub fn is_collapsed(&self) -> bool {
312        self.anchor == self.head
313    }
314
315    /// The two ends in document order.
316    pub fn ordered(&self) -> (Cursor, Cursor) {
317        (self.anchor.min(self.head), self.anchor.max(self.head))
318    }
319
320    /// Move the head, leaving the anchor — every shift+motion and every drag.
321    pub fn extend_to(self, head: Cursor) -> Self {
322        Self { head, ..self }
323    }
324
325    pub fn clamp(self, doc: &Doc) -> Self {
326        Self {
327            anchor: self.anchor.clamp(doc),
328            head: self.head.clamp(doc),
329        }
330    }
331
332    /// The whole document.
333    pub fn all(doc: &Doc) -> Self {
334        let start = Cursor::next_editable(doc, 0).unwrap_or_default();
335        let end =
336            Cursor::previous_editable(doc, doc.blocks.len().saturating_sub(1)).unwrap_or(start);
337        Self::new(start, end)
338    }
339}