bezel-markdown 0.2.13

A Notion-style block document model for gpui — markdown in, markdown out
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Positions and ranges in a [`Doc`].
//!
//! A [`Cursor`] is `(block, part, offset)` — which block, which of its texts,
//! and how far into it. The part is a *coordinate*, not a path: a block has one
//! kind of part and never a mix, so the model stays flat while a caret can still
//! reach inside a code block or a table cell.
//!
//! Because the three fields order lexicographically, a [`Selection`] is just two
//! cursors and `min`/`max` decides which end is which. That is what lets every
//! delete, every paste and every keystroke be the same operation —
//! [`Doc::replace`] over a range — rather than a special case per key.
//!
//! This half is pure, so the motion is testable without a window.

use std::ops::Range;

use ui::input::Granularity;

use crate::doc::{Doc, Part};

/// A caret: which block, which part of it, and how far into that part.
///
/// Byte offsets, like the marks — and like them, always on a character
/// boundary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Cursor {
    pub block: usize,
    pub part: Part,
    pub offset: usize,
}

impl Cursor {
    pub fn new(block: usize, part: Part, offset: usize) -> Self {
        Self {
            block,
            part,
            offset,
        }
    }

    /// How much text this position's part holds, or `None` when the block has
    /// no such part — an image and a rule have none at all.
    pub fn len_in(self, doc: &Doc) -> Option<usize> {
        doc.blocks
            .get(self.block)?
            .text_at(self.part)
            .map(|text| text.text.len())
    }

    /// Pull the caret back onto a real position: a block that exists, a part
    /// that block has, an offset inside it, and a character boundary.
    pub fn clamp(self, doc: &Doc) -> Self {
        if doc.blocks.is_empty() {
            return Self::default();
        }
        let block = self.block.min(doc.blocks.len() - 1);
        // The part can vanish under the caret — a table loses a row, a block
        // changes kind — so fall back to the block's first, and to nothing at
        // all for a block no caret can enter.
        let part = match doc.blocks[block].text_at(self.part) {
            Some(_) => self.part,
            None => match doc.blocks[block].parts().first() {
                Some(part) => *part,
                None => return Self::new(block, Part::default(), 0),
            },
        };
        let here = Self::new(block, part, self.offset);
        let Some(text) = doc.blocks[block].text_at(part) else {
            return here;
        };
        let mut offset = self.offset.min(text.text.len());
        while offset > 0 && !text.text.is_char_boundary(offset) {
            offset -= 1;
        }
        if let Some(mention) = text.mention_around(offset) {
            offset = match offset - mention.start <= mention.end - offset {
                true => mention.start,
                false => mention.end,
            };
        }
        Self { offset, ..here }
    }

    /// The first position at or after block `from` a caret can sit in.
    fn next_editable(doc: &Doc, from: usize) -> Option<Self> {
        (from..doc.blocks.len()).find_map(|ix| {
            doc.blocks[ix]
                .parts()
                .first()
                .map(|part| Self::new(ix, *part, 0))
        })
    }

    /// The last position at or before block `from` a caret can sit in, at its
    /// end — where stepping backwards into it lands.
    fn previous_editable(doc: &Doc, from: usize) -> Option<Self> {
        (0..=from.min(doc.blocks.len().saturating_sub(1)))
            .rev()
            .find_map(|ix| {
                let part = *doc.blocks[ix].parts().last()?;
                let at = Self::new(ix, part, 0);
                Some(Self {
                    offset: at.len_in(doc).unwrap_or(0),
                    ..at
                })
            })
    }

    /// The neighbouring part within this block — the cells of a table are the
    /// only case, and they are why this is not just "the next block".
    fn step_part(self, doc: &Doc, by: isize) -> Option<Part> {
        let parts = doc.blocks.get(self.block)?.parts();
        let ix = parts.iter().position(|part| *part == self.part)?;
        parts.get(ix.checked_add_signed(by)?).copied()
    }

