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 std::ops::Range;
16
17use ui::input::Granularity;
18
19use crate::doc::{Doc, Part};
20
21/// A caret: which block, which part of it, and how far into that part.
22///
23/// Byte offsets, like the marks — and like them, always on a character
24/// boundary.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
26pub struct Cursor {
27    pub block: usize,
28    pub part: Part,
29    pub offset: usize,
30}
31
32impl Cursor {
33    pub fn new(block: usize, part: Part, offset: usize) -> Self {
34        Self {
35            block,
36            part,
37            offset,
38        }
39    }
40
41    /// How much text this position's part holds, or `None` when the block has
42    /// no such part — an image and a rule have none at all.
43    pub fn len_in(self, doc: &Doc) -> Option<usize> {
44        doc.blocks
45            .get(self.block)?
46            .text_at(self.part)
47            .map(|text| text.text.len())
48    }
49
50    /// Pull the caret back onto a real position: a block that exists, a part
51    /// that block has, an offset inside it, and a character boundary.
52    pub fn clamp(self, doc: &Doc) -> Self {
53        if doc.blocks.is_empty() {
54            return Self::default();
55        }
56        let block = self.block.min(doc.blocks.len() - 1);
57        // The part can vanish under the caret — a table loses a row, a block
58        // changes kind — so fall back to the block's first, and to nothing at
59        // all for a block no caret can enter.
60        let part = match doc.blocks[block].text_at(self.part) {
61            Some(_) => self.part,
62            None => match doc.blocks[block].parts().first() {
63                Some(part) => *part,
64                None => return Self::new(block, Part::default(), 0),
65            },
66        };
67        let here = Self::new(block, part, self.offset);
68        let Some(text) = doc.blocks[block].text_at(part) else {
69            return here;
70        };
71        let mut offset = self.offset.min(text.text.len());
72        while offset > 0 && !text.text.is_char_boundary(offset) {
73            offset -= 1;
74        }
75        if let Some(mention) = text.mention_around(offset) {
76            offset = match offset - mention.start <= mention.end - offset {
77                true => mention.start,
78                false => mention.end,
79            };
80        }
81        Self { offset, ..here }
82    }
83
84    /// The first position at or after block `from` a caret can sit in.
85    fn next_editable(doc: &Doc, from: usize) -> Option<Self> {
86        (from..doc.blocks.len()).find_map(|ix| {
87            doc.blocks[ix]
88                .parts()
89                .first()
90                .map(|part| Self::new(ix, *part, 0))
91        })
92    }
93
94    /// The last position at or before block `from` a caret can sit in, at its
95    /// end — where stepping backwards into it lands.
96    fn previous_editable(doc: &Doc, from: usize) -> Option<Self> {
97        (0..=from.min(doc.blocks.len().saturating_sub(1)))
98            .rev()
99            .find_map(|ix| {
100                let part = *doc.blocks[ix].parts().last()?;
101                let at = Self::new(ix, part, 0);
102                Some(Self {
103                    offset: at.len_in(doc).unwrap_or(0),
104                    ..at
105                })
106            })
107    }
108
109    /// The neighbouring part within this block — the cells of a table are the
110    /// only case, and they are why this is not just "the next block".
111    fn step_part(self, doc: &Doc, by: isize) -> Option<Part> {
112        let parts = doc.blocks.get(self.block)?.parts();
113        let ix = parts.iter().position(|part| *part == self.part)?;
114        parts.get(ix.checked_add_signed(by)?).copied()
115    }
116
117    /// One character left, stepping into the part or block before it.
118    pub fn left(self, doc: &Doc) -> Self {
119        let here = self.clamp(doc);
120        if here.offset > 0
121            && let Some(text) = doc.blocks[here.block].text_at(here.part)
122        {
123            let mut offset = here.offset - 1;
124            while offset > 0 && !text.text.is_char_boundary(offset) {
125                offset -= 1;
126            }
127            let offset = text.mention_around(offset).map_or(offset, |m| m.start);
128            return Self { offset, ..here };
129        }
130        if let Some(part) = here.step_part(doc, -1) {
131            let at = Self { part, ..here };
132            return Self {
133                offset: at.len_in(doc).unwrap_or(0),
134                ..at
135            };
136        }
137        here.block
138            .checked_sub(1)
139            .and_then(|ix| Self::previous_editable(doc, ix))
140            .unwrap_or(here)
141    }
142
143    /// One character right, stepping into the part or block after it.
144    pub fn right(self, doc: &Doc) -> Self {
145        let here = self.clamp(doc);
146        let len = here.len_in(doc).unwrap_or(0);
147        if here.offset < len
148            && let Some(text) = doc.blocks[here.block].text_at(here.part)
149        {
150            let mut offset = here.offset + 1;
151            while offset < len && !text.text.is_char_boundary(offset) {
152                offset += 1;
153            }
154            let offset = text.mention_around(offset).map_or(offset, |m| m.end);
155            return Self { offset, ..here };
156        }
157        if let Some(part) = here.step_part(doc, 1) {
158            return Self {
159                part,
160                offset: 0,
161                ..here
162            };
163        }
164        Self::next_editable(doc, here.block + 1).unwrap_or(here)
165    }
166
167    /// The row above, keeping the offset where it fits.
168    ///
169    /// Within a table that is the cell above in the same column; everywhere
170    /// else it is the block above. A wrapped paragraph's visual lines are not
171    /// reachable from here — that needs the paint's layout, and the editor
172    /// resolves it there.
173    pub fn up(self, doc: &Doc) -> Self {
174        let here = self.clamp(doc);
175        if let Part::Cell { row, column } = here.part
176            && row > 0
177        {
178            return Self {
179                part: Part::Cell {
180                    row: row - 1,
181                    column,
182                },
183                ..here
184            }
185            .clamp(doc);
186        }
187        match here
188            .block
189            .checked_sub(1)
190            .and_then(|ix| Self::previous_editable(doc, ix))
191        {
192            Some(above) => Self {
193                offset: here.offset,
194                ..above
195            }
196            .clamp(doc),
197            None => Self { offset: 0, ..here },
198        }
199    }
200
201    /// The row below, keeping the offset where it fits.
202    pub fn down(self, doc: &Doc) -> Self {
203        let here = self.clamp(doc);
204        if let Part::Cell { row, column } = here.part {
205            let below = Self {
206                part: Part::Cell {
207                    row: row + 1,
208                    column,
209                },
210                ..here
211            };
212            if below.len_in(doc).is_some() {
213                return below.clamp(doc);
214            }
215        }
216        match Self::next_editable(doc, here.block + 1) {
217            Some(below) => Self {
218                offset: here.offset,
219                ..below
220            }
221            .clamp(doc),
222            None => Self {
223                offset: here.len_in(doc).unwrap_or(0),
224                ..here
225            },
226        }
227    }
228
229    pub fn home(self) -> Self {
230        Self { offset: 0, ..self }
231    }
232
233    pub fn end(self, doc: &Doc) -> Self {
234        let here = self.clamp(doc);
235        Self {
236            offset: here.len_in(doc).unwrap_or(0),
237            ..here
238        }
239    }
240
241    /// The start of the word at or before the caret — alt-left.
242    ///
243    /// Whitespace first, then the run of word characters, which is the rule
244    /// every platform's word-left follows and the one `ui::TextField` uses.
245    pub fn word_left(self, doc: &Doc) -> Self {
246        let here = self.clamp(doc);
247        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
248            return here.left(doc);
249        };
250        if here.offset == 0 {
251            return here.left(doc);
252        }
253        let head = &text.text[..here.offset];
254        let trimmed = head.trim_end_matches(|c: char| !c.is_alphanumeric());
255        let offset = trimmed.trim_end_matches(char::is_alphanumeric).len();
256        let offset = text.mention_around(offset).map_or(offset, |m| m.start);
257        Self { offset, ..here }
258    }
259
260    /// The end of the word at or after the caret — alt-right.
261    pub fn word_right(self, doc: &Doc) -> Self {
262        let here = self.clamp(doc);
263        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
264            return here.right(doc);
265        };
266        if here.offset >= text.text.len() {
267            return here.right(doc);
268        }
269        let tail = &text.text[here.offset..];
270        let skipped = tail.len()
271            - tail
272                .trim_start_matches(|c: char| !c.is_alphanumeric())
273                .len();
274        let rest = &tail[skipped..];
275        let word = rest.len() - rest.trim_start_matches(char::is_alphanumeric).len();
276        let offset = here.offset + skipped + word;
277        Self {
278            offset: text.mention_around(offset).map_or(offset, |m| m.end),
279            ..here
280        }
281    }
282
283    /// The span of `unit` holding the caret, inside its own part. A word is
284    /// the run of alphanumerics the caret touches, or of anything else when it
285    /// touches none; a line runs between the part's newlines. A mention is never split.
286    pub fn span(self, unit: Granularity, doc: &Doc) -> Range<Self> {
287        let here = self.clamp(doc);
288        let Some(text) = doc
289            .blocks
290            .get(here.block)
291            .and_then(|b| b.text_at(here.part))
292        else {
293            return here..here;
294        };
295        let (start, end) = match unit {
296            Granularity::Char => return here..here,
297            Granularity::Line => (
298                ui::input::line_start(&text.text, here.offset),
299                ui::input::line_end(&text.text, here.offset),
300            ),
301            Granularity::Word => {
302                let (head, tail) = text.text.split_at(here.offset);
303                let word = head.chars().next_back().is_some_and(char::is_alphanumeric)
304                    || tail.chars().next().is_some_and(char::is_alphanumeric);
305                let same = |c: &char| c.is_alphanumeric() == word;
306                let back: usize = head
307                    .chars()
308                    .rev()
309                    .take_while(same)
310                    .map(char::len_utf8)
311                    .sum();
312                let ahead: usize = tail.chars().take_while(same).map(char::len_utf8).sum();
313                (here.offset - back, here.offset + ahead)
314            }
315        };
316        let start = text.mention_around(start).map_or(start, |m| m.start);
317        let end = text.mention_around(end).map_or(end, |m| m.end);
318        Self {
319            offset: start,
320            ..here
321        }..Self {
322            offset: end,
323            ..here
324        }
325    }
326}
327
328/// Which painted row the head of a [`Selection`] sits on when its offset is a
329/// soft wrap, where one offset both ends a row and starts the next. Anywhere
330/// else it changes nothing.
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
332pub enum Affinity {
333    /// The start of the row after the wrap.
334    #[default]
335    Downstream,
336    /// The end of the row before the wrap.
337    Upstream,
338}
339
340/// A range in the document: where the selection started, and where it is being
341/// dragged to.
342///
343/// The head is the end that moves — shift+arrow and a mouse drag both leave the
344/// anchor where it was. Collapsed (`anchor == head`) is an ordinary caret, so
345/// there is one position type in the editor rather than two.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
347pub struct Selection {
348    pub anchor: Cursor,
349    pub head: Cursor,
350    /// The head's row at a soft wrap.
351    pub affinity: Affinity,
352}
353
354impl From<Cursor> for Selection {
355    fn from(cursor: Cursor) -> Self {
356        Self::at(cursor)
357    }
358}
359
360impl Selection {
361    /// A collapsed selection — a plain caret.
362    pub fn at(cursor: Cursor) -> Self {
363        Self::new(cursor, cursor)
364    }
365
366    pub fn new(anchor: Cursor, head: Cursor) -> Self {
367        Self {
368            anchor,
369            head,
370            affinity: Affinity::Downstream,
371        }
372    }
373
374    pub fn with_affinity(self, affinity: Affinity) -> Self {
375        Self { affinity, ..self }
376    }
377
378    pub fn is_collapsed(&self) -> bool {
379        self.anchor == self.head
380    }
381
382    /// The two ends in document order.
383    pub fn ordered(&self) -> (Cursor, Cursor) {
384        (self.anchor.min(self.head), self.anchor.max(self.head))
385    }
386
387    /// Move the head, leaving the anchor — every shift+motion and every drag.
388    pub fn extend_to(self, head: Cursor) -> Self {
389        Self::new(self.anchor, head)
390    }
391
392    pub fn clamp(self, doc: &Doc) -> Self {
393        Self {
394            anchor: self.anchor.clamp(doc),
395            head: self.head.clamp(doc),
396            ..self
397        }
398    }
399
400    /// The whole document.
401    pub fn all(doc: &Doc) -> Self {
402        let start = Cursor::next_editable(doc, 0).unwrap_or_default();
403        let end =
404            Cursor::previous_editable(doc, doc.blocks.len().saturating_sub(1)).unwrap_or(start);
405        Self::new(start, end)
406    }
407}