oak_core/source/
rope.rs

1use crate::source::{Source, TextChunk, TextEdit};
2use core::range::Range;
3use std::borrow::Cow;
4use triomphe::Arc;
5use url::Url;
6
7const CHUNK_SIZE: usize = 4096;
8
9/// A read-only, rope-based source implementation for efficient handling of large files.
10#[derive(Clone, Debug)]
11pub struct RopeSource {
12    url: Option<Url>,
13    chunks: Arc<[Arc<str>]>,
14    starts: Arc<[usize]>,
15    len: usize,
16}
17
18/// A mutable buffer for rope-based source code, supporting efficient edits.
19#[derive(Clone, Debug)]
20pub struct RopeBuffer {
21    url: Option<Url>,
22    chunks: Vec<Arc<str>>,
23    starts: Vec<usize>,
24    len: usize,
25}
26
27impl RopeBuffer {
28    /// Creates a new empty `RopeBuffer`.
29    pub fn new(input: impl ToString) -> Self {
30        Self::new_with_url(input, None)
31    }
32
33    /// Creates a new `RopeBuffer` with the specified input and URL.
34    pub fn new_with_url(input: impl ToString, url: impl Into<Option<Url>>) -> Self {
35        let url = url.into();
36        let text = input.to_string();
37        let chunks = chunkify(&text);
38        let (starts, len) = rebuild_starts(&chunks);
39        Self { url, chunks, starts, len }
40    }
41
42    /// Returns a read-only snapshot of the current buffer.
43    pub fn snapshot(&self) -> RopeSource {
44        RopeSource { url: self.url.clone(), chunks: Arc::<[Arc<str>]>::from(self.chunks.clone()), starts: Arc::<[usize]>::from(self.starts.clone()), len: self.len }
45    }
46
47    /// Applies multiple text edits to the buffer and returns the affected range.
48    pub fn apply_edits_range(&mut self, edits: &[TextEdit]) -> Range<usize> {
49        let old_len = self.len;
50        if edits.is_empty() {
51            return Range { start: old_len, end: old_len };
52        }
53
54        let mut order: Vec<usize> = (0..edits.len()).collect();
55        order.sort_by_key(|&i| edits[i].span.start);
56
57        let mut reparse_from = old_len;
58        let mut reparse_to = 0;
59        let mut delta: isize = 0;
60        for &i in &order {
61            let TextEdit { span, text } = &edits[i];
62            reparse_from = reparse_from.min(span.start);
63            let start_new = (span.start as isize + delta) as usize;
64            let end_new = start_new + text.len();
65            reparse_to = reparse_to.max(end_new);
66            delta += text.len() as isize - (span.end - span.start) as isize;
67        }
68
69        for &i in order.iter().rev() {
70            let TextEdit { span, text } = &edits[i];
71            self.replace_range(Range { start: span.start, end: span.end }, text);
72        }
73
74        Range { start: reparse_from, end: reparse_to }
75    }
76
77    /// Applies multiple text edits to the buffer and returns the minimum affected offset.
78    pub fn apply_edits(&mut self, edits: &[TextEdit]) -> usize {
79        self.apply_edits_range(edits).start
80    }
81
82    fn replace_range(&mut self, span: Range<usize>, replacement: &str) {
83        let start = span.start.min(self.len);
84        let end = span.end.min(self.len).max(start);
85
86        self.split_at(start);
87        self.split_at(end);
88
89        let start_idx = self.boundary_index(start);
90        let end_idx = self.boundary_index(end);
91        if start_idx < end_idx {
92            self.chunks.drain(start_idx..end_idx);
93        }
94
95        if !replacement.is_empty() {
96            let new_chunks = chunkify(replacement);
97            let insert_at = start_idx.min(self.chunks.len());
98            self.chunks.splice(insert_at..insert_at, new_chunks);
99        }
100
101        let (starts, len) = rebuild_starts(&self.chunks);
102        self.starts = starts;
103        self.len = len;
104    }
105
106    fn split_at(&mut self, offset: usize) {
107        if offset == 0 || offset >= self.len {
108            return;
109        }
110        let (idx, start) = self.chunk_index(offset);
111        let rel = offset - start;
112        let s = self.chunks[idx].clone();
113        let s = s.as_ref();
114        if rel == 0 || rel >= s.len() {
115            return;
116        }
117        let Some(left) = s.get(..rel)
118        else {
119            return;
120        };
121        let Some(right) = s.get(rel..)
122        else {
123            return;
124        };
125        if left.is_empty() || right.is_empty() {
126            return;
127        }
128        self.chunks[idx] = Arc::<str>::from(left.to_string());
129        self.chunks.insert(idx + 1, Arc::<str>::from(right.to_string()));
130        let (starts, len) = rebuild_starts(&self.chunks);
131        self.starts = starts;
132        self.len = len;
133    }
134
135    fn boundary_index(&self, offset: usize) -> usize {
136        if offset >= self.len {
137            return self.chunks.len();
138        }
139        match self.starts.binary_search(&offset) {
140            Ok(i) => i,
141            Err(i) => i,
142        }
143    }
144
145    fn chunk_index(&self, offset: usize) -> (usize, usize) {
146        let offset = offset.min(self.len.saturating_sub(1));
147        match self.starts.binary_search(&offset) {
148            Ok(i) => (i, self.starts[i]),
149            Err(0) => (0, 0),
150            Err(i) => (i - 1, self.starts[i - 1]),
151        }
152    }
153}
154
155impl Source for RopeBuffer {
156    fn length(&self) -> usize {
157        self.len
158    }
159
160    fn chunk_at(&self, offset: usize) -> TextChunk<'_> {
161        if offset >= self.len {
162            return TextChunk { start: self.len, text: "" };
163        }
164        let (idx, start) = self.chunk_index(offset);
165        TextChunk { start, text: self.chunks[idx].as_ref() }
166    }
167
168    fn get_text_in(&self, range: Range<usize>) -> Cow<'_, str> {
169        text_in_chunks(&self.chunks, &self.starts, self.len, range)
170    }
171
172    fn get_url(&self) -> Option<&Url> {
173        self.url.as_ref()
174    }
175}
176
177impl Source for RopeSource {
178    fn length(&self) -> usize {
179        self.len
180    }
181
182    fn chunk_at(&self, offset: usize) -> TextChunk<'_> {
183        if offset >= self.len {
184            return TextChunk { start: self.len, text: "" };
185        }
186        let idx = match self.starts.binary_search(&offset) {
187            Ok(i) => i,
188            Err(0) => 0,
189            Err(i) => i - 1,
190        };
191        let start = self.starts[idx];
192        TextChunk { start, text: self.chunks[idx].as_ref() }
193    }
194
195    fn get_text_in(&self, range: Range<usize>) -> Cow<'_, str> {
196        text_in_chunks(&self.chunks, &self.starts, self.len, range)
197    }
198
199    fn get_url(&self) -> Option<&Url> {
200        self.url.as_ref()
201    }
202}
203
204fn rebuild_starts(chunks: &[Arc<str>]) -> (Vec<usize>, usize) {
205    let mut starts = Vec::with_capacity(chunks.len());
206    let mut offset = 0usize;
207    for c in chunks {
208        starts.push(offset);
209        offset += c.len();
210    }
211    (starts, offset)
212}
213
214fn chunkify(text: &str) -> Vec<Arc<str>> {
215    if text.is_empty() {
216        return vec![];
217    }
218    let mut out = Vec::new();
219    let mut start = 0usize;
220    while start < text.len() {
221        let mut end = (start + CHUNK_SIZE).min(text.len());
222        while end > start && !text.is_char_boundary(end) {
223            end -= 1;
224        }
225        if end == start {
226            end = text.len();
227        }
228        let part = &text[start..end];
229        out.push(Arc::<str>::from(part.to_string()));
230        start = end;
231    }
232    out
233}
234
235fn text_in_chunks<'a>(chunks: &'a [Arc<str>], starts: &'a [usize], len: usize, range: Range<usize>) -> Cow<'a, str> {
236    if range.start >= range.end || range.start >= len {
237        return Cow::Borrowed("");
238    }
239    let start = range.start;
240    let end = range.end.min(len);
241
242    let start_idx = match starts.binary_search(&start) {
243        Ok(i) => i,
244        Err(0) => 0,
245        Err(i) => i - 1,
246    };
247    let end_idx = match starts.binary_search(&end) {
248        Ok(i) => i,
249        Err(0) => 0,
250        Err(i) => i - 1,
251    };
252
253    if start_idx == end_idx {
254        let base = starts[start_idx];
255        let rel_start = start - base;
256        let rel_end = end - base;
257        let s = chunks[start_idx].as_ref();
258        return s.get(rel_start..rel_end).map(Cow::Borrowed).unwrap_or(Cow::Borrowed(""));
259    }
260
261    let mut buf = String::new();
262    for (i, c) in chunks.iter().enumerate().skip(start_idx).take(end_idx - start_idx + 1) {
263        let base = starts[i];
264        let cs = c.as_ref();
265        let seg_start = if i == start_idx { start.saturating_sub(base) } else { 0 };
266        let seg_end = if i == end_idx { end.saturating_sub(base) } else { cs.len() };
267        if let Some(seg) = cs.get(seg_start..seg_end) {
268            buf.push_str(seg);
269        }
270    }
271    Cow::Owned(buf)
272}