Skip to main content

ass_editor/core/position/
builder.rs

1//! Fluent builder for constructing document [`Position`] values.
2//!
3//! Defines [`PositionBuilder`], which resolves line/column or byte
4//! offset specifications into a [`Position`], using the rope when the
5//! `rope` feature is enabled.
6
7#[cfg(feature = "rope")]
8use super::LineColumn;
9use super::Position;
10use crate::core::errors::{EditorError, Result};
11
12/// Builder for creating document positions with fluent API
13///
14/// Provides ergonomic ways to create positions:
15/// ```
16/// use ass_editor::{EditorDocument, PositionBuilder};
17///
18/// let document = EditorDocument::from_content("Line 1\nLine 2\nLine 3").unwrap();
19///
20/// // PositionBuilder requires a Rope, not EditorDocument
21/// // For this example, we'll use Position::new directly
22/// let pos = ass_editor::Position::new(7); // Position at start of "Line 2"
23///
24/// assert_eq!(pos.offset, 7);
25/// ```
26#[derive(Debug, Clone, Default)]
27pub struct PositionBuilder {
28    line: Option<usize>,
29    column: Option<usize>,
30    offset: Option<usize>,
31}
32
33impl PositionBuilder {
34    /// Create a new position builder
35    #[must_use]
36    pub const fn new() -> Self {
37        Self {
38            line: None,
39            column: None,
40            offset: None,
41        }
42    }
43
44    /// Set line number (1-indexed)
45    #[must_use]
46    pub const fn line(mut self, line: usize) -> Self {
47        self.line = Some(line);
48        self
49    }
50
51    /// Set column number (1-indexed)
52    #[must_use]
53    pub const fn column(mut self, column: usize) -> Self {
54        self.column = Some(column);
55        self
56    }
57
58    /// Set byte offset directly
59    #[must_use]
60    pub const fn offset(mut self, offset: usize) -> Self {
61        self.offset = Some(offset);
62        self
63    }
64
65    /// Build position at the start of a line
66    #[must_use]
67    pub const fn at_line_start(mut self, line: usize) -> Self {
68        self.line = Some(line);
69        self.column = Some(1);
70        self
71    }
72
73    /// Build position at the end of a line
74    #[must_use]
75    pub const fn at_line_end(mut self, line: usize) -> Self {
76        self.line = Some(line);
77        self.column = None; // Will be calculated
78        self
79    }
80
81    /// Build position at the start of the document
82    #[must_use]
83    pub const fn at_start() -> Self {
84        Self {
85            line: Some(1),
86            column: Some(1),
87            offset: Some(0),
88        }
89    }
90
91    /// Build position using rope for line/column conversion
92    ///
93    /// If offset is provided, uses that directly.
94    /// Otherwise converts from line/column using the rope.
95    #[cfg(feature = "rope")]
96    pub fn build(self, rope: &ropey::Rope) -> Result<Position> {
97        if let Some(offset) = self.offset {
98            if offset > rope.len_bytes() {
99                return Err(EditorError::PositionOutOfBounds {
100                    position: offset,
101                    length: rope.len_bytes(),
102                });
103            }
104            Ok(Position::new(offset))
105        } else if let Some(line) = self.line {
106            // Convert to 0-indexed
107            let line_idx = line.saturating_sub(1);
108
109            if line_idx >= rope.len_lines() {
110                return Err(EditorError::InvalidPosition { line, column: 1 });
111            }
112
113            let line_start = rope.line_to_byte(line_idx);
114
115            if let Some(column) = self.column {
116                LineColumn::new(line, column)?;
117                let col_idx = column.saturating_sub(1);
118                let line = rope.line(line_idx);
119
120                // Find the byte position of the column
121                let mut byte_pos = 0;
122                let mut char_pos = 0;
123
124                for ch in line.chars() {
125                    if char_pos == col_idx {
126                        break;
127                    }
128                    byte_pos += ch.len_utf8();
129                    char_pos += 1;
130                }
131
132                if char_pos < col_idx {
133                    return Err(EditorError::InvalidPosition {
134                        line: self.line.unwrap_or(0),
135                        column,
136                    });
137                }
138
139                Ok(Position::new(line_start + byte_pos))
140            } else {
141                // No column specified - go to end of line
142                let line_end = if line_idx + 1 < rope.len_lines() {
143                    rope.line_to_byte(line_idx + 1).saturating_sub(1)
144                } else {
145                    rope.len_bytes()
146                };
147                Ok(Position::new(line_end))
148            }
149        } else {
150            // Default to start if nothing specified
151            Ok(Position::start())
152        }
153    }
154
155    /// Build position without rope (offset must be specified)
156    #[cfg(not(feature = "rope"))]
157    pub fn build(self) -> Result<Position> {
158        if let Some(offset) = self.offset {
159            Ok(Position::new(offset))
160        } else {
161            Err(EditorError::FeatureNotEnabled {
162                feature: "line/column position".to_string(),
163                required_feature: "rope".to_string(),
164            })
165        }
166    }
167}