| use codespan_reporting::files::Error; |
| #[cfg(feature = "serialization")] |
| use serde::{Deserialize, Serialize}; |
| use std::ffi::{OsStr, OsString}; |
| use std::num::NonZeroU32; |
| |
| use crate::{ByteIndex, ColumnIndex, LineIndex, LineOffset, Location, RawIndex, Span}; |
| |
| /// A handle that points to a file in the database. |
| #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| #[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))] |
| pub struct FileId(NonZeroU32); |
| |
| impl FileId { |
| /// Offset of our `FileId`'s numeric value to an index on `Files::files`. |
| /// |
| /// This is to ensure the first `FileId` is non-zero for memory layout optimisations (e.g. |
| /// `Option<FileId>` is 4 bytes) |
| const OFFSET: u32 = 1; |
| |
| fn new(index: usize) -> FileId { |
| FileId(NonZeroU32::new(index as u32 + Self::OFFSET).expect("file index cannot be stored")) |
| } |
| |
| fn get(self) -> usize { |
| (self.0.get() - Self::OFFSET) as usize |
| } |
| } |
| |
| /// A database of source files. |
| /// |
| /// The `Source` generic parameter determines how source text is stored. Using [`String`] will have |
| /// `Files` take ownership of all source text. Smart pointer types such as [`Cow<'_, str>`], |
| /// [`Rc<str>`] or [`Arc<str>`] can be used to share the source text with the rest of the program. |
| /// |
| /// [`Cow<'_, str>`]: std::borrow::Cow |
| /// [`Rc<str>`]: std::rc::Rc |
| /// [`Arc<str>`]: std::sync::Arc |
| #[derive(Clone, Debug)] |
| pub struct Files<Source> { |
| files: Vec<File<Source>>, |
| } |
| |
| impl<Source> Default for Files<Source> |
| where |
| Source: AsRef<str>, |
| { |
| fn default() -> Self { |
| Self { files: vec![] } |
| } |
| } |
| |
| impl<Source> Files<Source> |
| where |
| Source: AsRef<str>, |
| { |
| /// Create a new, empty database of files. |
| pub fn new() -> Self { |
| Files::<Source>::default() |
| } |
| |
| /// Add a file to the database, returning the handle that can be used to |
| /// refer to it again. |
| pub fn add(&mut self, name: impl Into<OsString>, source: Source) -> FileId { |
| let file_id = FileId::new(self.files.len()); |
| self.files.push(File::new(name.into(), source.into())); |
| file_id |
| } |
| |
| /// Update a source file in place. |
| /// |
| /// This will mean that any outstanding byte indexes will now point to |
| /// invalid locations. |
| pub fn update(&mut self, file_id: FileId, source: Source) { |
| self.get_mut(file_id).update(source.into()) |
| } |
| |
| /// Get a the source file using the file id. |
| // FIXME: return an option or result? |
| fn get(&self, file_id: FileId) -> &File<Source> { |
| &self.files[file_id.get()] |
| } |
| |
| /// Get a the source file using the file id. |
| // FIXME: return an option or result? |
| fn get_mut(&mut self, file_id: FileId) -> &mut File<Source> { |
| &mut self.files[file_id.get()] |
| } |
| |
| /// Get the name of the source file. |
| /// |
| /// ```rust |
| /// use codespan::Files; |
| /// |
| /// let name = "test"; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add(name, "hello world!"); |
| /// |
| /// assert_eq!(files.name(file_id), name); |
| /// ``` |
| pub fn name(&self, file_id: FileId) -> &OsStr { |
| self.get(file_id).name() |
| } |
| |
| /// Get the span at the given line index. |
| /// |
| /// ```rust |
| /// use codespan::{Files, LineIndex, Span}; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add("test", "foo\nbar\r\n\nbaz"); |
| /// |
| /// let line_sources = (0..4) |
| /// .map(|line| files.line_span(file_id, line).unwrap()) |
| /// .collect::<Vec<_>>(); |
| /// |
| /// assert_eq!(line_sources, |
| /// [ |
| /// Span::new(0, 4), // 0: "foo\n" |
| /// Span::new(4, 9), // 1: "bar\r\n" |
| /// Span::new(9, 10), // 2: "" |
| /// Span::new(10, 13), // 3: "baz" |
| /// ] |
| /// ); |
| /// assert!(files.line_span(file_id, 4).is_err()); |
| /// ``` |
| pub fn line_span( |
| &self, |
| file_id: FileId, |
| line_index: impl Into<LineIndex>, |
| ) -> Result<Span, Error> { |
| self.get(file_id).line_span(line_index.into()) |
| } |
| |
| /// Get the line index at the given byte in the source file. |
| /// |
| /// ```rust |
| /// use codespan::{Files, LineIndex}; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add("test", "foo\nbar\r\n\nbaz"); |
| /// |
| /// assert_eq!(files.line_index(file_id, 0), LineIndex::from(0)); |
| /// assert_eq!(files.line_index(file_id, 7), LineIndex::from(1)); |
| /// assert_eq!(files.line_index(file_id, 8), LineIndex::from(1)); |
| /// assert_eq!(files.line_index(file_id, 9), LineIndex::from(2)); |
| /// assert_eq!(files.line_index(file_id, 100), LineIndex::from(3)); |
| /// ``` |
| pub fn line_index(&self, file_id: FileId, byte_index: impl Into<ByteIndex>) -> LineIndex { |
| self.get(file_id).line_index(byte_index.into()) |
| } |
| |
| /// Get the location at the given byte index in the source file. |
| /// |
| /// ```rust |
| /// use codespan::{ByteIndex, Files, Location, Span}; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add("test", "foo\nbar\r\n\nbaz"); |
| /// |
| /// assert_eq!(files.location(file_id, 0).unwrap(), Location::new(0, 0)); |
| /// assert_eq!(files.location(file_id, 7).unwrap(), Location::new(1, 3)); |
| /// assert_eq!(files.location(file_id, 8).unwrap(), Location::new(1, 4)); |
| /// assert_eq!(files.location(file_id, 9).unwrap(), Location::new(2, 0)); |
| /// assert!(files.location(file_id, 100).is_err()); |
| /// ``` |
| pub fn location( |
| &self, |
| file_id: FileId, |
| byte_index: impl Into<ByteIndex>, |
| ) -> Result<Location, Error> { |
| self.get(file_id).location(byte_index.into()) |
| } |
| |
| /// Get the source of the file. |
| /// |
| /// ```rust |
| /// use codespan::Files; |
| /// |
| /// let source = "hello world!"; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add("test", source); |
| /// |
| /// assert_eq!(*files.source(file_id), source); |
| /// ``` |
| pub fn source(&self, file_id: FileId) -> &Source { |
| self.get(file_id).source() |
| } |
| |
| /// Return the span of the full source. |
| /// |
| /// ```rust |
| /// use codespan::{Files, Span}; |
| /// |
| /// let source = "hello world!"; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add("test", source); |
| /// |
| /// assert_eq!(files.source_span(file_id), Span::from_str(source)); |
| /// ``` |
| pub fn source_span(&self, file_id: FileId) -> Span { |
| self.get(file_id).source_span() |
| } |
| |
| /// Return a slice of the source file, given a span. |
| /// |
| /// ```rust |
| /// use codespan::{Files, Span}; |
| /// |
| /// let mut files = Files::new(); |
| /// let file_id = files.add("test", "hello world!"); |
| /// |
| /// assert_eq!(files.source_slice(file_id, Span::new(0, 5)).unwrap(), "hello"); |
| /// assert!(files.source_slice(file_id, Span::new(0, 100)).is_err()); |
| /// ``` |
| pub fn source_slice(&self, file_id: FileId, span: impl Into<Span>) -> Result<&str, Error> { |
| self.get(file_id).source_slice(span.into()) |
| } |
| } |
| |
| impl<'a, Source> codespan_reporting::files::Files<'a> for Files<Source> |
| where |
| Source: AsRef<str>, |
| { |
| type FileId = FileId; |
| type Name = String; |
| type Source = &'a str; |
| |
| fn name(&self, id: FileId) -> Result<String, Error> { |
| use std::path::PathBuf; |
| |
| Ok(PathBuf::from(self.name(id)).display().to_string()) |
| } |
| |
| fn source(&'a self, id: FileId) -> Result<&str, Error> { |
| Ok(self.source(id).as_ref()) |
| } |
| |
| fn line_index(&self, id: FileId, byte_index: usize) -> Result<usize, Error> { |
| Ok(self.line_index(id, byte_index as u32).to_usize()) |
| } |
| |
| fn line_range( |
| &'a self, |
| id: FileId, |
| line_index: usize, |
| ) -> Result<std::ops::Range<usize>, Error> { |
| let span = self.line_span(id, line_index as u32)?; |
| |
| Ok(span.start().to_usize()..span.end().to_usize()) |
| } |
| } |
| |
| /// A file that is stored in the database. |
| #[derive(Debug, Clone)] |
| // `Serialize` is only implemented on `OsString` for windows/unix |
| #[cfg_attr( |
| all(feature = "serialization", any(windows, unix)), |
| derive(Deserialize, Serialize) |
| )] |
| struct File<Source> { |
| /// The name of the file. |
| name: OsString, |
| /// The source code of the file. |
| source: Source, |
| /// The starting byte indices in the source code. |
| line_starts: Vec<ByteIndex>, |
| } |
| |
| impl<Source> File<Source> |
| where |
| Source: AsRef<str>, |
| { |
| fn new(name: OsString, source: Source) -> Self { |
| let line_starts = line_starts(source.as_ref()) |
| .map(|i| ByteIndex::from(i as u32)) |
| .collect(); |
| |
| File { |
| name, |
| source, |
| line_starts, |
| } |
| } |
| |
| fn update(&mut self, source: Source) { |
| let line_starts = line_starts(source.as_ref()) |
| .map(|i| ByteIndex::from(i as u32)) |
| .collect(); |
| self.source = source; |
| self.line_starts = line_starts; |
| } |
| |
| fn name(&self) -> &OsStr { |
| &self.name |
| } |
| |
| fn line_start(&self, line_index: LineIndex) -> Result<ByteIndex, Error> { |
| use std::cmp::Ordering; |
| |
| match line_index.cmp(&self.last_line_index()) { |
| Ordering::Less => Ok(self.line_starts[line_index.to_usize()]), |
| Ordering::Equal => Ok(self.source_span().end()), |
| Ordering::Greater => Err(Error::LineTooLarge { |
| given: line_index.to_usize(), |
| max: self.last_line_index().to_usize(), |
| }), |
| } |
| } |
| |
| fn last_line_index(&self) -> LineIndex { |
| LineIndex::from(self.line_starts.len() as RawIndex) |
| } |
| |
| fn line_span(&self, line_index: LineIndex) -> Result<Span, Error> { |
| let line_start = self.line_start(line_index)?; |
| let next_line_start = self.line_start(line_index + LineOffset::from(1))?; |
| |
| Ok(Span::new(line_start, next_line_start)) |
| } |
| |
| fn line_index(&self, byte_index: ByteIndex) -> LineIndex { |
| match self.line_starts.binary_search(&byte_index) { |
| // Found the start of a line |
| Ok(line) => LineIndex::from(line as u32), |
| Err(next_line) => LineIndex::from(next_line as u32 - 1), |
| } |
| } |
| |
| fn location(&self, byte_index: ByteIndex) -> Result<Location, Error> { |
| let line_index = self.line_index(byte_index); |
| let line_start_index = self |
| .line_start(line_index) |
| .map_err(|_| Error::IndexTooLarge { |
| given: byte_index.to_usize(), |
| max: self.source().as_ref().len() - 1, |
| })?; |
| let line_src = self |
| .source |
| .as_ref() |
| .get(line_start_index.to_usize()..byte_index.to_usize()) |
| .ok_or_else(|| { |
| let given = byte_index.to_usize(); |
| let max = self.source().as_ref().len() - 1; |
| if given > max { |
| Error::IndexTooLarge { given, max } |
| } else { |
| Error::InvalidCharBoundary { given } |
| } |
| })?; |
| |
| Ok(Location { |
| line: line_index, |
| column: ColumnIndex::from(line_src.chars().count() as u32), |
| }) |
| } |
| |
| fn source(&self) -> &Source { |
| &self.source |
| } |
| |
| fn source_span(&self) -> Span { |
| Span::from_str(self.source.as_ref()) |
| } |
| |
| fn source_slice(&self, span: Span) -> Result<&str, Error> { |
| let start = span.start().to_usize(); |
| let end = span.end().to_usize(); |
| |
| self.source.as_ref().get(start..end).ok_or_else(|| { |
| let max = self.source().as_ref().len() - 1; |
| Error::IndexTooLarge { |
| given: if start > max { start } else { end }, |
| max, |
| } |
| }) |
| } |
| } |
| |
| // NOTE: this is copied from `codespan_reporting::files::line_starts` and should be kept in sync. |
| fn line_starts<'source>(source: &'source str) -> impl 'source + Iterator<Item = usize> { |
| std::iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1)) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| const TEST_SOURCE: &str = "foo\nbar\r\n\nbaz"; |
| |
| #[test] |
| fn line_starts() { |
| let mut files = Files::<String>::new(); |
| let file_id = files.add("test", TEST_SOURCE.to_owned()); |
| |
| assert_eq!( |
| files.get(file_id).line_starts, |
| [ |
| ByteIndex::from(0), // "foo\n" |
| ByteIndex::from(4), // "bar\r\n" |
| ByteIndex::from(9), // "" |
| ByteIndex::from(10), // "baz" |
| ], |
| ); |
| } |
| |
| #[test] |
| fn line_span_sources() { |
| // Also make sure we can use `Arc` for source |
| use std::sync::Arc; |
| |
| let mut files = Files::<Arc<str>>::new(); |
| let file_id = files.add("test", TEST_SOURCE.into()); |
| |
| let line_sources = (0..4) |
| .map(|line| { |
| let line_span = files.line_span(file_id, line).unwrap(); |
| files.source_slice(file_id, line_span).unwrap() |
| }) |
| .collect::<Vec<_>>(); |
| |
| assert_eq!(line_sources, ["foo\n", "bar\r\n", "\n", "baz"],); |
| } |
| |
| #[test] |
| fn interoperability() { |
| extern crate termcolor; |
| use codespan_reporting::{diagnostic::*, term::emit}; |
| use termcolor::{ColorChoice, StandardStream}; |
| |
| let mut files = Files::<String>::new(); |
| let file_id = files.add("test", TEST_SOURCE.to_owned()); |
| |
| let diagnostic = Diagnostic::note() |
| .with_message("middle") |
| .with_labels(vec![Label::primary(file_id, 4..7).with_message("middle")]); |
| |
| let config = codespan_reporting::term::Config::default(); |
| emit( |
| &mut StandardStream::stdout(ColorChoice::Auto), |
| &config, |
| &files, |
| &diagnostic, |
| ) |
| .unwrap(); |
| } |
| } |