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