    /// One character left, stepping into the part or block before it.
    pub fn left(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        if here.offset > 0
            && let Some(text) = doc.blocks[here.block].text_at(here.part)
        {
            let mut offset = here.offset - 1;
            while offset > 0 && !text.text.is_char_boundary(offset) {
                offset -= 1;
            }
            let offset = text.mention_around(offset).map_or(offset, |m| m.start);
            return Self { offset, ..here };
        }
        if let Some(part) = here.step_part(doc, -1) {
            let at = Self { part, ..here };
            return Self {
                offset: at.len_in(doc).unwrap_or(0),
                ..at
            };
        }
        here.block
            .checked_sub(1)
            .and_then(|ix| Self::previous_editable(doc, ix))
            .unwrap_or(here)
    }

    /// One character right, stepping into the part or block after it.
    pub fn right(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        let len = here.len_in(doc).unwrap_or(0);
        if here.offset < len
            && let Some(text) = doc.blocks[here.block].text_at(here.part)
        {
            let mut offset = here.offset + 1;
            while offset < len && !text.text.is_char_boundary(offset) {
                offset += 1;
            }
            let offset = text.mention_around(offset).map_or(offset, |m| m.end);
            return Self { offset, ..here };
        }
        if let Some(part) = here.step_part(doc, 1) {
            return Self {
                part,
                offset: 0,
                ..here
            };
        }
        Self::next_editable(doc, here.block + 1).unwrap_or(here)
    }

    /// The row above, keeping the offset where it fits.
    ///
    /// Within a table that is the cell above in the same column; everywhere
    /// else it is the block above. A wrapped paragraph's visual lines are not
    /// reachable from here — that needs the paint's layout, and the editor
    /// resolves it there.
    pub fn up(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        if let Part::Cell { row, column } = here.part
            && row > 0
        {
            return Self {
                part: Part::Cell {
                    row: row - 1,
                    column,
                },
                ..here
            }
            .clamp(doc);
        }
        match here
            .block
            .checked_sub(1)
            .and_then(|ix| Self::previous_editable(doc, ix))
        {
            Some(above) => Self {
                offset: here.offset,
                ..above
            }
            .clamp(doc),
            None => Self { offset: 0, ..here },
        }
    }

    /// The row below, keeping the offset where it fits.
    pub fn down(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        if let Part::Cell { row, column } = here.part {
            let below = Self {
                part: Part::Cell {
                    row: row + 1,
                    column,
                },
                ..here
            };
            if below.len_in(doc).is_some() {
                return below.clamp(doc);
            }
        }
        match Self::next_editable(doc, here.block + 1) {
            Some(below) => Self {
                offset: here.offset,
                ..below
            }
            .clamp(doc),
            None => Self {
                offset: here.len_in(doc).unwrap_or(0),
                ..here
            },
        }
    }

    pub fn home(self) -> Self {
        Self { offset: 0, ..self }
    }

    pub fn end(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        Self {
            offset: here.len_in(doc).unwrap_or(0),
            ..here
        }
    }

