oak_core/source/text.rs
1use crate::{
2 OakError,
3 source::{Source, SourceId, TextEdit},
4};
5use core::range::Range;
6use std::borrow::Cow;
7use triomphe::Arc;
8
9/// Represents source code text with optional source ID reference.
10///
11/// This struct manages the raw source text and provides utilities for:
12/// - Text extraction at specific offsets or ranges
13/// - Error reporting with precise location information
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub struct SourceText {
16 pub(crate) source_id: Option<SourceId>,
17 pub(crate) raw: Arc<str>,
18}
19
20impl Default for SourceText {
21 fn default() -> Self {
22 Self { source_id: None, raw: Arc::from("") }
23 }
24}
25
26impl Source for SourceText {
27 fn length(&self) -> usize {
28 self.raw.len()
29 }
30
31 fn chunk_at(&self, offset: usize) -> crate::source::TextChunk<'_> {
32 let len = self.raw.len();
33 if offset >= len {
34 return crate::source::TextChunk { start: len, text: "" };
35 }
36 crate::source::TextChunk { start: offset, text: self.raw.get(offset..).unwrap_or("") }
37 }
38
39 fn get_text_in(&self, range: Range<usize>) -> Cow<'_, str> {
40 self.raw.get(range.start..range.end).map(Cow::Borrowed).unwrap_or(Cow::Borrowed(""))
41 }
42
43 fn source_id(&self) -> Option<SourceId> {
44 self.source_id
45 }
46}
47
48impl SourceText {
49 /// Creates a new source text from the given input.
50 pub fn new(input: impl Into<Arc<str>>) -> Self {
51 Self { source_id: None, raw: input.into() }
52 }
53
54 /// Creates a new source text from the given input and source ID.
55 pub fn new_with_id(input: impl Into<Arc<str>>, source_id: SourceId) -> Self {
56 Self { source_id: Some(source_id), raw: input.into() }
57 }
58
59 /// Returns the raw source text as a string slice.
60 pub fn text(&self) -> &str {
61 &self.raw
62 }
63
64 /// Applies multiple text edits to the source text and returns the affected range.
65 pub fn apply_edits_range(&mut self, edits: &[TextEdit]) -> Range<usize> {
66 let old_len = self.raw.len();
67 if edits.is_empty() {
68 return Range { start: old_len, end: old_len };
69 }
70
71 let mut order: Vec<usize> = (0..edits.len()).collect();
72 order.sort_by_key(|&i| edits[i].span.start);
73
74 let mut reparse_from = old_len;
75 let mut reparse_to = 0;
76 let mut delta: isize = 0;
77
78 for &i in &order {
79 let TextEdit { span, text } = &edits[i];
80 reparse_from = reparse_from.min(span.start);
81 let start_new = (span.start as isize + delta) as usize;
82 let end_new = start_new + text.len();
83 reparse_to = reparse_to.max(end_new);
84 delta += text.len() as isize - (span.end - span.start) as isize;
85 }
86
87 let mut raw = self.raw.to_string();
88 for &i in order.iter().rev() {
89 let TextEdit { span, text } = &edits[i];
90 raw.replace_range(span.start..span.end, text);
91 }
92 self.raw = Arc::from(raw);
93
94 Range { start: reparse_from, end: reparse_to }
95 }
96
97 /// Applies multiple text edits to the source text and returns the minimum affected offset.
98 ///
99 /// This method is used for incremental updates to source code, such as those
100 /// received from LSP clients or other text editing operations.
101 ///
102 /// # Arguments
103 ///
104 /// * `edits` - A slice of [`TextEdit`] operations to apply
105 ///
106 /// # Returns
107 ///
108 /// The minimum byte offset that was affected by any of the edits. This is
109 /// useful for determining where to restart parsing after incremental changes.
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// # #![feature(new_range_api)]
115 /// # use oak_core::source::SourceText;
116 /// # use oak_core::source::TextEdit;
117 /// # use core::range::Range;
118 /// let mut source = SourceText::new("let x = 5;");
119 /// let edits = vec![TextEdit { span: Range { start: 4, end: 5 }, text: "y".to_string() }];
120 /// let min_offset = source.apply_edits(&edits);
121 /// assert_eq!(min_offset, 4);
122 /// ```
123 pub fn apply_edits(&mut self, edits: &[TextEdit]) -> usize {
124 self.apply_edits_range(edits).start
125 }
126
127 /// Gets the source ID associated with this source text, if any.
128 ///
129 /// # Returns
130 ///
131 /// An [`Option<SourceId>`] containing the source ID if one was set,
132 /// or `None` if no ID is associated with this source text.
133 ///
134 /// # Examples
135 ///
136 /// ```
137 /// # use oak_core::SourceText;
138 /// let source = SourceText::new_with_id("code", 1);
139 /// assert!(source.source_id().is_some());
140 /// ```
141 pub fn source_id(&self) -> Option<SourceId> {
142 self.source_id
143 }
144
145 /// Gets the length of the source text in bytes.
146 ///
147 /// # Returns
148 ///
149 /// The length of the source text in bytes.
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// # use oak_core::SourceText;
155 /// let source = SourceText::new("Hello, world!");
156 /// assert_eq!(source.len(), 13);
157 /// ```
158 pub fn len(&self) -> usize {
159 self.raw.len()
160 }
161
162 /// Checks if the source text is empty.
163 ///
164 /// # Returns
165 ///
166 /// `true` if the source text is empty, `false` otherwise.
167 ///
168 /// # Examples
169 ///
170 /// ```
171 /// # use oak_core::SourceText;
172 /// let source = SourceText::new("");
173 /// assert!(source.is_empty());
174 /// ```
175 pub fn is_empty(&self) -> bool {
176 self.raw.is_empty()
177 }
178
179 /// Creates a syntax error with location information.
180 ///
181 /// # Arguments
182 ///
183 /// * `message` - The error message
184 /// * `offset` - The byte offset where the error occurred
185 ///
186 /// # Returns
187 ///
188 /// An [`OakError`] with precise location information including line and column.
189 ///
190 /// # Examples
191 ///
192 /// ```
193 /// # use oak_core::SourceText;
194 /// let source = SourceText::new("let x =");
195 /// let error = source.syntax_error("Unexpected end of input", 7);
196 /// ```
197 pub fn syntax_error(&self, message: impl Into<String>, offset: usize) -> OakError {
198 OakError::syntax_error(message, offset, self.source_id)
199 }
200
201 /// Creates an error for an unexpected character at the specified offset.
202 pub fn unexpected_character(&self, character: char, offset: usize) -> OakError {
203 OakError::unexpected_character(character, offset, self.source_id)
204 }
205
206 /// Creates an error for an expected token that was missing at the specified offset.
207 pub fn expected_token(&self, expected: impl Into<String>, offset: usize) -> OakError {
208 OakError::expected_token(expected, offset, self.source_id)
209 }
210
211 /// Creates an error for an expected name that was missing at the specified offset.
212 pub fn expected_name(&self, name_kind: impl Into<String>, offset: usize) -> OakError {
213 OakError::expected_name(name_kind, offset, self.source_id)
214 }
215
216 /// Creates an error for a trailing comma that is not allowed at the specified offset.
217 pub fn trailing_comma_not_allowed(&self, offset: usize) -> OakError {
218 OakError::trailing_comma_not_allowed(offset, self.source_id)
219 }
220}