syntect/
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;
8use std::fs::File;
9use std::io::{self, 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
49impl<'a> HighlightLines<'a> {
50    pub fn new(syntax: &SyntaxReference, theme: &'a Theme) -> HighlightLines<'a> {
51        let highlighter = Highlighter::new(theme);
52        let highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
53        HighlightLines {
54            highlighter,
55            parse_state: ParseState::new(syntax),
56            highlight_state,
57        }
58    }
59
60    #[deprecated(
61        since = "5.0.0",
62        note = "Renamed to `highlight_line` to make it clear it should be passed a single line at a time"
63    )]
64    pub fn highlight<'b>(
65        &mut self,
66        line: &'b str,
67        syntax_set: &SyntaxSet,
68    ) -> Vec<(Style, &'b str)> {
69        self.highlight_line(line, syntax_set)
70            .expect("`highlight` is deprecated, use `highlight_line` instead")
71    }
72
73    /// Highlights a line of a file
74    pub fn highlight_line<'b>(
75        &mut self,
76        line: &'b str,
77        syntax_set: &SyntaxSet,
78    ) -> Result<Vec<(Style, &'b str)>, Error> {
79        // println!("{}", self.highlight_state.path);
80        let ops = self.parse_state.parse_line(line, syntax_set)?;
81        // use util::debug_print_ops;
82        // debug_print_ops(line, &ops);
83        let iter =
84            HighlightIterator::new(&mut self.highlight_state, &ops[..], line, &self.highlighter);
85        Ok(iter.collect())
86    }
87
88    /// This starts again from a previous state, useful for highlighting a file incrementally for
89    /// which you've cached the highlight and parse state.
90    pub fn from_state(
91        theme: &'a Theme,
92        highlight_state: HighlightState,
93        parse_state: ParseState,
94    ) -> HighlightLines<'a> {
95        HighlightLines {
96            highlighter: Highlighter::new(theme),
97            parse_state,
98            highlight_state,
99        }
100    }
101
102    /// Returns the current highlight and parse states, useful for caching and incremental highlighting.
103    pub fn state(self) -> (HighlightState, ParseState) {
104        (self.highlight_state, self.parse_state)
105    }
106}
107
108/// Convenience struct containing everything you need to highlight a file
109///
110/// Use the `reader` to get the lines of the file and the `highlight_lines` to highlight them. See
111/// the [`new`] method docs for more information.
112///
113/// [`new`]: #method.new
114pub struct HighlightFile<'a> {
115    pub reader: BufReader<File>,
116    pub highlight_lines: HighlightLines<'a>,
117}
118
119impl<'a> HighlightFile<'a> {
120    /// Constructs a file reader and a line highlighter to get you reading files as fast as possible.
121    ///
122    /// This auto-detects the syntax from the extension and constructs a [`HighlightLines`] with the
123    /// correct syntax and theme.
124    ///
125    /// [`HighlightLines`]: struct.HighlightLines.html
126    ///
127    /// # Examples
128    ///
129    /// Using the `newlines` mode is a bit involved but yields more robust and glitch-free highlighting,
130    /// as well as being slightly faster since it can re-use a line buffer.
131    ///
132    /// ```
133    /// use syntect::parsing::SyntaxSet;
134    /// use syntect::highlighting::{ThemeSet, Style};
135    /// use syntect::util::as_24_bit_terminal_escaped;
136    /// use syntect::easy::HighlightFile;
137    /// use std::io::BufRead;
138    ///
139    /// # use std::io;
140    /// # fn foo() -> io::Result<()> {
141    /// let ss = SyntaxSet::load_defaults_newlines();
142    /// let ts = ThemeSet::load_defaults();
143    ///
144    /// let mut highlighter = HighlightFile::new("testdata/highlight_test.erb", &ss, &ts.themes["base16-ocean.dark"]).unwrap();
145    /// let mut line = String::new();
146    /// while highlighter.reader.read_line(&mut line)? > 0 {
147    ///     {
148    ///         let regions: Vec<(Style, &str)> = highlighter.highlight_lines.highlight_line(&line, &ss).unwrap();
149    ///         print!("{}", as_24_bit_terminal_escaped(&regions[..], true));
150    ///     } // until NLL this scope is needed so we can clear the buffer after
151    ///     line.clear(); // read_line appends so we need to clear between lines
152    /// }
153    /// # Ok(())
154    /// # }
155    /// ```
156    ///
157    /// This example uses `reader.lines()` to get lines without a newline character, it's simpler but may break on rare tricky cases.
158    ///
159    /// ```
160    /// use syntect::parsing::SyntaxSet;
161    /// use syntect::highlighting::{ThemeSet, Style};
162    /// use syntect::util::as_24_bit_terminal_escaped;
163    /// use syntect::easy::HighlightFile;
164    /// use std::io::BufRead;
165    ///
166    /// let ss = SyntaxSet::load_defaults_nonewlines();
167    /// let ts = ThemeSet::load_defaults();
168    ///
169    /// let mut highlighter = HighlightFile::new("testdata/highlight_test.erb", &ss, &ts.themes["base16-ocean.dark"]).unwrap();
170    /// for maybe_line in highlighter.reader.lines() {
171    ///     let line = maybe_line.unwrap();
172    ///     let regions: Vec<(Style, &str)> = highlighter.highlight_lines.highlight_line(&line, &ss).unwrap();
173    ///     println!("{}", as_24_bit_terminal_escaped(&regions[..], true));
174    /// }
175    /// ```
176    pub fn new<P: AsRef<Path>>(
177        path_obj: P,
178        ss: &SyntaxSet,
179        theme: &'a Theme,
180    ) -> io::Result<HighlightFile<'a>> {
181        let path: &Path = path_obj.as_ref();
182        let f = File::open(path)?;
183        let syntax = ss
184            .find_syntax_for_file(path)?
185            .unwrap_or_else(|| ss.find_syntax_plain_text());
186
187        Ok(HighlightFile {
188            reader: BufReader::new(f),
189            highlight_lines: HighlightLines::new(syntax, theme),
190        })
191    }
192}
193
194/// Iterator over the ranges of a line which a given the operation from the parser applies.
195///
196/// Use [`ScopeRegionIterator`] to obtain directly regions (`&str`s) from the line.
197///
198/// To use, just keep your own [`ScopeStack`] and then `ScopeStack.apply(op)` the operation that is
199/// yielded at the top of your `for` loop over this iterator. Now you have a substring of the line
200/// and the scope stack for that token.
201///
202/// See the `synstats.rs` example for an example of using this iterator.
203///
204/// **Note:** This will often return empty ranges, just `continue` after applying the op if you
205/// don't want them.
206///
207/// [`ScopeStack`]: ../parsing/struct.ScopeStack.html
208/// [`ScopeRegionIterator`]: ./struct.ScopeRegionIterator.html
209#[derive(Debug)]
210pub struct ScopeRangeIterator<'a> {
211    ops: &'a [(usize, ScopeStackOp)],
212    line: &'a str,
213    index: usize,
214    last_str_index: usize,
215}
216
217impl<'a> ScopeRangeIterator<'a> {
218    pub fn new(ops: &'a [(usize, ScopeStackOp)], line: &'a str) -> ScopeRangeIterator<'a> {
219        ScopeRangeIterator {
220            ops,
221            line,
222            index: 0,
223            last_str_index: 0,
224        }
225    }
226}
227
228static NOOP_OP: ScopeStackOp = ScopeStackOp::Noop;
229
230impl<'a> Iterator for ScopeRangeIterator<'a> {
231    type Item = (std::ops::Range<usize>, &'a ScopeStackOp);
232    fn next(&mut self) -> Option<Self::Item> {
233        if self.index > self.ops.len() {
234            return None;
235        }
236
237        // region extends up to next operation (ops[index]) or string end if there is none
238        // note the next operation may be at, last_str_index, in which case the region is empty
239        let next_str_i = if self.index == self.ops.len() {
240            self.line.len()
241        } else {
242            self.ops[self.index].0
243        };
244        let range = self.last_str_index..next_str_i;
245        self.last_str_index = next_str_i;
246
247        // the first region covers everything before the first op, which may be empty
248        let op = if self.index == 0 {
249            &NOOP_OP
250        } else {
251            &self.ops[self.index - 1].1
252        };
253
254        self.index += 1;
255        Some((range, op))
256    }
257}
258
259/// A convenience wrapper over [`ScopeRangeIterator`] to return `&str`s directly.
260///
261/// To use, just keep your own [`ScopeStack`] and then `ScopeStack.apply(op)` the operation that is
262/// yielded at the top of your `for` loop over this iterator. Now you have a substring of the line
263/// and the scope stack for that token.
264///
265/// See the `synstats.rs` example for an example of using this iterator.
266///
267/// **Note:** This will often return empty regions, just `continue` after applying the op if you
268/// don't want them.
269///
270/// [`ScopeStack`]: ../parsing/struct.ScopeStack.html
271/// [`ScopeRangeIterator`]: ./struct.ScopeRangeIterator.html
272#[derive(Debug)]
273pub struct ScopeRegionIterator<'a> {
274    range_iter: ScopeRangeIterator<'a>,
275}
276
277impl<'a> ScopeRegionIterator<'a> {
278    pub fn new(ops: &'a [(usize, ScopeStackOp)], line: &'a str) -> ScopeRegionIterator<'a> {
279        ScopeRegionIterator {
280            range_iter: ScopeRangeIterator::new(ops, line),
281        }
282    }
283}
284
285impl<'a> Iterator for ScopeRegionIterator<'a> {
286    type Item = (&'a str, &'a ScopeStackOp);
287    fn next(&mut self) -> Option<Self::Item> {
288        let (range, op) = self.range_iter.next()?;
289        Some((&self.range_iter.line[range], op))
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    #[cfg(feature = "default-themes")]
297    use crate::highlighting::ThemeSet;
298    use crate::parsing::{ParseState, ScopeStack, SyntaxSet};
299    use std::str::FromStr;
300
301    #[cfg(all(feature = "default-syntaxes", feature = "default-themes"))]
302    #[test]
303    fn can_highlight_lines() {
304        let ss = SyntaxSet::load_defaults_nonewlines();
305        let ts = ThemeSet::load_defaults();
306        let syntax = ss.find_syntax_by_extension("rs").unwrap();
307        let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
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        )
324        .unwrap();
325    }
326
327    #[cfg(feature = "default-syntaxes")]
328    #[test]
329    fn can_find_regions() {
330        let ss = SyntaxSet::load_defaults_nonewlines();
331        let mut state = ParseState::new(ss.find_syntax_by_extension("rb").unwrap());
332        let line = "lol =5+2";
333        let ops = state.parse_line(line, &ss).expect("#[cfg(test)]");
334
335        let mut stack = ScopeStack::new();
336        let mut token_count = 0;
337        for (s, op) in ScopeRegionIterator::new(&ops, line) {
338            stack.apply(op).expect("#[cfg(test)]");
339            if s.is_empty() {
340                // in this case we don't care about blank tokens
341                continue;
342            }
343            if token_count == 1 {
344                assert_eq!(
345                    stack,
346                    ScopeStack::from_str("source.ruby keyword.operator.assignment.ruby").unwrap()
347                );
348                assert_eq!(s, "=");
349            }
350            token_count += 1;
351            println!("{:?} {}", s, stack);
352        }
353        assert_eq!(token_count, 5);
354    }
355
356    #[cfg(feature = "default-syntaxes")]
357    #[test]
358    fn can_find_regions_with_trailing_newline() {
359        let ss = SyntaxSet::load_defaults_newlines();
360        let mut state = ParseState::new(ss.find_syntax_by_extension("rb").unwrap());
361        let lines = ["# hello world\n", "lol=5+2\n"];
362        let mut stack = ScopeStack::new();
363
364        for line in lines.iter() {
365            let ops = state.parse_line(line, &ss).expect("#[cfg(test)]");
366            println!("{:?}", ops);
367
368            let mut iterated_ops: Vec<&ScopeStackOp> = Vec::new();
369            for (_, op) in ScopeRegionIterator::new(&ops, line) {
370                stack.apply(op).expect("#[cfg(test)]");
371                iterated_ops.push(op);
372                println!("{:?}", op);
373            }
374
375            let all_ops = ops.iter().map(|t| &t.1);
376            assert_eq!(all_ops.count(), iterated_ops.len() - 1); // -1 because we want to ignore the NOOP
377        }
378    }
379
380    #[cfg(all(feature = "default-syntaxes", feature = "default-themes"))]
381    #[test]
382    fn can_start_again_from_previous_state() {
383        let ss = SyntaxSet::load_defaults_nonewlines();
384        let ts = ThemeSet::load_defaults();
385        let mut highlighter = HighlightLines::new(
386            ss.find_syntax_by_extension("py").unwrap(),
387            &ts.themes["base16-ocean.dark"],
388        );
389
390        let lines = ["\"\"\"", "def foo():", "\"\"\""];
391
392        let highlighted_first_line = highlighter
393            .highlight_line(lines[0], &ss)
394            .expect("#[cfg(test)]");
395
396        let (highlight_state, parse_state) = highlighter.state();
397
398        let mut other_highlighter = HighlightLines::from_state(
399            &ts.themes["base16-ocean.dark"],
400            highlight_state,
401            parse_state,
402        );
403
404        let highlighted_second_line = other_highlighter
405            .highlight_line(lines[1], &ss)
406            .expect("#[cfg(test)]");
407
408        // Check that the second line is highlighted correctly (i.e. as a docstring)
409        // using the first line's previous state
410        assert!(highlighted_second_line.len() == 1);
411        assert!(highlighted_second_line[0].0 == highlighted_first_line[0].0);
412    }
413}