television_utils/
syntax.rs

1use bat::assets::HighlightingAssets;
2use color_eyre::Result;
3use gag::Gag;
4use std::path::{Path, PathBuf};
5use syntect::easy::HighlightLines;
6use syntect::highlighting::{
7    HighlightIterator, HighlightState, Highlighter, Style, Theme,
8};
9use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
10use tracing::warn;
11
12#[allow(dead_code)]
13#[derive(Debug, Clone)]
14pub struct HighlightingState {
15    parse_state: ParseState,
16    highlight_state: HighlightState,
17}
18
19impl HighlightingState {
20    pub fn new(
21        parse_state: ParseState,
22        highlight_state: HighlightState,
23    ) -> Self {
24        Self {
25            parse_state,
26            highlight_state,
27        }
28    }
29}
30
31struct LineHighlighter<'a> {
32    highlighter: Highlighter<'a>,
33    pub parse_state: ParseState,
34    pub highlight_state: HighlightState,
35}
36
37impl<'a> LineHighlighter<'a> {
38    pub fn new(
39        syntax: &SyntaxReference,
40        theme: &'a Theme,
41    ) -> LineHighlighter<'a> {
42        let highlighter = Highlighter::new(theme);
43        let highlight_state =
44            HighlightState::new(&highlighter, ScopeStack::new());
45        Self {
46            highlighter,
47            parse_state: ParseState::new(syntax),
48            highlight_state,
49        }
50    }
51
52    #[allow(dead_code)]
53    pub fn from_state(
54        state: HighlightingState,
55        theme: &'a Theme,
56    ) -> LineHighlighter<'a> {
57        Self {
58            highlighter: Highlighter::new(theme),
59            parse_state: state.parse_state,
60            highlight_state: state.highlight_state,
61        }
62    }
63
64    /// Highlights a line of a file
65    pub fn highlight_line<'b>(
66        &mut self,
67        line: &'b str,
68        syntax_set: &SyntaxSet,
69    ) -> Result<Vec<(Style, &'b str)>, syntect::Error> {
70        let ops = self.parse_state.parse_line(line, syntax_set)?;
71        let iter = HighlightIterator::new(
72            &mut self.highlight_state,
73            &ops[..],
74            line,
75            &self.highlighter,
76        );
77        Ok(iter.collect())
78    }
79}
80
81#[deprecated(
82    note = "Use `compute_highlights_incremental` instead, which also returns the state"
83)]
84pub fn compute_highlights_for_path(
85    file_path: &Path,
86    lines: &[String],
87    syntax_set: &SyntaxSet,
88    syntax_theme: &Theme,
89) -> Result<Vec<Vec<(Style, String)>>> {
90    let syntax = set_syntax_set(syntax_set, file_path);
91    let mut highlighter = HighlightLines::new(syntax, syntax_theme);
92    let mut highlighted_lines = Vec::new();
93    for line in lines {
94        let hl_regions = highlighter.highlight_line(line, syntax_set)?;
95        highlighted_lines.push(
96            hl_regions
97                .iter()
98                .map(|(style, text)| (*style, (*text).to_string()))
99                .collect(),
100        );
101    }
102    Ok(highlighted_lines)
103}
104
105fn set_syntax_set<'a>(
106    syntax_set: &'a SyntaxSet,
107    file_path: &Path,
108) -> &'a SyntaxReference {
109    syntax_set
110        .find_syntax_for_file(file_path)
111        .unwrap_or(None)
112        .unwrap_or_else(|| {
113            warn!(
114                "No syntax found for {:?}, defaulting to plain text",
115                file_path
116            );
117            syntax_set.find_syntax_plain_text()
118        })
119}
120
121#[derive(Debug, Clone)]
122pub struct HighlightedLines {
123    pub lines: Vec<Vec<(Style, String)>>,
124    //pub state: Option<HighlightingState>,
125}
126
127impl HighlightedLines {
128    pub fn new(
129        lines: Vec<Vec<(Style, String)>>,
130        _state: &Option<HighlightingState>,
131    ) -> Self {
132        Self { lines, /*state*/ }
133    }
134}
135
136pub fn compute_highlights_incremental(
137    file_path: &Path,
138    lines: &[String],
139    syntax_set: &SyntaxSet,
140    syntax_theme: &Theme,
141    _cached_lines: &Option<HighlightedLines>,
142) -> Result<HighlightedLines> {
143    let mut highlighted_lines: Vec<_>;
144    let mut highlighter: LineHighlighter;
145
146    //if let Some(HighlightedLines {
147    //    lines: c_lines,
148    //    state: Some(s),
149    //}) = cached_lines
150    //{
151    //    highlighter = LineHighlighter::from_state(s, syntax_theme);
152    //    highlighted_lines = c_lines;
153    //} else {
154    //    let syntax = set_syntax_set(syntax_set, file_path);
155    //    highlighter = LineHighlighter::new(syntax, syntax_theme);
156    //    highlighted_lines = Vec::new();
157    //};
158    let syntax = set_syntax_set(syntax_set, file_path);
159    highlighter = LineHighlighter::new(syntax, syntax_theme);
160    highlighted_lines = Vec::with_capacity(lines.len());
161
162    for line in lines {
163        let hl_regions = highlighter.highlight_line(line, syntax_set)?;
164        highlighted_lines.push(
165            hl_regions
166                .iter()
167                .map(|(style, text)| (*style, (*text).to_string()))
168                .collect(),
169        );
170    }
171
172    Ok(HighlightedLines::new(
173        highlighted_lines,
174        &Some(HighlightingState::new(
175            highlighter.parse_state.clone(),
176            highlighter.highlight_state.clone(),
177        )),
178    ))
179}
180
181#[allow(dead_code)]
182pub fn compute_highlights_for_line<'a>(
183    line: &'a str,
184    syntax_set: &SyntaxSet,
185    syntax_theme: &Theme,
186    file_path: &str,
187) -> Result<Vec<(Style, &'a str)>> {
188    let syntax = syntax_set.find_syntax_for_file(file_path)?;
189    match syntax {
190        None => {
191            warn!(
192                "No syntax found for path {:?}, defaulting to plain text",
193                file_path
194            );
195            Ok(vec![(Style::default(), line)])
196        }
197        Some(syntax) => {
198            let mut highlighter = HighlightLines::new(syntax, syntax_theme);
199            Ok(highlighter.highlight_line(line, syntax_set)?)
200        }
201    }
202}
203
204// Based on code from https://github.com/sharkdp/bat e981e974076a926a38f124b7d8746de2ca5f0a28
205//
206// Copyright (c) 2018-2023 bat-developers (https://github.com/sharkdp/bat).
207//
208// Permission is hereby granted, free of charge, to any person obtaining a copy
209// of this software and associated documentation files (the "Software"), to deal
210// in the Software without restriction, including without limitation the rights
211// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
212// copies of the Software, and to permit persons to whom the Software is
213// furnished to do so, subject to the following conditions:
214//
215// The above copyright notice and this permission notice shall be included in all
216// copies or substantial portions of the Software.
217//
218// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
219// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
220// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
221// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
222// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
223// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
224// SOFTWARE.
225
226use directories::BaseDirs;
227use lazy_static::lazy_static;
228
229#[cfg(target_os = "macos")]
230use std::env;
231
232/// Wrapper for 'dirs' that treats `MacOS` more like `Linux`, by following the XDG specification.
233///
234/// This means that the `XDG_CACHE_HOME` and `XDG_CONFIG_HOME` environment variables are
235/// checked first. The fallback directories are `~/.cache/bat` and `~/.config/bat`, respectively.
236pub struct BatProjectDirs {
237    cache_dir: PathBuf,
238}
239
240impl BatProjectDirs {
241    fn new() -> Option<BatProjectDirs> {
242        #[cfg(target_os = "macos")]
243        let cache_dir_op = env::var_os("XDG_CACHE_HOME")
244            .map(PathBuf::from)
245            .filter(|p| p.is_absolute())
246            .or_else(|| BaseDirs::new().map(|d| d.home_dir().join(".cache")));
247
248        #[cfg(not(target_os = "macos"))]
249        let cache_dir_op = BaseDirs::new().map(|d| d.cache_dir().to_owned());
250
251        let cache_dir = cache_dir_op.map(|d| d.join("bat"))?;
252
253        Some(BatProjectDirs { cache_dir })
254    }
255
256    pub fn cache_dir(&self) -> &Path {
257        &self.cache_dir
258    }
259}
260
261lazy_static! {
262    pub static ref PROJECT_DIRS: BatProjectDirs = BatProjectDirs::new()
263        .unwrap_or_else(|| panic!("Could not get home directory"));
264}
265
266pub fn load_highlighting_assets() -> HighlightingAssets {
267    HighlightingAssets::from_cache(PROJECT_DIRS.cache_dir())
268        .unwrap_or_else(|_| HighlightingAssets::from_binary())
269}
270
271pub trait HighlightingAssetsExt {
272    fn get_theme_no_output(&self, theme_name: &str) -> &Theme;
273}
274
275impl HighlightingAssetsExt for HighlightingAssets {
276    /// Get a theme by name. If the theme is not found, the default theme is returned.
277    ///
278    /// This is an ugly hack to work around the fact that bat actually prints a warning
279    /// to stderr when a theme is not found which might mess up the TUI. This function
280    /// suppresses that warning by temporarily redirecting stderr and stdout.
281    fn get_theme_no_output(&self, theme_name: &str) -> &Theme {
282        let _e = Gag::stderr().unwrap();
283        let _o = Gag::stdout().unwrap();
284        let theme = self.get_theme(theme_name);
285        theme
286    }
287}