1//! An example that shows how to implement a simple custom file database.
2//! The database uses 32-bit file-ids, which could be useful for optimizing
3//! memory usage.
4//!
5//! To run this example, execute the following command from the top level of
6//! this repository:
7//!
8//! ```sh
9//! cargo run --example custom_files
10//! ```
11
12use codespan_reporting::diagnostic::{Diagnostic, Label};
13use codespan_reporting::term;
14use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
15use std::ops::Range;
16
17fn main() -> anyhow::Result<()> {
18    let mut files = files::Files::new();
19
20    let file_id0 = files.add("0.greeting", "hello world!").unwrap();
21    let file_id1 = files.add("1.greeting", "bye world").unwrap();
22
23    let messages = vec![
24        Message::UnwantedGreetings {
25            greetings: vec![(file_id0, 0..5), (file_id1, 0..3)],
26        },
27        Message::OverTheTopExclamations {
28            exclamations: vec![(file_id0, 11..12)],
29        },
30    ];
31
32    let writer = StandardStream::stderr(ColorChoice::Always);
33    let config = term::Config::default();
34    for message in &messages {
35        let writer = &mut writer.lock();
36        term::emit(writer, &config, &files, &message.to_diagnostic())?;
37    }
38
39    Ok(())
40}
41
42/// A module containing the file implementation
43mod files {
44    use codespan_reporting::files;
45    use std::ops::Range;
46
47    /// A file that is backed by an `Arc<String>`.
48    #[derive(Debug, Clone)]
49    struct File {
50        /// The name of the file.
51        name: String,
52        /// The source code of the file.
53        source: String,
54        /// The starting byte indices in the source code.
55        line_starts: Vec<usize>,
56    }
57
58    impl File {
59        fn line_start(&self, line_index: usize) -> Result<usize, files::Error> {
60            use std::cmp::Ordering;
61
62            match line_index.cmp(&self.line_starts.len()) {
63                Ordering::Less => Ok(self
64                    .line_starts
65                    .get(line_index)
66                    .expect("failed despite previous check")
67                    .clone()),
68                Ordering::Equal => Ok(self.source.len()),
69                Ordering::Greater => Err(files::Error::LineTooLarge {
70                    given: line_index,
71                    max: self.line_starts.len() - 1,
72                }),
73            }
74        }
75    }
76
77    /// An opaque file identifier.
78    #[derive(Copy, Clone, PartialEq, Eq)]
79    pub struct FileId(u32);
80
81    #[derive(Debug, Clone)]
82    pub struct Files {
83        files: Vec<File>,
84    }
85
86    impl Files {
87        /// Create a new files database.
88        pub fn new() -> Files {
89            Files { files: Vec::new() }
90        }
91
92        /// Add a file to the database, returning the handle that can be used to
93        /// refer to it again.
94        pub fn add(
95            &mut self,
96            name: impl Into<String>,
97            source: impl Into<String>,
98        ) -> Option<FileId> {
99            use std::convert::TryFrom;
100
101            let file_id = FileId(u32::try_from(self.files.len()).ok()?);
102            let name = name.into();
103            let source = source.into();
104            let line_starts = files::line_starts(&source).collect();
105
106            self.files.push(File {
107                name,
108                line_starts,
109                source,
110            });
111
112            Some(file_id)
113        }
114
115        /// Get the file corresponding to the given id.
116        fn get(&self, file_id: FileId) -> Result<&File, files::Error> {
117            self.files
118                .get(file_id.0 as usize)
119                .ok_or(files::Error::FileMissing)
120        }
121    }
122
123    impl<'files> files::Files<'files> for Files {
124        type FileId = FileId;
125        type Name = &'files str;
126        type Source = &'files str;
127
128        fn name(&self, file_id: FileId) -> Result<&str, files::Error> {
129            Ok(self.get(file_id)?.name.as_ref())
130        }
131
132        fn source(&self, file_id: FileId) -> Result<&str, files::Error> {
133            Ok(&self.get(file_id)?.source)
134        }
135
136        fn line_index(&self, file_id: FileId, byte_index: usize) -> Result<usize, files::Error> {
137            self.get(file_id)?
138                .line_starts
139                .binary_search(&byte_index)
140                .or_else(|next_line| Ok(next_line - 1))
141        }
142
143        fn line_range(
144            &self,
145            file_id: FileId,
146            line_index: usize,
147        ) -> Result<Range<usize>, files::Error> {
148            let file = self.get(file_id)?;
149            let line_start = file.line_start(line_index)?;
150            let next_line_start = file.line_start(line_index + 1)?;
151
152            Ok(line_start..next_line_start)
153        }
154    }
155}
156
157/// A Diagnostic message.
158enum Message {
159    UnwantedGreetings {
160        greetings: Vec<(files::FileId, Range<usize>)>,
161    },
162    OverTheTopExclamations {
163        exclamations: Vec<(files::FileId, Range<usize>)>,
164    },
165}
166
167impl Message {
168    fn to_diagnostic(&self) -> Diagnostic<files::FileId> {
169        match self {
170            Message::UnwantedGreetings { greetings } => Diagnostic::error()
171                .with_message("greetings are not allowed")
172                .with_labels(
173                    greetings
174                        .iter()
175                        .map(|(file_id, range)| {
176                            Label::primary(*file_id, range.clone()).with_message("a greeting")
177                        })
178                        .collect(),
179                )
180                .with_notes(vec![
181                    "found greetings!".to_owned(),
182                    "pleas no greetings :(".to_owned(),
183                ]),
184            Message::OverTheTopExclamations { exclamations } => Diagnostic::error()
185                .with_message("over-the-top exclamations")
186                .with_labels(
187                    exclamations
188                        .iter()
189                        .map(|(file_id, range)| {
190                            Label::primary(*file_id, range.clone()).with_message("an exclamation")
191                        })
192                        .collect(),
193                )
194                .with_notes(vec!["ridiculous!".to_owned()]),
195        }
196    }
197}
198