Skip to main content

atelier_sdk/
read.rs

1use atelier_sdk_diff::PackageId;
2
3use crate::error::Error;
4
5/// The most bytes one read returns; also the default window. No unbounded
6/// responses exist on the surface.
7pub const READ_WINDOW_MAX: usize = 50_000;
8
9/// Where in the text a read's content sits, in bytes of the text read —
10/// the projection's for a projected document, the document's own otherwise.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ReadWindow {
13    /// The window's first byte offset.
14    pub start: usize,
15    /// The offset one past the window's last byte.
16    pub end: usize,
17    /// The whole text's size in bytes.
18    pub total: usize,
19}
20
21/// One windowed read: bounded content plus the cursor to continue from.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ReadResult {
24    /// The window's content.
25    pub content: String,
26    /// Where the content sits in the text.
27    pub window: ReadWindow,
28    /// The byte offset the next read starts at; `None` when this window
29    /// reached the end.
30    pub next: Option<usize>,
31    /// The package whose projection was read; `None` for plain text.
32    pub projected_by: Option<PackageId>,
33}
34
35/// The window size a caller asked for, bounded to `1..=READ_WINDOW_MAX`.
36pub(crate) fn window_size(requested: Option<usize>) -> Result<usize, Error> {
37    match requested {
38        None => Ok(READ_WINDOW_MAX),
39        Some(size) if (1..=READ_WINDOW_MAX).contains(&size) => Ok(size),
40        Some(_) => Err(Error::WindowTooLarge {
41            max: READ_WINDOW_MAX,
42        }),
43    }
44}
45
46/// The window of `text` starting at byte `start`, at most `size` bytes,
47/// both edges snapped to character boundaries so the content stays valid
48/// UTF-8. A start at or past the end yields an empty window at the end.
49pub(crate) fn window_text(
50    text: &str,
51    start: usize,
52    size: usize,
53    projected_by: Option<PackageId>,
54) -> ReadResult {
55    let total = text.len();
56    let start = snap_forward(text, start.min(total));
57    let end = snap_back(text, start.saturating_add(size).min(total)).max(start);
58    // Every face trusts these window invariants; hold them here, where
59    // the window is built.
60    assert!(start <= end);
61    assert!(end <= total);
62    assert!(
63        end - start <= size + 3,
64        "a boundary snap moves at most one character"
65    );
66    ReadResult {
67        content: text[start..end].to_owned(),
68        window: ReadWindow { start, end, total },
69        next: (end < total).then_some(end),
70        projected_by,
71    }
72}
73
74fn snap_forward(text: &str, mut at: usize) -> usize {
75    while at < text.len() && !text.is_char_boundary(at) {
76        at += 1;
77    }
78    at
79}
80
81fn snap_back(text: &str, mut at: usize) -> usize {
82    while at > 0 && !text.is_char_boundary(at) {
83        at -= 1;
84    }
85    at
86}
87
88#[cfg(test)]
89mod tests {
90    use super::{READ_WINDOW_MAX, window_size, window_text};
91
92    #[test]
93    fn windows_chain_through_the_text_and_reassemble_it() {
94        let text = "0123456789";
95
96        let first = window_text(text, 0, 4, None);
97        assert_eq!(first.content, "0123");
98        assert_eq!((first.window.start, first.window.end), (0, 4));
99        assert_eq!(first.window.total, 10);
100        assert_eq!(first.next, Some(4));
101
102        let second = window_text(text, 4, 4, None);
103        assert_eq!(second.content, "4567");
104        assert_eq!(second.next, Some(8));
105
106        let last = window_text(text, 8, 4, None);
107        assert_eq!(last.content, "89");
108        assert_eq!(last.next, None);
109
110        assert_eq!(
111            format!("{}{}{}", first.content, second.content, last.content),
112            text
113        );
114    }
115
116    #[test]
117    fn window_edges_snap_to_character_boundaries() {
118        // é is two bytes; a window ending inside it must retreat.
119        let text = "ané";
120        let clipped = window_text(text, 0, 3, None);
121        assert_eq!(clipped.content, "an");
122        assert_eq!(clipped.next, Some(2));
123
124        let rest = window_text(text, 2, 3, None);
125        assert_eq!(rest.content, "é");
126        assert_eq!(rest.next, None);
127    }
128
129    #[test]
130    fn a_start_past_the_end_yields_an_empty_final_window() {
131        let result = window_text("abc", 10, 5, None);
132        assert_eq!(result.content, "");
133        assert_eq!((result.window.start, result.window.end), (3, 3));
134        assert_eq!(result.next, None);
135    }
136
137    #[test]
138    fn window_sizes_are_bounded() {
139        assert_eq!(window_size(None).unwrap(), READ_WINDOW_MAX);
140        assert_eq!(window_size(Some(1)).unwrap(), 1);
141        assert!(window_size(Some(0)).is_err());
142        assert!(window_size(Some(READ_WINDOW_MAX + 1)).is_err());
143    }
144}