Skip to main content

markdown_editor/
position.rs

1use std::cmp::Ordering;
2
3use super::{
4    document::{BlockContent, EditorDocument},
5    id::BlockId,
6};
7
8/// Caret side at a soft wrap or mark boundary. Not part of ordering.
9#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
10pub enum Affinity {
11    Upstream,
12    #[default]
13    Downstream,
14}
15
16/// A caret location. Plain value type: its fields are the definition.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
18pub struct BlockPosition {
19    pub block: BlockId,
20    /// UTF-8 byte offset into the block's flat text, on a grapheme boundary.
21    /// Byte offset in the leaf's text (inline text or code). For atomic
22    /// blocks: 0 = before, 1 = after.
23    pub offset: usize,
24    pub affinity: Affinity,
25}
26
27impl BlockPosition {
28    pub fn new(block: BlockId, offset: usize) -> Self {
29        Self {
30            block,
31            offset,
32            affinity: Affinity::Downstream,
33        }
34    }
35}
36
37#[non_exhaustive]
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39pub enum PositionError {
40    /// Removed, from another document, or from before `set_source`.
41    UnknownBlock(BlockId),
42    /// The block holds other blocks (e.g. List, TableRow).
43    NotACaretBlock(BlockId),
44    OffsetOutOfBounds {
45        len: usize,
46    },
47    NotOnGraphemeBoundary,
48}
49
50impl EditorDocument {
51    pub(crate) fn validate(&self, p: BlockPosition) -> Result<BlockPosition, PositionError> {
52        let entry = self
53            .get(p.block)
54            .ok_or(PositionError::UnknownBlock(p.block))?;
55        match &entry.content {
56            BlockContent::Children(_) => Err(PositionError::NotACaretBlock(p.block)),
57            BlockContent::Inline(inline) => {
58                if p.offset > inline.len() {
59                    Err(PositionError::OffsetOutOfBounds { len: inline.len() })
60                } else if !inline.is_grapheme_boundary(p.offset) {
61                    Err(PositionError::NotOnGraphemeBoundary)
62                } else {
63                    Ok(p)
64                }
65            }
66            BlockContent::Plain(code) => {
67                if p.offset > code.len() {
68                    Err(PositionError::OffsetOutOfBounds { len: code.len() })
69                } else if !code.is_char_boundary(p.offset) {
70                    Err(PositionError::NotOnGraphemeBoundary)
71                } else {
72                    Ok(p)
73                }
74            }
75            BlockContent::Atom(_) => {
76                if p.offset > 1 {
77                    Err(PositionError::OffsetOutOfBounds { len: 1 })
78                } else {
79                    Ok(p)
80                }
81            }
82        }
83    }
84
85    /// The nearest valid position, falling back to the document start.
86    pub(crate) fn clamp(&self, p: BlockPosition) -> BlockPosition {
87        match self.validate(p) {
88            Ok(p) => p,
89            Err(PositionError::OffsetOutOfBounds { len }) => BlockPosition { offset: len, ..p },
90            Err(PositionError::NotOnGraphemeBoundary) => {
91                let offset = match self.plain(p.block) {
92                    Some(code) => (0..=p.offset.min(code.len()))
93                        .rev()
94                        .find(|i| code.is_char_boundary(*i))
95                        .unwrap_or(0),
96                    None => self
97                        .inline(p.block)
98                        .and_then(|inline| inline.prev_grapheme(p.offset))
99                        .unwrap_or(0),
100                };
101                BlockPosition { offset, ..p }
102            }
103            Err(_) => self.first_caret(),
104        }
105    }
106
107    /// Document order, O(1) via the order index.
108    pub(crate) fn cmp_positions(
109        &self,
110        a: BlockPosition,
111        b: BlockPosition,
112    ) -> Result<Ordering, PositionError> {
113        let a = self.validate(a)?;
114        let b = self.validate(b)?;
115        let rank = |p: BlockPosition| {
116            self.order
117                .rank(p.block)
118                .ok_or(PositionError::UnknownBlock(p.block))
119        };
120        Ok(rank(a)?.cmp(&rank(b)?).then(a.offset.cmp(&b.offset)))
121    }
122}