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