    /// The start of the word at or before the caret — alt-left.
    ///
    /// Whitespace first, then the run of word characters, which is the rule
    /// every platform's word-left follows and the one `ui::TextField` uses.
    pub fn word_left(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
            return here.left(doc);
        };
        if here.offset == 0 {
            return here.left(doc);
        }
        let head = &text.text[..here.offset];
        let trimmed = head.trim_end_matches(|c: char| !c.is_alphanumeric());
        let offset = trimmed.trim_end_matches(char::is_alphanumeric).len();
        let offset = text.mention_around(offset).map_or(offset, |m| m.start);
        Self { offset, ..here }
    }

    /// The end of the word at or after the caret — alt-right.
    pub fn word_right(self, doc: &Doc) -> Self {
        let here = self.clamp(doc);
        let Some(text) = doc.blocks[here.block].text_at(here.part) else {
            return here.right(doc);
        };
        if here.offset >= text.text.len() {
            return here.right(doc);
        }
        let tail = &text.text[here.offset..];
        let skipped = tail.len()
            - tail
                .trim_start_matches(|c: char| !c.is_alphanumeric())
                .len();
        let rest = &tail[skipped..];
        let word = rest.len() - rest.trim_start_matches(char::is_alphanumeric).len();
        let offset = here.offset + skipped + word;
        Self {
            offset: text.mention_around(offset).map_or(offset, |m| m.end),
            ..here
        }
    }

    /// The span of `unit` holding the caret, inside its own part. A word is
    /// the run of alphanumerics the caret touches, or of anything else when it
    /// touches none; a line runs between the part's newlines. A mention is never split.
    pub fn span(self, unit: Granularity, doc: &Doc) -> Range<Self> {
        let here = self.clamp(doc);
        let Some(text) = doc
            .blocks
            .get(here.block)
            .and_then(|b| b.text_at(here.part))
        else {
            return here..here;
        };
        let (start, end) = match unit {
            Granularity::Char => return here..here,
            Granularity::Line => (
                ui::input::line_start(&text.text, here.offset),
                ui::input::line_end(&text.text, here.offset),
            ),
            Granularity::Word => {
                let (head, tail) = text.text.split_at(here.offset);
                let word = head.chars().next_back().is_some_and(char::is_alphanumeric)
                    || tail.chars().next().is_some_and(char::is_alphanumeric);
                let same = |c: &char| c.is_alphanumeric() == word;
                let back: usize = head
                    .chars()
                    .rev()
                    .take_while(same)
                    .map(char::len_utf8)
                    .sum();
                let ahead: usize = tail.chars().take_while(same).map(char::len_utf8).sum();
                (here.offset - back, here.offset + ahead)
            }
        };
        let start = text.mention_around(start).map_or(start, |m| m.start);
        let end = text.mention_around(end).map_or(end, |m| m.end);
        Self {
            offset: start,
            ..here
        }..Self {
            offset: end,
            ..here
        }
    }
}

/// Which painted row the head of a [`Selection`] sits on when its offset is a
/// soft wrap, where one offset both ends a row and starts the next. Anywhere
/// else it changes nothing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Affinity {
    /// The start of the row after the wrap.
    #[default]
    Downstream,
    /// The end of the row before the wrap.
    Upstream,
}

/// A range in the document: where the selection started, and where it is being
/// dragged to.
///
/// The head is the end that moves — shift+arrow and a mouse drag both leave the
/// anchor where it was. Collapsed (`anchor == head`) is an ordinary caret, so
/// there is one position type in the editor rather than two.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Selection {
    pub anchor: Cursor,
    pub head: Cursor,
    /// The head's row at a soft wrap.
    pub affinity: Affinity,
}

impl From<Cursor> for Selection {
    fn from(cursor: Cursor) -> Self {
        Self::at(cursor)
    }
}

impl Selection {
    /// A collapsed selection — a plain caret.
    pub fn at(cursor: Cursor) -> Self {
        Self::new(cursor, cursor)
    }

    pub fn new(anchor: Cursor, head: Cursor) -> Self {
        Self {
            anchor,
            head,
            affinity: Affinity::Downstream,
        }
    }

    pub fn with_affinity(self, affinity: Affinity) -> Self {
        Self { affinity, ..self }
    }

    pub fn is_collapsed(&self) -> bool {
        self.anchor == self.head
    }

    /// The two ends in document order.
    pub fn ordered(&self) -> (Cursor, Cursor) {
        (self.anchor.min(self.head), self.anchor.max(self.head))
    }

    /// Move the head, leaving the anchor — every shift+motion and every drag.
    pub fn extend_to(self, head: Cursor) -> Self {
        Self::new(self.anchor, head)
    }

    pub fn clamp(self, doc: &Doc) -> Self {
        Self {
            anchor: self.anchor.clamp(doc),
            head: self.head.clamp(doc),
            ..self
        }
    }

    /// The whole document.
    pub fn all(doc: &Doc) -> Self {
        let start = Cursor::next_editable(doc, 0).unwrap_or_default();
        let end =
            Cursor::previous_editable(doc, doc.blocks.len().saturating_sub(1)).unwrap_or(start);
        Self::new(start, end)
    }
}