1#[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#[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#[derive(Default, TryClone)]
84pub struct Source {
85 name: SourceName,
87 source: Box<str>,
89 #[cfg(feature = "std")]
91 path: Option<Box<Path>>,
92 line_starts: Box<[usize]>,
94}
95
96impl Source {
97 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 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 #[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 #[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 pub(crate) fn line_starts(&self) -> &[usize] {
188 &self.line_starts
189 }
190
191 pub fn name(&self) -> &str {
193 match &self.name {
194 SourceName::Memory => "<memory>",
195 SourceName::Name(name) => name,
196 }
197 }
198
199 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 pub(crate) fn as_str(&self) -> &str {
209 &self.source
210 }
211
212 #[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 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 pub fn find_utf8_line_column(&self, position: usize) -> (usize, usize) {
249 let (line, offset, _) = self.position(position);
250 (line, offset)
251 }
252
253 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 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 #[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 #[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 #[cfg(feature = "emit")]
304 pub(crate) fn line_count(&self) -> usize {
305 self.line_starts.len()
306 }
307
308 #[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
369pub struct SourceLine<'a> {
371 #[cfg(feature = "emit")]
372 name: &'a str,
373 pub line: usize,
375 pub column: usize,
377 pub text: &'a str,
379 #[cfg(feature = "emit")]
380 span: Span,
381}
382
383impl SourceLine<'_> {
384 #[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#[derive(Default, Debug, TryClone, PartialEq, Eq)]
426enum SourceName {
427 #[default]
430 Memory,
431 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
440fn 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}