markdown_editor/
position.rs1use std::cmp::Ordering;
2
3use super::{
4 document::{BlockContent, EditorDocument},
5 id::BlockId,
6};
7
8#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
10pub enum Affinity {
11 Upstream,
12 #[default]
13 Downstream,
14}
15
16#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
18pub struct BlockPosition {
19 pub block: BlockId,
20 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 UnknownBlock(BlockId),
42 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 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 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}