Skip to main content

documents/
span.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 stretch of one document version, counted in bytes from its first
7//! byte (`crates/documents/Spec.lean` D2).
8
9use kernel::{AxCode, AxError};
10use serde::{Deserialize, Serialize};
11
12/// A half-open byte interval `[start, end)` of one document version.
13///
14/// Half-open, so an empty span - an insertion point, the whole of an
15/// empty document - can be spelled, which the closed byte range of a
16/// `Locator` cannot do. [`Span::new`] is the one construction point and
17/// the wire reads through it, so a span whose start lies past its end
18/// is never held.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20#[serde(try_from = "SpanFields", into = "SpanFields")]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22pub struct Span {
23    start: u64,
24    end: u64,
25}
26
27impl Span {
28    /// The interval from `start` up to, not including, `end`.
29    ///
30    /// # Errors
31    /// `E_INVALID_ARGS` naming both offsets when `start` lies past `end`.
32    pub fn new(start: u64, end: u64) -> Result<Span, AxError> {
33        if start > end {
34            return Err(AxError::failure(
35                AxCode::InvalidArgs,
36                "name a stretch of a document",
37                format!("{start}..{end}"),
38            )
39            .with_recovery("give the start first: a span runs from its start up to its end"));
40        }
41        Ok(Span { start, end })
42    }
43
44    /// The empty span at `at`: where an insertion lands.
45    pub const fn at(at: u64) -> Span {
46        Span { start: at, end: at }
47    }
48
49    pub const fn start(self) -> u64 {
50        self.start
51    }
52
53    pub const fn end(self) -> u64 {
54        self.end
55    }
56
57    /// How many bytes the span covers.
58    pub const fn len(self) -> u64 {
59        self.end.saturating_sub(self.start)
60    }
61
62    pub const fn is_empty(self) -> bool {
63        self.start == self.end
64    }
65
66    /// The span from the lesser of two offsets to the greater, for the
67    /// callers in this crate that computed both and know their order.
68    pub(crate) fn ordered(one: u64, other: u64) -> Span {
69        Span {
70            start: one.min(other),
71            end: one.max(other),
72        }
73    }
74
75    /// The same span as indices into a byte slice of `len` bytes, or
76    /// `None` when it reaches past the slice.
77    pub(crate) fn within(self, len: usize) -> Option<(usize, usize)> {
78        let start = usize::try_from(self.start).ok()?;
79        let end = usize::try_from(self.end).ok()?;
80        (end <= len).then_some((start, end))
81    }
82}
83
84/// A byte count or index as an offset. Every slice this crate reads fits
85/// in a `u64`, so the ceiling is never reached on a machine this runs on.
86pub(crate) fn offset(index: usize) -> u64 {
87    u64::try_from(index).unwrap_or(u64::MAX)
88}
89
90/// The wire spelling of a [`Span`], read back through [`Span::new`].
91#[derive(Serialize, Deserialize)]
92#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
93#[cfg_attr(feature = "schema", schemars(rename = "Span"))]
94struct SpanFields {
95    start: u64,
96    end: u64,
97}
98
99impl TryFrom<SpanFields> for Span {
100    type Error = AxError;
101
102    fn try_from(fields: SpanFields) -> Result<Span, AxError> {
103        Span::new(fields.start, fields.end)
104    }
105}
106
107impl From<Span> for SpanFields {
108    fn from(span: Span) -> SpanFields {
109        SpanFields {
110            start: span.start,
111            end: span.end,
112        }
113    }
114}