Skip to main content

rune/
source.rs

1//! Module for dealing with sources.
2//!
3//! The primary type in here is the [`Source`] struct, which holds onto all
4//! metadata necessary related to a source in order to build it.
5//!
6//! Sources are stored in the [`Sources`] collection.
7//!
8//! [`Sources`]: crate::sources::Sources
9
10#[cfg(feature = "emit")]
11use core::cmp;
12use core::fmt;
13use core::iter;
14#[cfg(feature = "emit")]
15use core::ops::Range;
16use core::slice;
17
18#[cfg(feature = "emit")]
19use std::io;
20#[cfg(feature = "std")]
21use std::path::Path;
22
23use crate as rune;
24#[cfg(feature = "std")]
25use crate::alloc::borrow::Cow;
26use crate::alloc::prelude::*;
27use crate::alloc::{self, Box};
28use crate::ast::Span;
29#[cfg(feature = "emit")]
30use crate::termcolor::{self, WriteColor};
31
32/// Error raised when constructing a source.
33#[derive(Debug)]
34pub struct FromPathError {
35    kind: FromPathErrorKind,
36}
37
38impl From<alloc::Error> for FromPathError {
39    fn from(error: alloc::Error) -> Self {
40        Self {
41            kind: FromPathErrorKind::Alloc(error),
42        }
43    }
44}
45
46#[cfg(feature = "std")]
47impl From<std::io::Error> for FromPathError {
48    fn from(error: std::io::Error) -> Self {
49        Self {
50            kind: FromPathErrorKind::Io(error),
51        }
52    }
53}
54
55impl fmt::Display for FromPathError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match &self.kind {
58            FromPathErrorKind::Alloc(error) => error.fmt(f),
59            #[cfg(feature = "std")]
60            FromPathErrorKind::Io(error) => error.fmt(f),
61        }
62    }
63}
64
65#[derive(Debug)]
66enum FromPathErrorKind {
67    Alloc(alloc::Error),
68    #[cfg(feature = "std")]
69    Io(std::io::Error),
70}
71
72impl core::error::Error for FromPathError {
73    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
74        match &self.kind {
75            FromPathErrorKind::Alloc(error) => Some(error),
76            #[cfg(feature = "std")]
77            FromPathErrorKind::Io(error) => Some(error),
78        }
79    }
80}
81
82/// A single source file.
83#[derive(Default, TryClone)]
84pub struct Source {
85    /// The name of the source.
86    name: SourceName,
87    /// The source string.
88    source: Box<str>,
89    /// The path the source was loaded from.
90    #[cfg(feature = "std")]
91    path: Option<Box<Path>>,
92    /// The starting byte indices in the source code.
93    line_starts: Box<[usize]>,
94}
95
96impl Source {
97    /// Construct a new source with the given name.
98    pub fn new(name: impl AsRef<str>, source: impl AsRef<str>) -> alloc::Result<Self> {
99        let name = Box::try_from(name.as_ref())?;
100        let source = source.as_ref();
101        let line_starts = line_starts(source).try_collect::<Box<[_]>>()?;
102
103        Ok(Self {
104            name: SourceName::Name(name),
105            source: source.try_into()?,
106            #[cfg(feature = "std")]
107            path: None,
108            line_starts,
109        })
110    }
111
112    /// Construct a new anonymously named `<memory>` source.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use rune::Source;
118    ///
119    /// let source = Source::memory("pub fn main() { 42 }")?;
120    /// assert_eq!(source.name(), "<memory>");
121    /// # Ok::<_, rune::support::Error>(())
122    /// ```
123    pub fn memory(source: impl AsRef<str>) -> alloc::Result<Self> {
124        let source = source.as_ref();
125        let line_starts = line_starts(source).try_collect::<Box<[_]>>()?;
126
127        Ok(Self {
128            name: SourceName::Memory,
129            source: source.try_into()?,
130            #[cfg(feature = "std")]
131            path: None,
132            line_starts,
133        })
134    }
135
136    /// Read and load a source from the given filesystem path.
137    #[cfg(feature = "std")]
138    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
139    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, FromPathError> {
140        let name = Box::try_from(Cow::try_from(path.as_ref().to_string_lossy())?)?;
141        let source = Box::try_from(std::fs::read_to_string(path.as_ref())?)?;
142        let path = Some(path.as_ref().try_into()?);
143        let line_starts = line_starts(source.as_ref()).try_collect::<Box<[_]>>()?;
144
145        Ok(Self {
146            name: SourceName::Name(name),
147            source,
148            path,
149            line_starts,
150        })
151    }
152
153    /// Construct a new source with the given content and path.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use std::path::Path;
159    /// use rune::Source;
160    ///
161    /// let source = Source::with_path("test", "pub fn main() { 42 }", "test.rn")?;
162    /// assert_eq!(source.name(), "test");
163    /// assert_eq!(source.path(), Some(Path::new("test.rn")));
164    /// # Ok::<_, rune::support::Error>(())
165    /// ```
166    #[cfg(feature = "std")]
167    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
168    pub fn with_path(
169        name: impl AsRef<str>,
170        source: impl AsRef<str>,
171        path: impl AsRef<Path>,
172    ) -> alloc::Result<Self> {
173        let name = Box::try_from(name.as_ref())?;
174        let source = Box::try_from(source.as_ref())?;
175        let path = Some(path.as_ref().try_into()?);
176        let line_starts = line_starts(source.as_ref()).try_collect::<Box<[_]>>()?;
177
178        Ok(Self {
179            name: SourceName::Name(name),
180            source,
181            path,
182            line_starts,
183        })
184    }
185
186    /// Access all line starts in the source.
187    pub(crate) fn line_starts(&self) -> &[usize] {
188        &self.line_starts
189    }
190
191    /// Get the name of the source.
192    pub fn name(&self) -> &str {
193        match &self.name {
194            SourceName::Memory => "<memory>",
195            SourceName::Name(name) => name,
196        }
197    }
198
199    ///  et the given range from the source.
200    pub(crate) fn get<I>(&self, i: I) -> Option<&I::Output>
201    where
202        I: slice::SliceIndex<str>,
203    {
204        self.source.get(i)
205    }
206
207    /// Access the underlying string for the source.
208    pub(crate) fn as_str(&self) -> &str {
209        &self.source
210    }
211
212    /// Get the path associated with the source.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use std::path::Path;
218    /// use rune::Source;
219    ///
220    /// let source = Source::with_path("test", "pub fn main() { 42 }", "test.rn")?;
221    /// assert_eq!(source.name(), "test");
222    /// assert_eq!(source.path(), Some(Path::new("test.rn")));
223    /// # Ok::<_, rune::support::Error>(())
224    /// ```
225    #[cfg(feature = "std")]
226    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
227    pub fn path(&self) -> Option<&Path> {
228        self.path.as_deref()
229    }
230
231    /// Convert the given position to a utf-8 line position in code units.
232    ///
233    /// A position is a character offset into the source in utf-8 characters.
234    ///
235    /// Note that utf-8 code units is what you'd count when using the
236    /// [`str::chars()`] iterator.
237    pub fn find_line_column(&self, position: usize) -> (usize, usize) {
238        let (line, offset, rest) = self.position(position);
239        let col = rest.char_indices().take_while(|&(n, _)| n < offset).count();
240        (line, col)
241    }
242
243    /// Convert the given position to a line and the number of bytes into that
244    /// line.
245    ///
246    /// This is what the language server protocol counts when it has been told
247    /// to use `utf-8` positions: a character there is a byte, not a character.
248    pub fn find_utf8_line_column(&self, position: usize) -> (usize, usize) {
249        let (line, offset, _) = self.position(position);
250        (line, offset)
251    }
252
253    /// Convert the given position to a utf-16 code units line and character.
254    ///
255    /// A position is a character offset into the source in utf-16 characters.
256    ///
257    /// Note that utf-16 code units is what you'd count when iterating over the
258    /// string in terms of characters as-if they would have been encoded with
259    /// [`char::encode_utf16()`].
260    pub fn find_utf16cu_line_column(&self, position: usize) -> (usize, usize) {
261        let (line, offset, rest) = self.position(position);
262
263        let col = rest
264            .char_indices()
265            .flat_map(|(n, c)| (n < offset).then(|| c.encode_utf16(&mut [0u16; 2]).len()))
266            .sum();
267
268        (line, col)
269    }
270
271    /// Fetch [`SourceLine`] information for the given span.
272    pub fn source_line(&self, span: Span) -> Option<SourceLine<'_>> {
273        let (line, column, text, _span) = line_for(self, span)?;
274
275        Some(SourceLine {
276            #[cfg(feature = "emit")]
277            name: self.name(),
278            line,
279            column,
280            text,
281            #[cfg(feature = "emit")]
282            span: _span,
283        })
284    }
285
286    /// Get the line index for the given byte.
287    #[cfg(feature = "emit")]
288    pub(crate) fn line_index(&self, byte_index: usize) -> usize {
289        self.line_starts
290            .binary_search(&byte_index)
291            .unwrap_or_else(|next_line| next_line.saturating_sub(1))
292    }
293
294    /// Get the range corresponding to the given line index.
295    #[cfg(feature = "emit")]
296    pub(crate) fn line_range(&self, line_index: usize) -> Option<Range<usize>> {
297        let line_start = self.line_start(line_index)?;
298        let next_line_start = self.line_start(line_index.saturating_add(1))?;
299        Some(line_start..next_line_start)
300    }
301
302    /// Get the number of lines in the source.
303    #[cfg(feature = "emit")]
304    pub(crate) fn line_count(&self) -> usize {
305        self.line_starts.len()
306    }
307
308    /// Access the line number of content that starts with the given span.
309    #[cfg(feature = "emit")]
310    pub(crate) fn line(&self, span: Span) -> Option<(usize, usize, [&str; 3])> {
311        let from = span.range();
312        let (lin, col) = self.find_line_column(from.start);
313        let line = self.line_range(lin)?;
314
315        let start = from.start.checked_sub(line.start)?;
316        let end = from.end.checked_sub(line.start)?;
317
318        let text = self.source.get(line)?;
319        let prefix = text.get(..start)?;
320        let mid = text.get(start..end)?;
321        let suffix = text.get(end..)?;
322
323        Some((lin, col, [prefix, mid, suffix]))
324    }
325
326    fn position(&self, offset: usize) -> (usize, usize, &str) {
327        if offset == 0 {
328            return Default::default();
329        }
330
331        let line = match self.line_starts.binary_search(&offset) {
332            Ok(exact) => exact,
333            Err(0) => return Default::default(),
334            Err(n) => n - 1,
335        };
336
337        let line_start = self.line_starts[line];
338
339        let rest = &self.source[line_start..];
340        let offset = offset.saturating_sub(line_start);
341        (line, offset, rest)
342    }
343
344    #[cfg(feature = "emit")]
345    fn line_start(&self, line_index: usize) -> Option<usize> {
346        match line_index.cmp(&self.line_starts.len()) {
347            cmp::Ordering::Less => self.line_starts.get(line_index).copied(),
348            cmp::Ordering::Equal => Some(self.source.as_ref().len()),
349            cmp::Ordering::Greater => None,
350        }
351    }
352
353    #[cfg(feature = "workspace")]
354    pub(crate) fn len(&self) -> usize {
355        self.source.len()
356    }
357}
358
359impl fmt::Debug for Source {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        let mut st = f.debug_struct("Source");
362        st.field("name", &self.name);
363        #[cfg(feature = "std")]
364        st.field("path", &self.path);
365        st.finish()
366    }
367}
368
369/// An extracted source line.
370pub struct SourceLine<'a> {
371    #[cfg(feature = "emit")]
372    name: &'a str,
373    /// The line number in the source.
374    pub line: usize,
375    /// The column number in the source.
376    pub column: usize,
377    /// The text of the span.
378    pub text: &'a str,
379    #[cfg(feature = "emit")]
380    span: Span,
381}
382
383impl SourceLine<'_> {
384    /// Pretty write a source line to the given output.
385    #[cfg(feature = "emit")]
386    pub(crate) fn write(&self, o: &mut dyn WriteColor) -> io::Result<()> {
387        let mut highlight = termcolor::ColorSpec::new();
388        highlight.set_fg(Some(termcolor::Color::Yellow));
389
390        let mut new_line = termcolor::ColorSpec::new();
391        new_line.set_fg(Some(termcolor::Color::Red));
392
393        let text = self.text.trim_end();
394        let end = self.span.end.into_usize().min(text.len());
395
396        let before = &text[0..self.span.start.into_usize()].trim_start();
397        let inner = &text[self.span.start.into_usize()..end];
398        let after = &text[end..];
399
400        {
401            let name = self.name;
402            let line = self.line + 1;
403            let start = self.column + 1;
404            let end = start + inner.chars().count();
405            write!(o, "{name}:{line}:{start}-{end}: ")?;
406        }
407
408        write!(o, "{before}")?;
409        o.set_color(&highlight)?;
410        write!(o, "{inner}")?;
411        o.reset()?;
412        write!(o, "{after}")?;
413
414        if self.span.end != end {
415            o.set_color(&new_line)?;
416            write!(o, "\\n")?;
417            o.reset()?;
418        }
419
420        Ok(())
421    }
422}
423
424/// Holder for the name of a source.
425#[derive(Default, Debug, TryClone, PartialEq, Eq)]
426enum SourceName {
427    /// An in-memory source, will use `<memory>` when the source is being
428    /// referred to in diagnostics.
429    #[default]
430    Memory,
431    /// A named source.
432    Name(Box<str>),
433}
434
435#[inline(always)]
436fn line_starts(source: &str) -> impl Iterator<Item = usize> + '_ {
437    iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1))
438}
439
440/// Get the line number and source line for the given source and span.
441fn line_for(source: &Source, span: Span) -> Option<(usize, usize, &str, Span)> {
442    let line_starts = source.line_starts();
443
444    let line = match line_starts.binary_search(&span.start.into_usize()) {
445        Ok(n) => n,
446        Err(n) => n.saturating_sub(1),
447    };
448
449    let start = *line_starts.get(line)?;
450    let end = line.checked_add(1)?;
451
452    let s = if let Some(end) = line_starts.get(end) {
453        source.get(start..*end)?
454    } else {
455        source.get(start..)?
456    };
457
458    let line_end = span.start.into_usize().saturating_sub(start);
459
460    let column = s
461        .get(..line_end)
462        .into_iter()
463        .flat_map(|s| s.chars())
464        .count();
465
466    let start = start.try_into().unwrap();
467
468    Some((
469        line,
470        column,
471        s,
472        Span::new(
473            span.start.saturating_sub(start),
474            span.end.saturating_sub(start),
475        ),
476    ))
477}