From 90fe50c152853e569e6af288041f909694ed7902 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Tue, 26 Sep 2023 10:29:05 +0200 Subject: [PATCH] cli: Implement pretty diffs with highlighting Implements a new kind of diff output that includes syntax highlighting. This type of diff will be used for code review. --- Cargo.lock | 144 ++++++++++ radicle-cli/Cargo.toml | 13 + radicle-cli/src/git.rs | 2 +- radicle-cli/src/git/pretty_diff.rs | 377 ++++++++++++++++++++++++++ radicle-cli/src/git/unified_diff.rs | 7 +- radicle-cli/src/terminal.rs | 1 + radicle-cli/src/terminal/highlight.rs | 327 ++++++++++++++++++++++ 7 files changed, 869 insertions(+), 2 deletions(-) create mode 100644 radicle-cli/src/git/pretty_diff.rs create mode 100644 radicle-cli/src/terminal/highlight.rs diff --git a/Cargo.lock b/Cargo.lock index 63d18012..edc108dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1828,6 +1828,19 @@ dependencies = [ "tempfile", "thiserror", "timeago", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-c", + "tree-sitter-css", + "tree-sitter-go", + "tree-sitter-highlight", + "tree-sitter-html", + "tree-sitter-md", + "tree-sitter-python", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-toml", + "tree-sitter-typescript", "ureq", "zeroize", ] @@ -2921,6 +2934,137 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree-sitter" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-bash" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "096f57b3b44c04bfc7b21a4da44bfa16adf1f88aba18993b8478a091076d0968" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-c" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-css" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44fce8f9b603fef51e6384e2771ec5448bca1b71f1aa4ee2717a1803f9b279b" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-go" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad6d11f19441b961af2fda7f12f5d0dac325f6d6de83836a1d3750018cc5114" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-highlight" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042342584c5a7a0b833d9fc4e2bdab3f9868ddc6c4b339a1e01451c6720868bc" +dependencies = [ + "regex", + "thiserror", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-html" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "184e6b77953a354303dc87bf5fe36558c83569ce92606e7b382a0dc1b7443443" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-md" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a237fa10f6b466b76c783c79b08cc172581e547ef1dbb6ddf1f8b4e230157e1" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-python" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-ruby" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac30cbb1560363ae76e1ccde543d6d99087421e228cc47afcec004b86bb711a" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-toml" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca517f578a98b23d20780247cc2688407fa81effad5b627a5a364ec3339b53e8" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079c695c32d39ad089101c66393aeaca30e967fba3486a91f573d2f0e12d290a" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "try-lock" version = "0.2.4" diff --git a/radicle-cli/Cargo.toml b/radicle-cli/Cargo.toml index f5657837..b00b16b6 100644 --- a/radicle-cli/Cargo.toml +++ b/radicle-cli/Cargo.toml @@ -29,6 +29,19 @@ serde_yaml = { version = "0.8" } similar = { version = "2.2.1" } thiserror = { version = "1" } timeago = { version = "0.3", default-features = false } +tree-sitter = { version = "0.20.0" } +tree-sitter-highlight = { version = "0.20" } +tree-sitter-rust = { version = "0.20" } +tree-sitter-typescript = { version = "0.20" } +tree-sitter-html = { version = "0.19" } +tree-sitter-css = { version = "0.19" } +tree-sitter-toml = { version = "0.20" } +tree-sitter-c = { version = "0.20" } +tree-sitter-python = { version = "0.20" } +tree-sitter-ruby = { version = "0.20" } +tree-sitter-bash = { version = "0.20" } +tree-sitter-go = { version = "0.20.0" } +tree-sitter-md = { version = "0.1.5" } ureq = { version = "2.6.1", default-features = false, features = ["json"] } zeroize = { version = "1.1" } diff --git a/radicle-cli/src/git.rs b/radicle-cli/src/git.rs index 8d36db6d..96dffc19 100644 --- a/radicle-cli/src/git.rs +++ b/radicle-cli/src/git.rs @@ -1,7 +1,7 @@ //! Git-related functions and types. pub mod ddiff; -#[path = "git/unified_diff.rs"] +pub mod pretty_diff; pub mod unified_diff; use std::collections::HashSet; diff --git a/radicle-cli/src/git/pretty_diff.rs b/radicle-cli/src/git/pretty_diff.rs new file mode 100644 index 00000000..e5d5d0a4 --- /dev/null +++ b/radicle-cli/src/git/pretty_diff.rs @@ -0,0 +1,377 @@ +use radicle::git; +use radicle_surf::diff; +use radicle_surf::diff::{Diff, DiffContent, FileDiff, Hunk, Modification}; +use radicle_term as term; +use term::cell::Cell; + +use crate::git::unified_diff::FileHeader; +use crate::terminal::highlight::{Highlighter, Theme}; + +use super::unified_diff::{Decode, HunkHeader}; + +/// A repository of Git blobs. +pub trait Repo { + /// Lookup a blob from the repo. + fn blob(&self, oid: git::Oid) -> Result; +} + +impl Repo for git::raw::Repository { + fn blob(&self, oid: git::Oid) -> Result { + self.find_blob(*oid) + } +} + +/// Blobs passed down to the hunk renderer. +#[derive(Debug, Default)] +pub struct Blobs { + old: Option>, + new: Option>, +} + +/// Types that can be rendered as pretty diffs. +pub trait ToPretty { + /// The output of the render process. + type Output: term::Element; + /// Context that can be passed down from parent objects during rendering. + type Context; + + /// Render to pretty diff output. + fn pretty( + &self, + hi: &mut Highlighter, + context: &Self::Context, + repo: &R, + ) -> Self::Output; +} + +impl ToPretty for Diff { + type Output = term::VStack<'static>; + type Context = (); + + fn pretty( + &self, + hi: &mut Highlighter, + context: &Self::Context, + repo: &R, + ) -> Self::Output { + term::VStack::default() + .padding(0) + .children(self.files().map(|f| f.pretty(hi, context, repo).boxed())) + } +} + +impl ToPretty for FileHeader { + type Output = term::Line; + type Context = (); + + fn pretty( + &self, + _hi: &mut Highlighter, + _context: &Self::Context, + _repo: &R, + ) -> Self::Output { + match self { + FileHeader::Added { path, .. } => term::Line::new(path.display().to_string()), + FileHeader::Moved { new_path, .. } => term::Line::new(new_path.display().to_string()), + FileHeader::Deleted { path, .. } => term::Line::new(path.display().to_string()), + FileHeader::Modified { path, .. } => term::Line::new(path.display().to_string()), + FileHeader::Copied { new_path, .. } => term::Line::new(new_path.display().to_string()), + } + } +} + +impl ToPretty for FileDiff { + type Output = term::VStack<'static>; + type Context = (); + + fn pretty( + &self, + hi: &mut Highlighter, + _context: &Self::Context, + repo: &R, + ) -> Self::Output { + let content = match self { + FileDiff::Added(f) => f.diff.pretty(hi, self, repo), + FileDiff::Moved(f) => f.diff.pretty(hi, self, repo), + FileDiff::Deleted(f) => f.diff.pretty(hi, self, repo), + FileDiff::Modified(f) => f.diff.pretty(hi, self, repo), + FileDiff::Copied(f) => f.diff.pretty(hi, self, repo), + }; + term::VStack::default() + .padding(0) + .child(content) + .child(term::Line::blank()) + } +} + +impl ToPretty for DiffContent { + type Output = term::VStack<'static>; + type Context = FileDiff; + + fn pretty( + &self, + hi: &mut Highlighter, + context: &Self::Context, + repo: &R, + ) -> Self::Output { + let header = FileHeader::from(context); + let theme = Theme::default(); + + let (old, new) = match context { + FileDiff::Added(f) => (None, Some(f.new.oid)), + FileDiff::Moved(f) => (Some(f.old.oid), Some(f.new.oid)), + FileDiff::Deleted(f) => (Some(f.old.oid), None), + FileDiff::Modified(f) => (Some(f.old.oid), Some(f.new.oid)), + FileDiff::Copied(_) => { + // This is due to not having `oid`s for copied files yet. + unimplemented!("DiffContent::pretty: copied files are not supported in diffs") + } + }; + + let mut header = header.pretty(hi, &(), repo); + let mut additions = 0; + let mut deletions = 0; + + match self { + DiffContent::Plain { hunks, .. } => { + for h in hunks.iter() { + for l in &h.lines { + match l { + Modification::Addition(_) => additions += 1, + Modification::Deletion(_) => deletions += 1, + _ => {} + } + } + } + } + DiffContent::Empty => {} + DiffContent::Binary => {} + } + if deletions > 0 { + header.push(term::label(format!(" -{deletions}")).fg(theme.color("negative.light"))); + } + if additions > 0 { + header.push(term::label(format!(" +{additions}")).fg(theme.color("positive.light"))); + } + + let old = old.and_then(|oid| repo.blob(oid).ok()); + let new = new.and_then(|oid| repo.blob(oid).ok()); + let mut blobs = Blobs::default(); + + if let Some(blob) = old { + if !blob.is_binary() { + blobs.old = hi.highlight(context.path(), blob.content()).ok().flatten(); + } + } + if let Some(blob) = new { + if !blob.is_binary() { + blobs.new = hi.highlight(context.path(), blob.content()).ok().flatten(); + } + } + let mut vstack = term::VStack::default() + .border(Some(term::colors::FAINT)) + .padding(0) + .child(term::Line::new(term::Label::space()).extend(header)) + .divider(); + + match self { + DiffContent::Plain { hunks, .. } => { + for (i, h) in hunks.iter().enumerate() { + vstack.push(h.pretty(hi, &blobs, repo)); + if i != hunks.0.len() - 1 { + vstack = vstack.divider(); + } + } + } + DiffContent::Empty => { + vstack.push(term::Line::new(term::format::italic("Empty file"))); + } + DiffContent::Binary => { + vstack.push(term::Line::new(term::format::italic("Binary file"))); + } + } + vstack + } +} + +impl ToPretty for HunkHeader { + type Output = term::Line; + type Context = (); + + fn pretty( + &self, + _hi: &mut Highlighter, + _context: &Self::Context, + _repo: &R, + ) -> Self::Output { + term::Line::spaced([ + term::label(format!( + "@@ -{},{} +{},{} @@", + self.old_line_no, self.old_size, self.new_line_no, self.new_size, + )) + .fg(term::colors::fixed::FAINT), + term::label(String::from_utf8_lossy(&self.text).to_string()) + .fg(term::colors::fixed::DIM), + ]) + } +} + +impl ToPretty for Hunk { + type Output = term::VStack<'static>; + type Context = Blobs; + + fn pretty(&self, hi: &mut Highlighter, blobs: &Blobs, repo: &R) -> Self::Output { + let mut vstack = term::VStack::default().padding(0); + let mut table = term::Table::<5, term::Filled>::new(term::TableOptions { + overflow: false, + spacing: 0, + border: None, + }); + let theme = Theme::default(); + + if let Ok(header) = HunkHeader::from_bytes(self.header.as_bytes()) { + vstack.push(header.pretty(hi, &(), repo)); + } + for line in &self.lines { + match line { + Modification::Addition(a) => { + table.push([ + term::Label::space() + .pad(5) + .bg(theme.color("positive")) + .to_line() + .filled(theme.color("positive")), + term::label(a.line_no.to_string()) + .pad(5) + .fg(theme.color("positive.light")) + .to_line() + .filled(theme.color("positive")), + term::label(" + ") + .fg(theme.color("positive.light")) + .to_line() + .filled(theme.color("positive.dark")), + line.pretty(hi, blobs, repo) + .filled(theme.color("positive.dark")), + term::Line::blank().filled(term::Color::default()), + ]); + } + Modification::Deletion(a) => { + table.push([ + term::label(a.line_no.to_string()) + .pad(5) + .fg(theme.color("negative.light")) + .to_line() + .filled(theme.color("negative")), + term::Label::space() + .pad(5) + .fg(theme.color("dim")) + .to_line() + .filled(theme.color("negative")), + term::label(" - ") + .fg(theme.color("negative.light")) + .to_line() + .filled(theme.color("negative.dark")), + line.pretty(hi, blobs, repo) + .filled(theme.color("negative.dark")), + term::Line::blank().filled(term::Color::default()), + ]); + } + Modification::Context { + line_no_old, + line_no_new, + .. + } => { + table.push([ + term::label(line_no_old.to_string()) + .pad(5) + .fg(theme.color("dim")) + .to_line() + .filled(theme.color("faint")), + term::label(line_no_new.to_string()) + .pad(5) + .fg(theme.color("dim")) + .to_line() + .filled(theme.color("faint")), + term::label(" ").to_line().filled(term::Color::default()), + line.pretty(hi, blobs, repo).filled(term::Color::default()), + term::Line::blank().filled(term::Color::default()), + ]); + } + } + } + vstack.push(table); + vstack + } +} + +impl ToPretty for Modification { + type Output = term::Line; + type Context = Blobs; + + fn pretty(&self, _hi: &mut Highlighter, blobs: &Blobs, _repo: &R) -> Self::Output { + match self { + Modification::Deletion(diff::Deletion { line, line_no }) => { + if let Some(lines) = &blobs.old.as_ref() { + lines[*line_no as usize - 1].clone() + } else { + term::Line::new(String::from_utf8_lossy(line.as_bytes()).as_ref()) + } + } + Modification::Addition(diff::Addition { line, line_no }) => { + if let Some(lines) = &blobs.new.as_ref() { + lines[*line_no as usize - 1].clone() + } else { + term::Line::new(String::from_utf8_lossy(line.as_bytes()).as_ref()) + } + } + Modification::Context { + line, line_no_new, .. + } => { + // Nb. we can check in the old or the new blob, we choose the new. + if let Some(lines) = &blobs.new.as_ref() { + lines[*line_no_new as usize - 1].clone() + } else { + term::Line::new(String::from_utf8_lossy(line.as_bytes()).as_ref()) + } + } + } + } +} + +#[cfg(test)] +mod test { + use std::ffi::OsStr; + + use term::Constraint; + use term::Element; + + use super::*; + use radicle::git::raw::RepositoryOpenFlags; + use radicle::git::raw::{Oid, Repository}; + + #[test] + #[ignore] + fn test_pretty() { + let repo = Repository::open_ext::<_, _, &[&OsStr]>( + env!("CARGO_MANIFEST_DIR"), + RepositoryOpenFlags::all(), + &[], + ) + .unwrap(); + let commit = repo + .find_commit(Oid::from_str("5078396028e2ec5660aa54a00208f6e11df84aa9").unwrap()) + .unwrap(); + let parent = commit.parents().next().unwrap(); + let old_tree = parent.tree().unwrap(); + let new_tree = commit.tree().unwrap(); + let diff = repo + .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None) + .unwrap(); + let diff = Diff::try_from(diff).unwrap(); + + let mut hi = Highlighter::default(); + let pretty = diff.pretty(&mut hi, &(), &repo); + + pretty.write(Constraint::from_env().maximize()); + } +} diff --git a/radicle-cli/src/git/unified_diff.rs b/radicle-cli/src/git/unified_diff.rs index 140bf659..1abb2455 100644 --- a/radicle-cli/src/git/unified_diff.rs +++ b/radicle-cli/src/git/unified_diff.rs @@ -148,7 +148,12 @@ pub trait Decode: Sized { /// Decode from a string input. fn parse(s: &str) -> Result { - let mut r = io::BufReader::new(s.as_bytes()); + Self::from_bytes(s.as_bytes()) + } + + /// Decode from a string input. + fn from_bytes(bytes: &[u8]) -> Result { + let mut r = io::BufReader::new(bytes); Self::decode(&mut r) } } diff --git a/radicle-cli/src/terminal.rs b/radicle-cli/src/terminal.rs index a197031d..8c7adc94 100644 --- a/radicle-cli/src/terminal.rs +++ b/radicle-cli/src/terminal.rs @@ -4,6 +4,7 @@ pub mod format; pub mod io; pub use io::{proposal, signer}; pub mod comment; +pub mod highlight; pub mod issue; pub mod patch; diff --git a/radicle-cli/src/terminal/highlight.rs b/radicle-cli/src/terminal/highlight.rs new file mode 100644 index 00000000..a994d604 --- /dev/null +++ b/radicle-cli/src/terminal/highlight.rs @@ -0,0 +1,327 @@ +use std::{collections::HashMap, path::Path}; + +use radicle_term as term; +use tree_sitter_highlight as ts; + +/// Highlight groups enabled. +const HIGHLIGHTS: &[&str] = &[ + "attribute", + "constant", + "constant.builtin", + "comment", + "constructor", + "function.builtin", + "function", + "integer_literal", + "float.literal", + "keyword", + "label", + "operator", + "property", + "punctuation", + "punctuation.bracket", + "punctuation.delimiter", + "punctuation.special", + "string", + "string.special", + "tag", + "type", + "type.builtin", + "variable", + "variable.builtin", + "variable.parameter", + "text.literal", + "text.title", +]; + +/// Syntax highlighter based on `tree-sitter`. +#[derive(Default)] +pub struct Highlighter { + configs: HashMap<&'static str, ts::HighlightConfiguration>, +} + +/// Syntax theme. +pub struct Theme { + color: fn(&'static str) -> Option, +} + +impl Default for Theme { + fn default() -> Self { + let color = if term::Paint::truecolor() { + term::colors::rgb::theme + } else { + term::colors::fixed::theme + }; + Self { color } + } +} + +impl Theme { + /// Get the named color. + pub fn color(&self, color: &'static str) -> term::Color { + if let Some(c) = (self.color)(color) { + c + } else { + term::Color::Unset + } + } + + /// Return the color of a syntax group. + pub fn highlight(&self, group: &'static str) -> Option { + let color = match group { + "keyword" => self.color("red"), + "comment" => self.color("grey"), + "constant" => self.color("orange"), + "string" => self.color("teal"), + "function" => self.color("purple"), + "operator" => self.color("blue"), + // Eg. `true` and `false` in rust. + "constant.builtin" => self.color("blue"), + "type.builtin" => self.color("cyan"), + "punctuation.bracket" | "punctuation.delimiter" => term::Color::default(), + // Eg. the '#' in Markdown titles. + "punctuation.special" => self.color("dim"), + // Eg. Markdown code blocks. + "text.literal" => self.color("blue"), + "text.title" => self.color("orange"), + "variable.builtin" => term::Color::default(), + "property" => self.color("blue"), + // Eg. `#[derive(Debug)]` in rust + "attribute" => self.color("blue"), + "label" => self.color("green"), + // `Option` + "type" => self.color("grey.light"), + "variable.parameter" => term::Color::default(), + "constructor" => self.color("orange"), + + _ => return None, + }; + Some(color) + } +} + +/// Syntax highlighted file builder. +#[derive(Default)] +struct Builder { + /// Output lines. + lines: Vec, + /// Current output line. + line: Vec, + /// Current label. + label: Vec, + /// Current stack of styles. + styles: Vec, +} + +impl Builder { + /// Run the builder to completion. + fn run( + mut self, + highlights: impl Iterator>, + code: &[u8], + theme: &Theme, + ) -> Result, ts::Error> { + for event in highlights { + match event? { + ts::HighlightEvent::Source { start, end } => { + let range = &code[start..end]; + + for byte in range { + if *byte == b'\n' { + self.advance(); + // Start on new line. + self.lines.push(term::Line::from(self.line.clone())); + self.line.clear(); + } else { + // Add to existing label. + self.label.push(*byte); + } + } + } + ts::HighlightEvent::HighlightStart(h) => { + let name = HIGHLIGHTS[h.0]; + + self.advance(); + self.styles.push( + term::Style::default() + .fg(theme.highlight(name).unwrap_or(term::Color::default())), + ); + } + ts::HighlightEvent::HighlightEnd => { + self.advance(); + self.styles.pop(); + } + } + } + Ok(self.lines) + } + + /// Advance the state by pushing the current label onto the current line, + /// using the current styling. + fn advance(&mut self) { + if !self.label.is_empty() { + // Take the top-level style when there are more than one. + let style = self.styles.first().cloned().unwrap_or_default(); + self.line + .push(term::Label::new(String::from_utf8_lossy(&self.label).as_ref()).style(style)); + self.label.clear(); + } + } +} + +impl Highlighter { + /// Highlight a source code file. Returns `None` if the file type was not recognized. + pub fn highlight( + &mut self, + path: &Path, + code: &[u8], + ) -> Result>, ts::Error> { + let theme = Theme::default(); + let mut highlighter = ts::Highlighter::new(); + let Some(config) = self.detect(path, code) else { + return Ok(None); + }; + config.configure(HIGHLIGHTS); + + let highlights = highlighter.highlight(config, code, None, |_| { + // Language injection callback. + None + })?; + + Builder::default().run(highlights, code, &theme).map(Some) + } + + /// Detect language. + fn detect(&mut self, path: &Path, _code: &[u8]) -> Option<&mut ts::HighlightConfiguration> { + match path.extension().and_then(|e| e.to_str()) { + Some("rs") => self.config("rust"), + Some("ts" | "js" | "json") => self.config("typescript"), + Some("sh" | "bash") => self.config("shell"), + Some("md" | "markdown") => self.config("markdown"), + Some("go") => self.config("go"), + Some("c") => self.config("c"), + Some("py") => self.config("python"), + Some("rb") => self.config("ruby"), + Some("tsx") => self.config("tsx"), + Some("html") | Some("htm") | Some("xml") => self.config("html"), + Some("css") => self.config("css"), + Some("toml") => self.config("toml"), + _ => None, + } + } + + /// Get a language configuration. + fn config(&mut self, language: &'static str) -> Option<&mut ts::HighlightConfiguration> { + match language { + "rust" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_rust::language(), + tree_sitter_rust::HIGHLIGHT_QUERY, + tree_sitter_rust::INJECTIONS_QUERY, + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "typescript" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_typescript::language_typescript(), + tree_sitter_typescript::HIGHLIGHT_QUERY, + "", + tree_sitter_typescript::LOCALS_QUERY, + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "markdown" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_md::language(), + tree_sitter_md::HIGHLIGHT_QUERY_BLOCK, + tree_sitter_md::INJECTION_QUERY_BLOCK, + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "css" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_css::language(), + tree_sitter_css::HIGHLIGHTS_QUERY, + "", + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "go" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_go::language(), + tree_sitter_go::HIGHLIGHT_QUERY, + "", + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "shell" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_bash::language(), + tree_sitter_bash::HIGHLIGHT_QUERY, + "", + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "c" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_c::language(), + tree_sitter_c::HIGHLIGHT_QUERY, + "", + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "python" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_python::language(), + tree_sitter_python::HIGHLIGHT_QUERY, + "", + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "ruby" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_ruby::language(), + tree_sitter_ruby::HIGHLIGHT_QUERY, + "", + tree_sitter_ruby::LOCALS_QUERY, + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "tsx" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_typescript::language_tsx(), + tree_sitter_typescript::HIGHLIGHT_QUERY, + "", + tree_sitter_typescript::LOCALS_QUERY, + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "html" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_html::language(), + tree_sitter_html::HIGHLIGHT_QUERY, + tree_sitter_html::INJECTION_QUERY, + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + "toml" => Some(self.configs.entry(language).or_insert_with(|| { + ts::HighlightConfiguration::new( + tree_sitter_toml::language(), + tree_sitter_toml::HIGHLIGHT_QUERY, + "", + "", + ) + .expect("Highlighter::config: highlight configuration must be valid") + })), + _ => None, + } + } +}