Skip to main content

sipha_source/
position.rs

1//! Line and column position types for source locations.
2
3/// A position in source code, represented as line and column numbers.
4///
5/// Positions are 1-indexed for user-facing display (line 1, column 1 is the first character).
6/// The byte offset is also stored for efficient conversion back to spans.
7///
8/// # Example
9///
10/// ```rust
11/// use sipha_source::Position;
12///
13/// let pos = Position::new(2, 5, 20);
14/// assert_eq!(pos.line(), 2);
15/// assert_eq!(pos.column(), 5);
16/// assert_eq!(pos.byte_offset(), 20);
17/// ```
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct Position {
20    /// 1-indexed line number.
21    line: usize,
22    /// 1-indexed column number (character position, UTF-8 aware).
23    column: usize,
24    /// Byte offset in the source.
25    byte_offset: usize,
26}
27
28impl Position {
29    /// Create a new position.
30    ///
31    /// # Arguments
32    ///
33    /// * `line` - 1-indexed line number
34    /// * `column` - 1-indexed column number (character position)
35    /// * `byte_offset` - Byte offset in the source
36    ///
37    /// # Example
38    ///
39    /// ```rust
40    /// use sipha_source::Position;
41    ///
42    /// let pos = Position::new(1, 1, 0);
43    /// ```
44    pub fn new(line: usize, column: usize, byte_offset: usize) -> Self {
45        Self {
46            line,
47            column,
48            byte_offset,
49        }
50    }
51
52    /// Get the 1-indexed line number.
53    pub fn line(&self) -> usize {
54        self.line
55    }
56
57    /// Get the 1-indexed column number.
58    pub fn column(&self) -> usize {
59        self.column
60    }
61
62    /// Get the byte offset.
63    pub fn byte_offset(&self) -> usize {
64        self.byte_offset
65    }
66}
67
68#[cfg(feature = "serde")]
69impl serde::Serialize for Position {
70    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
71    where
72        S: serde::Serializer,
73    {
74        use serde::ser::SerializeStruct;
75        let mut state = serializer.serialize_struct("Position", 3)?;
76        state.serialize_field("line", &self.line)?;
77        state.serialize_field("column", &self.column)?;
78        state.serialize_field("byte_offset", &self.byte_offset)?;
79        state.end()
80    }
81}
82
83#[cfg(feature = "serde")]
84impl<'de> serde::Deserialize<'de> for Position {
85    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86    where
87        D: serde::Deserializer<'de>,
88    {
89        use serde::de::{self, MapAccess, Visitor};
90        use std::fmt;
91
92        struct PositionVisitor;
93
94        impl<'de> Visitor<'de> for PositionVisitor {
95            type Value = Position;
96
97            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
98                formatter.write_str("struct Position")
99            }
100
101            fn visit_map<V>(self, mut map: V) -> Result<Position, V::Error>
102            where
103                V: MapAccess<'de>,
104            {
105                let mut line = None;
106                let mut column = None;
107                let mut byte_offset = None;
108                while let Some(key) = map.next_key()? {
109                    match key {
110                        "line" => {
111                            if line.is_some() {
112                                return Err(de::Error::duplicate_field("line"));
113                            }
114                            line = Some(map.next_value()?);
115                        }
116                        "column" => {
117                            if column.is_some() {
118                                return Err(de::Error::duplicate_field("column"));
119                            }
120                            column = Some(map.next_value()?);
121                        }
122                        "byte_offset" => {
123                            if byte_offset.is_some() {
124                                return Err(de::Error::duplicate_field("byte_offset"));
125                            }
126                            byte_offset = Some(map.next_value()?);
127                        }
128                        _ => {
129                            let _ = map.next_value::<de::IgnoredAny>()?;
130                        }
131                    }
132                }
133                let line = line.ok_or_else(|| de::Error::missing_field("line"))?;
134                let column = column.ok_or_else(|| de::Error::missing_field("column"))?;
135                let byte_offset =
136                    byte_offset.ok_or_else(|| de::Error::missing_field("byte_offset"))?;
137                Ok(Position::new(line, column, byte_offset))
138            }
139        }
140
141        const FIELDS: &[&str] = &["line", "column", "byte_offset"];
142        deserializer.deserialize_struct("Position", FIELDS, PositionVisitor)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_position_creation() {
152        let pos = Position::new(1, 5, 10);
153        assert_eq!(pos.line(), 1);
154        assert_eq!(pos.column(), 5);
155        assert_eq!(pos.byte_offset(), 10);
156    }
157}