syntect_no_panic/
easy.rs

1//! API wrappers for common use cases like highlighting strings and
2//! files without caring about intermediate semantic representation
3//! and caching.
4
5use crate::highlighting::{HighlightIterator, HighlightState, Highlighter, Style, Theme};
6use crate::parsing::{ParseState, ScopeStack, ScopeStackOp, SyntaxReference, SyntaxSet};
7use crate::{Error, LoadingError};
8use std::fs::File;
9use std::io::BufReader;
10use std::path::Path;
11// use util::debug_print_ops;
12
13/// Simple way to go directly from lines of text to colored tokens.
14///
15/// Depending on how you load the syntaxes (see the [`SyntaxSet`] docs), this can either take
16/// strings with trailing `\n`s or without.
17///
18/// [`SyntaxSet`]: ../parsing/struct.SyntaxSet.html
19///
20/// # Examples
21///
22/// Prints colored lines of a string to the terminal
23///
24/// ```
25/// use syntect::easy::HighlightLines;
26/// use syntect::parsing::SyntaxSet;
27/// use syntect::highlighting::{ThemeSet, Style};
28/// use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
29///
30/// // Load these once at the start of your program
31/// let ps = SyntaxSet::load_defaults_newlines();
32/// let ts = ThemeSet::load_defaults();
33///
34/// let syntax = ps.find_syntax_by_extension("rs").unwrap();
35/// let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
36/// let s = "pub struct Wow { hi: u64 }\nfn blah() -> u64 {}";
37/// for line in LinesWithEndings::from(s) { // LinesWithEndings enables use of newlines mode
38///     let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
39///     let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
40///     print!("{}", escaped);
41/// }
42/// ```
43pub struct HighlightLines<'a> {
44    highlighter: Highlighter<'a>,
45    parse_state: ParseState,
46    highlight_state: HighlightState,
47}
48
49
50/// Options for highlighting operations
51#[derive(Debug, Clone, Copy, Default)]
52pub struct HighlightOptions {
53    /// If true, errors in parsing the syntax will have the minimal possible impact,
54    /// highlighting will proceed as if the error did not occur.
55    /// If false, errors will be propagated as errors.
56    pub ignore_errors: bool,
57}
58
59impl<'a> HighlightLines<'a> {
60    pub fn new(
61        syntax: &SyntaxReference,
62        theme: &'a Theme,
63        options: HighlightOptions,
64    ) -> HighlightLines<'a> {
65        let highlighter = Highlighter::new(theme);
66        let highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
67        HighlightLines {
68            highlighter,
69            parse_state: ParseState::new(syntax, options.ignore_errors),
70            highlight_state,
71        }
72    }
73
74    #[deprecated(
75        since = "5.0.0",
76        note = "Renamed to `highlight_line` to make it clear it should be passed a single line at a time"
77    )]
78    pub fn highlight<'b>(
79        &mut self,
80        line: &'b str,
81        syntax_set: &SyntaxSet,
82    ) -> Vec<(Style, &'b str)> {
83        self.highlight_line(line, syntax_set)
84            .expect("`highlight` is deprecated, use `highlight_line` instead")
85    }
86
87    /// Highlights a line of a file
88    pub fn highlight_line<'b>(
89        &mut self,
90        line: &'b str,
91        syntax_set: &SyntaxSet,
92    ) -> Result<Vec<(Style, &'b str)>, Error> {
93        // println!("{}", self.highlight_state.path);
94        let ops = self.parse_state.parse_line(line, syntax_set)?;
95        // use util::debug_print_ops;
96        // debug_print_ops(line, &ops);
97        let iter =
98            HighlightIterator::new(&mut self.highlight_state, &ops[..], line, &self.highlighter);
99        Ok(iter.collect())
100    }
101}
102
103/// Convenience struct containing everything you need to highlight a file
104///
105/// Use the `reader` to get the lines of the file and the `highlight_lines` to highlight them. See
106/// the [`new`] method docs for more information.
107///
108/// [`new`]: #method.new
109pub struct HighlightFile<'a> {
110    pub reader: BufReader<File>,
111    pub highlight_lines: HighlightLines<'a>,
112}
113
114impl<'a> HighlightFile<'a> {
115    /// Constructs a file reader and a line highlighter to get you reading files as fast as possible.
116    ///
117    /// This auto-detects the syntax from the extension and constructs a [`HighlightLines`] with the
118    /// correct syntax and theme.
119    ///
120    /// [`HighlightLines`]: struct.HighlightLines.html
121    ///
122    /// # Examples
123    ///
124    /// Using the `newlines` mode is a bit involved but yields more robust and glitch-free highlighting,
125    /// as well as being slightly faster since it can re-use a line buffer.
126    ///
127    /// ```
128    /// use syntect::parsing::SyntaxSet;
129    /// use syntect::highlighting::{ThemeSet, Style};
130    /// use syntect::util::as_24_bit_terminal_escaped;
131    /// use syntect::easy::HighlightFile;
132    /// use std::io::BufRead;
133    ///
134    /// # use std::io;
135    /// # fn foo() -> io::Result<()> {
136    /// let ss = SyntaxSet::load_defaults_newlines();
137    /// let ts = ThemeSet::load_defaults();
138    ///
139    /// let mut highlighter = HighlightFile::new("testdata/highlight_test.erb", &ss, &ts.themes["base16-ocean.dark"]).unwrap();
140    /// let mut line = String::new();
141    /// while highlighter.reader.read_line(&mut line)? > 0 {
142    ///     {
143    ///         let regions: Vec<(Style, &str)> = highlighter.highlight_lines.highlight_line(&line, &ss).unwrap();
144    ///         print!("{}", as_24_bit_terminal_escaped(&regions[..], true));
145    ///     } // until NLL this scope is needed so we can clear the buffer after
146    ///     line.clear(); // read_line appends so we need to clear between lines
147    /// }
148    /// # Ok(())
149    /// # }
150    /// ```
151    ///
152    /// This example uses `reader.lines()` to get lines without a newline character, it's simpler but may break on rare tricky cases.
153    ///
154    /// ```
155    /// use syntect::parsing::SyntaxSet;
156    /// use syntect::highlighting::{ThemeSet, Style};
157    /// use syntect::util::as_24_bit_terminal_escaped;
158    /// use syntect::easy::HighlightFile;
159    /// use std::io::BufRead;
160    ///
161    /// let ss = SyntaxSet::load_defaults_nonewlines();
162    /// let ts = ThemeSet::load_defaults();
163    ///
164    /// let mut highlighter = HighlightFile::new("testdata/highlight_test.erb", &ss, &ts.themes["base16-ocean.dark"]).unwrap();
165    /// for maybe_line in highlighter.reader.lines() {
166    ///     let line = maybe_line.unwrap();
167    ///     let regions: Vec<(Style, &str)> = highlighter.highlight_lines.highlight_line(&line, &ss).unwrap();
168    ///     println!("{}", as_24_bit_terminal_escaped(&regions[..], true));
169    /// }
170    /// ```
171    pub fn new<P: AsRef<Path>>(
172        path_obj: P,
173        ss: &SyntaxSet,
174        theme: &'a Theme,
175        options: HighlightOptions,
176    ) -> Result<HighlightFile<'a>, LoadingError> {
177        let path: &Path = path_obj.as_ref();
178        let f = File::open(path)?;
179        let syntax = ss
180            .find_syntax_for_file(path)?
181            .unwrap_or_else(|| ss.find_syntax_plain_text());
182
183        Ok(HighlightFile {
184            reader: BufReader::new(f),
185            highlight_lines: HighlightLines::new(syntax, theme, options),
186        })
187    }
188}
189
190/// Iterator over the ranges of a line which a given the operation from the parser applies.
191///
192/// Use [`ScopeRegionIterator`] to obtain directly regions (`&str`s) from the line.
193///
194/// To use, just keep your own [`ScopeStack`] and then `ScopeStack.apply(op)` the operation that is
195/// yielded at the top of your `for` loop over this iterator. Now you have a substring of the line
196/// and the scope stack for that token.
197///
198/// See the `synstats.rs` example for an example of using this iterator.
199///
200/// **Note:** This will often return empty ranges, just `continue` after applying the op if you
201/// don't want them.
202///
203/// [`ScopeStack`]: ../parsing/struct.ScopeStack.html
204/// [`ScopeRegionIterator`]: ./struct.ScopeRegionIterator.html
205#[derive(Debug)]
206pub struct ScopeRangeIterator<'a> {
207    ops: &'a [(usize, ScopeStackOp)],
208    line: &'a str,
209    index: usize,
210    last_str_index: usize,
211}
212
213impl<'a> ScopeRangeIterator<'a> {
214    pub fn new(ops: &'a [(usize, ScopeStackOp)], line: &'a str) -> ScopeRangeIterator<'a> {
215        ScopeRangeIterator {
216            ops,
217            line,
218            index: 0,
219            last_str_index: 0,
220        }
221    }
222}
223
224static NOOP_OP: ScopeStackOp = ScopeStackOp::Noop;
225
226impl<'a> Iterator for ScopeRangeIterator<'a> {
227    type Item = (std::ops::Range<usize>, &'a ScopeStackOp);
228    fn next(&mut self) -> Option<Self::Item> {
229        if self.index > self.ops.len() {
230            return None;
231        }
232
233        // region extends up to next operation (ops[index]) or string end if there is none
234        // note the next operation may be at, last_str_index, in which case the region is empty
235        let next_str_i = if self.index == self.ops.len() {
236            self.line.len()
237        } else {
238            self.ops[self.index].0
239        };
240        let range = self.last_str_index..next_str_i;
241        self.last_str_index = next_str_i;
242
243        // the first region covers everything before the first op, which may be empty
244        let op = if self.index == 0 {
245            &NOOP_OP
246        } else {
247            &self.ops[self.index - 1].1
248        };
249
250        self.index += 1;
251        Some((range, op))
252    }
253}
254
255/// A convenience wrapper over [`ScopeRangeIterator`] to return `&str`s directly.
256///
257/// To use, just keep your own [`ScopeStack`] and then `ScopeStack.apply(op)` the operation that is
258/// yielded at the top of your `for` loop over this iterator. Now you have a substring of the line
259/// and the scope stack for that token.
260///
261/// See the `synstats.rs` example for an example of using this iterator.
262///
263/// **Note:** This will often return empty regions, just `continue` after applying the op if you
264/// don't want them.
265///
266/// [`ScopeStack`]: ../parsing/struct.ScopeStack.html
267/// [`ScopeRangeIterator`]: ./struct.ScopeRangeIterator.html
268#[derive(Debug)]
269pub struct ScopeRegionIterator<'a> {
270    range_iter: ScopeRangeIterator<'a>,
271}
272
273impl<'a> ScopeRegionIterator<'a> {
274    pub fn new(ops: &'a [(usize, ScopeStackOp)], line: &'a str) -> ScopeRegionIterator<'a> {
275        ScopeRegionIterator {
276            range_iter: ScopeRangeIterator::new(ops, line),
277        }
278    }
279}
280
281impl<'a> Iterator for ScopeRegionIterator<'a> {
282    type Item = (&'a str, &'a ScopeStackOp);
283    fn next(&mut self) -> Option<Self::Item> {
284        let (range, op) = self.range_iter.next()?;
285        Some((&self.range_iter.line[range], op))
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    #[cfg(feature = "default-themes")]
293    use crate::highlighting::ThemeSet;
294    use crate::parsing::{ParseState, ScopeStack, SyntaxSet};
295    use std::str::FromStr;
296
297    #[cfg(all(feature = "default-syntaxes", feature = "default-themes"))]
298    #[test]
299    fn can_highlight_lines() {
300        let ss = SyntaxSet::load_defaults_nonewlines();
301        let ts = ThemeSet::load_defaults();
302        let syntax = ss.find_syntax_by_extension("rs").unwrap();
303        let mut h = HighlightLines::new(
304            syntax,
305            &ts.themes["base16-ocean.dark"],
306            HighlightOptions::default(),
307        );
308        let ranges = h
309            .highlight_line("pub struct Wow { hi: u64 }", &ss)
310            .expect("#[cfg(test)]");
311        assert!(ranges.len() > 4);
312    }
313
314    #[cfg(all(feature = "default-syntaxes", feature = "default-themes"))]
315    #[test]
316    fn can_highlight_file() {
317        let ss = SyntaxSet::load_defaults_nonewlines();
318        let ts = ThemeSet::load_defaults();
319        HighlightFile::new(
320            "testdata/highlight_test.erb",
321            &ss,
322            &ts.themes["base16-ocean.dark"],
323            HighlightOptions::default(),
324        )
325        .unwrap();
326    }
327
328    #[cfg(feature = "default-syntaxes")]
329    #[test]
330    fn can_find_regions() {
331        let ss = SyntaxSet::load_defaults_nonewlines();
332        let mut state = ParseState::new(ss.find_syntax_by_extension("rb").unwrap(), false);
333        let line = "lol =5+2";
334        let ops = state.parse_line(line, &ss).expect("#[cfg(test)]");
335
336        let mut stack = ScopeStack::new();
337        let mut token_count = 0;
338        for (s, op) in ScopeRegionIterator::new(&ops, line) {
339            stack.apply(op).expect("#[cfg(test)]");
340            if s.is_empty() {
341                // in this case we don't care about blank tokens
342                continue;
343            }
344            if token_count == 1 {
345                assert_eq!(
346                    stack,
347                    ScopeStack::from_str("source.ruby keyword.operator.assignment.ruby").unwrap()
348                );
349                assert_eq!(s, "=");
350            }
351            token_count += 1;
352            println!("{:?} {}", s, stack);
353        }
354        assert_eq!(token_count, 5);
355    }
356
357    #[cfg(feature = "default-syntaxes")]
358    #[test]
359    fn can_find_regions_with_trailing_newline() {
360        let ss = SyntaxSet::load_defaults_newlines();
361        let mut state = ParseState::new(ss.find_syntax_by_extension("rb").unwrap(), false);
362        let lines = ["# hello world\n", "lol=5+2\n"];
363        let mut stack = ScopeStack::new();
364
365        for line in lines.iter() {
366            let ops = state.parse_line(line, &ss).expect("#[cfg(test)]");
367            println!("{:?}", ops);
368
369            let mut iterated_ops: Vec<&ScopeStackOp> = Vec::new();
370            for (_, op) in ScopeRegionIterator::new(&ops, line) {
371                stack.apply(op).expect("#[cfg(test)]");
372                iterated_ops.push(op);
373                println!("{:?}", op);
374            }
375
376            let all_ops = ops.iter().map(|t| &t.1);
377            assert_eq!(all_ops.count(), iterated_ops.len() - 1); // -1 because we want to ignore the NOOP
378        }
379    }
380}