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        Self { offset, ..here }
72    }
73
74    /// The first position at or after block `from` a caret can sit in.
75    fn next_editable(doc: &Doc, from: usize) -> Option<Self> {
76        (from..doc.blocks.len()).find_map(|ix| {
77            doc.blocks[ix]
78                .parts()
79                .first()
80                .map(|part| Self::new(ix, *part, 0))
81        })
82    }
83
84    /// The last position at or before block `from` a caret can sit in, at its
85    /// end — where stepping backwards into it lands.
86    fn previous_editable(doc: &Doc, from: usize) -> Option<Self> {
87        (0..=from.min(doc.blocks.len().saturating_sub(1)))
88            .rev()
89            .find_map(|ix| {
90                let part = *doc.blocks[ix].parts().last()?;
91                let at = Self::new(ix, part, 0);
92                Some(Self {
93                    offset: at.len_in(doc).unwrap_or(0),
94                    ..at
95                })
96            })
97    }
98
99    /// The neighbouring part within this block — the cells of a table are the
100    /// only case, and they are why this is not just "the next block".
101    fn step_part(self, doc: &Doc, by: isize) -> Option<Part> {
102        let parts = doc.blocks.get(self.block)?.parts();
103        let ix = parts.iter().position(|part| *part == self.part)?;
104        parts.get(ix.checked_add_signed(by)?).copied()
105    }
106
107    /// One character left, stepping into the part or block before it.
108    pub fn left(self, doc: &Doc) -> Self {
109        let here = self.clamp(doc);
110        if here.offset > 0
111            && let Some(text) = doc.blocks[here.block].text_at(here.part)
112        {
113            let mut offset = here.offset - 1;
114            while offset > 0 && !text.text.is_char_boundary(offset) {
115                offset -= 1;
116            }
117            return Self { offset, ..here };
118        }
119        if let Some(part) = here.step_part(doc, -1) {
120            let at = Self { part, ..here };
121            return Self {
122                offset: at.len_in(doc).unwrap_or(0),
123                ..at
124            };
125        }
126        here.block
127            .checked_sub(1)
128            .and_then(|ix| Self::previous_editable(doc, ix))
129            .unwrap_or(here)
130    }
131
132    /// One character right, stepping into the part or block after it.
133    pub fn right(self, doc: &Doc) -> Self {
134        let here = self.clamp(doc);
135        let len = here.len_in(doc).unwrap_or(0);
136        if here.offset < len
137            && let Some(text) = doc.blocks[here.block].text_at(here.part)
138        {
139            let mut offset = here.offset + 1;
140            while offset < len && !text.text.is_char_boundary(offset) {
141                offset += 1;
142            }
143            return Self { offset, ..here };
144        }
145        if let Some(part) = here.step_part(doc, 1) {
146            return Self {
147                part,
148                offset: 0,
149                ..here
150            };
151        }
152        Self::next_editable(doc, here.block + 1).unwrap_or(here)
153    }
154
155    /// The row above, keeping the offset where it fits.
156    ///
157    /// Within a table that is the cell above in the same column; everywhere
158    /// else it is the block above. A wrapped paragraph's visual lines are not
159    /// reachable from here — that needs the paint's layout, and the editor
160    /// resolves it there.
161    pub fn up(self, doc: &Doc) -> Self {
162        let here = self.clamp(doc);
163        if let Part::Cell { row, column } = here.part
164            && row > 0
165        {
166            return Self {
167                part: Part::Cell {
168                    row: row - 1,
169                    column,
170                },
171                ..here
172            }
173            .clamp(doc);
174        }
175        match here
176            .block
177            .checked_sub(1)
178            .and_then(|ix| Self::previous_editable(doc, ix))
179        {
180            Some(above) => Self {
181                offset: here.offset,
182                ..above
183            }
184            .clamp(doc),
185            None => Self { offset: 0, ..here },
186        }
187    }
188
189    /// The row below, keeping the offset where it fits.
190    pub fn down(self, doc: &Doc) -> Self {
191        let here = self.clamp(doc);
192        if let Part::Cell { row, column } = here.part {
193            let below = Self {
194                part: Part::Cell {
195                    row: row + 1,
196                    column,
197                },
198                ..here
199            };
200            if below.len_in(doc).is_some() {
201                return below.clamp(doc);
202            }
203        }
204        match Self::next_editable(doc, here.block + 1) {
205            Some(below) => Self {
206                offset: here.offset,
207                ..below
208            }
209            .clamp(doc),
210            None => Self {
211                offset: here.len_in(doc).unwrap_or(0),
212                ..here
213            },
214        }
215    }
216
217    pub fn home(self) -> Self {
218        Self { offset: 0, ..self }
219    }
220
221    pub fn end(self, doc: &Doc) -> Self {
222        let here = self.clamp(doc);
223        Self {
224            offset: here.len_in(doc).unwrap_or(0),
225            ..here
226        }
227    }
228
229    /// The start of the word at or before the caret — alt-left.
230    ///
231    /// Whitespace first, then the run of word characters, which is the rule
232    /// every platform's word-left follows and the one `ui::TextField` uses.
233    pub fn word_left(self, doc: &Doc) -> Self {
234        let here = self.clamp(doc);
235        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
236            return here.left(doc);
237        };
238        if here.offset == 0 {
239            return here.left(doc);
240        }
241        let head = &text.text[..here.offset];
242        let trimmed = head.trim_end_matches(|c: char| !c.is_alphanumeric());
243        let offset = trimmed.trim_end_matches(char::is_alphanumeric).len();
244        Self { offset, ..here }
245    }
246
247    /// The end of the word at or after the caret — alt-right.
248    pub fn word_right(self, doc: &Doc) -> Self {
249        let here = self.clamp(doc);
250        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
251            return here.right(doc);
252        };
253        if here.offset >= text.text.len() {
254            return here.right(doc);
255        }
256        let tail = &text.text[here.offset..];
257        let skipped = tail.len()
258            - tail
259                .trim_start_matches(|c: char| !c.is_alphanumeric())
260                .len();
261        let rest = &tail[skipped..];
262        let word = rest.len() - rest.trim_start_matches(char::is_alphanumeric).len();
263        Self {
264            offset: here.offset + skipped + word,
265            ..here
266        }
267    }
268}
269
270/// A range in the document: where the selection started, and where it is being
271/// dragged to.
272///
273/// The head is the end that moves — shift+arrow and a mouse drag both leave the
274/// anchor where it was. Collapsed (`anchor == head`) is an ordinary caret, so
275/// there is one position type in the editor rather than two.
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
277pub struct Selection {
278    pub anchor: Cursor,
279    pub head: Cursor,
280}
281
282impl Selection {
283    /// A collapsed selection — a plain caret.
284    pub fn at(cursor: Cursor) -> Self {
285        Self {
286            anchor: cursor,
287            head: cursor,
288        }
289    }
290
291    pub fn new(anchor: Cursor, head: Cursor) -> Self {
292        Self { anchor, head }
293    }
294
295    pub fn is_collapsed(&self) -> bool {
296        self.anchor == self.head
297    }
298
299    /// The two ends in document order.
300    pub fn ordered(&self) -> (Cursor, Cursor) {
301        (self.anchor.min(self.head), self.anchor.max(self.head))
302    }
303
304    /// Move the head, leaving the anchor — every shift+motion and every drag.
305    pub fn extend_to(self, head: Cursor) -> Self {
306        Self { head, ..self }
307    }
308
309    pub fn clamp(self, doc: &Doc) -> Self {
310        Self {
311            anchor: self.anchor.clamp(doc),
312            head: self.head.clamp(doc),
313        }
314    }
315
316    /// The whole document.
317    pub fn all(doc: &Doc) -> Self {
318        let start = Cursor::next_editable(doc, 0).unwrap_or_default();
319        let end =
320            Cursor::previous_editable(doc, doc.blocks.len().saturating_sub(1)).unwrap_or(start);
321        Self::new(start, end)
322    }
323}