Skip to main content

documents/
selection.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! A reader's place in a document, carried across a save
7//! (`crates/documents/Spec.lean` D10).
8
9use crate::edit::Transaction;
10use crate::span::Span;
11
12/// Where a selection was begun and where it was carried to, as byte
13/// offsets of one version; collapsed when the two are the same offset.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Selection {
16    pub anchor: u64,
17    pub focus: u64,
18}
19
20impl Selection {
21    /// A caret at `at`.
22    pub const fn collapsed(at: u64) -> Selection {
23        Selection {
24            anchor: at,
25            focus: at,
26        }
27    }
28
29    /// The bytes the selection covers, whichever way it was drawn.
30    pub fn span(self) -> Span {
31        Span::ordered(self.anchor, self.focus)
32    }
33
34    /// Where this selection stands in the version `transaction` makes.
35    ///
36    /// An offset before an edit stays put, an insertion at the offset
37    /// itself included; one after it moves by what the edit added and
38    /// removed; one inside a replaced span moves to the end of what
39    /// replaced it, because the bytes it pointed into are gone.
40    pub fn mapped(self, transaction: &Transaction) -> Selection {
41        Selection {
42            anchor: position_after(self.anchor, transaction),
43            focus: position_after(self.focus, transaction),
44        }
45    }
46}
47
48fn position_after(at: u64, transaction: &Transaction) -> u64 {
49    let mut added = 0_u64;
50    let mut removed = 0_u64;
51    for edit in transaction.edits() {
52        let span = edit.span;
53        if at <= span.start() {
54            break;
55        }
56        let written = crate::span::offset(edit.bytes.len());
57        if at < span.end() {
58            return span
59                .start()
60                .saturating_add(added)
61                .saturating_sub(removed)
62                .saturating_add(written);
63        }
64        added = added.saturating_add(written);
65        removed = removed.saturating_add(span.len());
66    }
67    at.saturating_add(added).saturating_sub(removed)
68}
69
70#[cfg(test)]
71#[allow(
72    clippy::unwrap_used,
73    clippy::expect_used,
74    clippy::panic,
75    clippy::indexing_slicing,
76    reason = "test code"
77)]
78mod tests {
79    use kernel::B3Hash;
80
81    use super::*;
82    use crate::edit::Edit;
83
84    fn save(source: &[u8], edits: &[(u64, u64, &[u8])]) -> Transaction {
85        Transaction::new(
86            B3Hash::digest(source),
87            edits
88                .iter()
89                .map(|(start, end, bytes)| Edit {
90                    span: Span::new(*start, *end).unwrap(),
91                    bytes: bytes.to_vec(),
92                })
93                .collect(),
94        )
95        .unwrap()
96    }
97
98    /// A caret before, inside and after the edits of one save lands on
99    /// the same character it stood beside, or past what replaced the
100    /// bytes it stood in.
101    #[test]
102    fn a_selection_follows_the_text_it_stood_beside() {
103        let source = b"0123456789";
104        let saved = save(source, &[(2, 2, b"ab"), (4, 7, b"x")]);
105        let after = saved.apply(source).unwrap();
106        assert_eq!(after.bytes(), b"01ab23x789");
107        let mapped = |at: u64| Selection::collapsed(at).mapped(&saved).anchor;
108        assert_eq!(mapped(1), 1);
109        assert_eq!(mapped(2), 2);
110        assert_eq!(mapped(3), 5);
111        assert_eq!(mapped(5), 7);
112        assert_eq!(mapped(8), 8);
113        assert_eq!(mapped(10), 10);
114        let drawn = Selection {
115            anchor: 9,
116            focus: 1,
117        }
118        .mapped(&saved);
119        assert_eq!(drawn.span(), Span::new(1, 9).unwrap());
120    }
121}