Skip to main content

ass_editor/core/document/
text_access.rs

1//! Text content queries and byte/line-column conversions
2//!
3//! Read-only accessors for length, emptiness, raw text, rope access, range
4//! extraction, and position-to-line/column mapping.
5
6use super::EditorDocument;
7use crate::core::errors::{EditorError, Result};
8use crate::core::position::{LineColumn, Position, Range};
9
10#[cfg(not(feature = "std"))]
11use alloc::string::{String, ToString};
12
13impl EditorDocument {
14    /// Get total length in bytes
15    #[must_use]
16    pub fn len_bytes(&self) -> usize {
17        #[cfg(feature = "rope")]
18        {
19            self.text_rope.len_bytes()
20        }
21        #[cfg(not(feature = "rope"))]
22        {
23            self.text_content.len()
24        }
25    }
26
27    /// Get total number of lines
28    #[must_use]
29    pub fn len_lines(&self) -> usize {
30        #[cfg(feature = "rope")]
31        {
32            self.text_rope.len_lines()
33        }
34        #[cfg(not(feature = "rope"))]
35        {
36            self.text_content.lines().count().max(1)
37        }
38    }
39
40    /// Check if document is empty
41    #[must_use]
42    pub fn is_empty(&self) -> bool {
43        self.len_bytes() == 0
44    }
45
46    /// Get text content as string
47    #[must_use]
48    pub fn text(&self) -> String {
49        #[cfg(feature = "rope")]
50        {
51            self.text_rope.to_string()
52        }
53        #[cfg(not(feature = "rope"))]
54        {
55            self.text_content.clone()
56        }
57    }
58
59    /// Get direct access to the rope for advanced operations
60    #[cfg(feature = "rope")]
61    #[must_use]
62    pub fn rope(&self) -> &ropey::Rope {
63        &self.text_rope
64    }
65
66    /// Get the length of the document in bytes
67    #[must_use]
68    pub fn len(&self) -> usize {
69        #[cfg(feature = "rope")]
70        {
71            self.text_rope.len_bytes()
72        }
73        #[cfg(not(feature = "rope"))]
74        {
75            self.text_content.len()
76        }
77    }
78
79    /// Get text content for a range
80    pub fn text_range(&self, range: Range) -> Result<String> {
81        let start = range.start.offset;
82        let end = range.end.offset;
83
84        if end > self.len_bytes() {
85            return Err(EditorError::InvalidRange {
86                start,
87                end,
88                length: self.len_bytes(),
89            });
90        }
91
92        #[cfg(feature = "rope")]
93        {
94            // Convert byte offsets to char indices for rope operations
95            let start_char = self.text_rope.byte_to_char(start);
96            let end_char = self.text_rope.byte_to_char(end);
97            Ok(self.text_rope.slice(start_char..end_char).to_string())
98        }
99        #[cfg(not(feature = "rope"))]
100        {
101            Ok(self.text_content[start..end].to_string())
102        }
103    }
104
105    /// Convert byte position to line/column
106    #[cfg(feature = "rope")]
107    pub fn position_to_line_column(&self, pos: Position) -> Result<LineColumn> {
108        if pos.offset > self.len_bytes() {
109            return Err(EditorError::PositionOutOfBounds {
110                position: pos.offset,
111                length: self.len_bytes(),
112            });
113        }
114
115        let line_idx = self.text_rope.byte_to_line(pos.offset);
116        let line_start = self.text_rope.line_to_byte(line_idx);
117        let col_offset = pos.offset - line_start;
118
119        // Convert byte offset to character offset within line
120        let line = self.text_rope.line(line_idx);
121        let mut char_col = 0;
122        let mut byte_count = 0;
123
124        for ch in line.chars() {
125            if byte_count >= col_offset {
126                break;
127            }
128            byte_count += ch.len_utf8();
129            char_col += 1;
130        }
131
132        // Convert to 1-indexed
133        LineColumn::new(line_idx + 1, char_col + 1)
134    }
135
136    /// Convert byte position to line/column (without rope)
137    #[cfg(not(feature = "rope"))]
138    pub fn position_to_line_column(&self, pos: Position) -> Result<LineColumn> {
139        if pos.offset > self.len_bytes() {
140            return Err(EditorError::PositionOutOfBounds {
141                position: pos.offset,
142                length: self.len_bytes(),
143            });
144        }
145
146        let mut line = 1;
147        let mut col = 1;
148        let mut byte_pos = 0;
149
150        for ch in self.text_content.chars() {
151            if byte_pos >= pos.offset {
152                break;
153            }
154
155            if ch == '\n' {
156                line += 1;
157                col = 1;
158            } else {
159                col += 1;
160            }
161
162            byte_pos += ch.len_utf8();
163        }
164
165        LineColumn::new(line, col)
166    }
167}