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}