diff --git a/Cargo.lock b/Cargo.lock index da50677..4dc3428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -67,6 +73,21 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -83,6 +104,16 @@ dependencies = [ "serde", ] +[[package]] +name = "cc" +version = "1.2.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -135,6 +166,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -160,6 +200,15 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -182,6 +231,28 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "getrandom" version = "0.3.4" @@ -263,6 +334,12 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -284,6 +361,7 @@ dependencies = [ "mccabre-core", "owo-colors", "serde_json", + "syntect", ] [[package]] @@ -306,6 +384,22 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "once_cell" version = "1.21.3" @@ -318,12 +412,59 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "owo-colors" version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -333,6 +474,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.42" @@ -445,6 +595,18 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "strsim" version = "0.11.1" @@ -462,6 +624,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -495,6 +678,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "toml" version = "0.8.23" @@ -670,3 +884,12 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a067b52..59194de 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,3 +9,4 @@ owo-colors = "4.2.3" anyhow = "1.0" serde_json = "1.0" mccabre-core = { path = "../core" } +syntect = "5.2" diff --git a/crates/cli/src/commands/analyze.rs b/crates/cli/src/commands/analyze.rs index 30be9fe..f28cf94 100644 --- a/crates/cli/src/commands/analyze.rs +++ b/crates/cli/src/commands/analyze.rs @@ -3,15 +3,18 @@ use mccabre_core::{ cloner::CloneDetector, complexity::{CyclomaticMetrics, LocMetrics}, config::Config, - loader::FileLoader, + loader::{FileLoader, SourceFile}, reporter::{FileReport, Report}, }; use owo_colors::OwoColorize; +use std::collections::HashMap; use std::path::PathBuf; +use crate::highlight::Highlighter; + pub fn run( path: PathBuf, json: bool, threshold: Option, min_tokens: Option, config_path: Option, - respect_gitignore: bool, + respect_gitignore: bool, highlight: bool, ) -> Result<()> { let config = if let Some(config_path) = config_path { Config::from_file(config_path)? @@ -53,13 +56,13 @@ pub fn run( if json { println!("{}", report.to_json()?); } else { - print_pretty_report(&report, &config); + print_pretty_report(&report, &config, &files, highlight); } Ok(()) } -fn print_pretty_report(report: &Report, config: &Config) { +fn print_pretty_report(report: &Report, config: &Config, files: &[SourceFile], highlight: bool) { println!("{}", "=".repeat(80).cyan()); println!("{}", "MCCABRE CODE ANALYSIS REPORT".cyan().bold()); println!("{}", "=".repeat(80).cyan()); @@ -136,6 +139,9 @@ fn print_pretty_report(report: &Report, config: &Config) { println!("{}", "DETECTED CLONES".green().bold()); println!("{}", "-".repeat(80).cyan()); + let file_map: HashMap<_, _> = files.iter().map(|f| (&f.path, f)).collect(); + let highlighter = if highlight { Some(Highlighter::new()) } else { None }; + for clone in &report.clones { println!( "{} {} {} {} {} {}", @@ -154,6 +160,21 @@ fn print_pretty_report(report: &Report, config: &Config) { loc.file.display(), format!("{}-{}", loc.start_line, loc.end_line).dimmed() ); + + if highlight && let Some(source_file) = file_map.get(&loc.file) { + let code_block = extract_lines(&source_file.content, loc.start_line, loc.end_line); + + if let Some(ref hl) = highlighter { + let ext = source_file.path.extension().and_then(|e| e.to_str()).unwrap_or("txt"); + let highlighted = hl.highlight(&code_block, ext); + + println!("{}", " ┌─────".dimmed()); + for line in highlighted.lines() { + println!(" │ {}", line); + } + println!("{}", " └─────".dimmed()); + } + } } println!(); } @@ -161,3 +182,17 @@ fn print_pretty_report(report: &Report, config: &Config) { println!("{}", "=".repeat(80).cyan()); } + +/// Extract lines from source code by line numbers (1-indexed) +fn extract_lines(source: &str, start_line: usize, end_line: usize) -> String { + source + .lines() + .enumerate() + .filter(|(idx, _)| { + let line_num = idx + 1; + line_num >= start_line && line_num <= end_line + }) + .map(|(_, line)| line) + .collect::>() + .join("\n") +} diff --git a/crates/cli/src/commands/clones.rs b/crates/cli/src/commands/clones.rs index 774594e..7ffcf43 100644 --- a/crates/cli/src/commands/clones.rs +++ b/crates/cli/src/commands/clones.rs @@ -1,10 +1,19 @@ use anyhow::Result; -use mccabre_core::{cloner::CloneDetector, config::Config, loader::FileLoader, reporter::Report}; +use mccabre_core::{ + cloner::CloneDetector, + config::Config, + loader::{FileLoader, SourceFile}, + reporter::Report, +}; use owo_colors::OwoColorize; +use std::collections::HashMap; use std::path::PathBuf; +use crate::highlight::Highlighter; + pub fn run( path: PathBuf, json: bool, min_tokens: Option, config_path: Option, respect_gitignore: bool, + highlight: bool, ) -> Result<()> { let config = if let Some(config_path) = config_path { Config::from_file(config_path)? @@ -33,13 +42,13 @@ pub fn run( if json { println!("{}", report.to_json()?); } else { - print_clones_report(&report); + print_clones_report(&report, &files, highlight); } Ok(()) } -fn print_clones_report(report: &Report) { +fn print_clones_report(report: &Report, files: &[SourceFile], highlight: bool) { println!("{}", "=".repeat(80).cyan()); println!("{}", "CLONE DETECTION REPORT".cyan().bold()); println!("{}\n", "=".repeat(80).cyan()); @@ -55,6 +64,9 @@ fn print_clones_report(report: &Report) { ); println!(); + let file_map: HashMap<_, _> = files.iter().map(|f| (&f.path, f)).collect(); + let highlighter = if highlight { Some(Highlighter::new()) } else { None }; + for clone in &report.clones { println!( "{} {} {} {} {} {}", @@ -73,6 +85,21 @@ fn print_clones_report(report: &Report) { loc.file.display(), format!("{}-{}", loc.start_line, loc.end_line).dimmed() ); + + if highlight && let Some(source_file) = file_map.get(&loc.file) { + let code_block = extract_lines(&source_file.content, loc.start_line, loc.end_line); + + if let Some(ref hl) = highlighter { + let ext = source_file.path.extension().and_then(|e| e.to_str()).unwrap_or("txt"); + let highlighted = hl.highlight(&code_block, ext); + + println!("{}", " ┌─────".dimmed()); + for line in highlighted.lines() { + println!(" │ {}", line); + } + println!("{}", " └─────".dimmed()); + } + } } println!(); } @@ -80,3 +107,17 @@ fn print_clones_report(report: &Report) { println!("{}", "=".repeat(80).cyan()); } + +/// Extract lines from source code by line numbers (1-indexed) +fn extract_lines(source: &str, start_line: usize, end_line: usize) -> String { + source + .lines() + .enumerate() + .filter(|(idx, _)| { + let line_num = idx + 1; + line_num >= start_line && line_num <= end_line + }) + .map(|(_, line)| line) + .collect::>() + .join("\n") +} diff --git a/crates/cli/src/highlight.rs b/crates/cli/src/highlight.rs new file mode 100644 index 0000000..5c32701 --- /dev/null +++ b/crates/cli/src/highlight.rs @@ -0,0 +1,235 @@ +use owo_colors::OwoColorize; +use syntect::easy::HighlightLines; +use syntect::highlighting::{Color, Style, ThemeSet}; +use syntect::parsing::SyntaxSet; +use syntect::util::LinesWithEndings; + +pub struct Highlighter { + syntax_set: SyntaxSet, + theme_set: ThemeSet, +} + +impl Highlighter { + pub fn new() -> Self { + Self { syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: ThemeSet::load_defaults() } + } + + /// Highlight code with syntax highlighting + pub fn highlight(&self, code: &str, file_extension: &str) -> String { + let syntax = self + .syntax_set + .find_syntax_by_extension(file_extension) + .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); + + let theme = &self.theme_set.themes["base16-ocean.dark"]; + + let mut highlighter = HighlightLines::new(syntax, theme); + let mut output = String::new(); + + for line in LinesWithEndings::from(code) { + let ranges = highlighter.highlight_line(line, &self.syntax_set).unwrap_or_default(); + + for (style, text) in ranges { + output.push_str(&style_to_owo(&style, text)); + } + } + + output + } +} + +impl Default for Highlighter { + fn default() -> Self { + Self::new() + } +} + +/// Convert syntect Style to owo-colors styled text +fn style_to_owo(style: &Style, text: &str) -> String { + let fg = style.foreground; + + let colored = if is_grayscale(fg) { + if fg.r < 100 { + text.bright_black().to_string() + } else if fg.r < 180 { + text.white().to_string() + } else { + text.bright_white().to_string() + } + } else { + match dominant_color(fg) { + ColorChannel::Red => { + if fg.r > 200 { + text.bright_red().to_string() + } else { + text.red().to_string() + } + } + ColorChannel::Green => { + if fg.g > 200 { + text.bright_green().to_string() + } else { + text.green().to_string() + } + } + ColorChannel::Blue => { + if fg.b > 200 { + text.bright_cyan().to_string() + } else { + text.cyan().to_string() + } + } + ColorChannel::Yellow => { + if fg.r > 200 && fg.g > 200 { + text.bright_yellow().to_string() + } else { + text.yellow().to_string() + } + } + ColorChannel::Magenta => { + if fg.r > 200 && fg.b > 200 { + text.bright_magenta().to_string() + } else { + text.magenta().to_string() + } + } + } + }; + + if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) { + colored.bold().to_string() + } else { + colored + } +} + +enum ColorChannel { + Red, + Green, + Blue, + Yellow, + Magenta, +} + +fn is_grayscale(color: Color) -> bool { + let max_diff = color.r.abs_diff(color.g).max(color.g.abs_diff(color.b)); + max_diff < 30 +} + +fn dominant_color(color: Color) -> ColorChannel { + let r = color.r as u16; + let g = color.g as u16; + let b = color.b as u16; + + if r > 150 && g > 150 && b < 100 { + return ColorChannel::Yellow; + } + if r > 150 && b > 150 && g < 100 { + return ColorChannel::Magenta; + } + + if r >= g && r >= b { + ColorChannel::Red + } else if g >= r && g >= b { + ColorChannel::Green + } else { + ColorChannel::Blue + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_highlighter_creation() { + let highlighter = Highlighter::new(); + assert!(!highlighter.syntax_set.syntaxes().is_empty()); + assert!(!highlighter.theme_set.themes.is_empty()); + } + + #[test] + fn test_default_highlighter() { + let highlighter = Highlighter::default(); + assert!(!highlighter.syntax_set.syntaxes().is_empty()); + } + + #[test] + fn test_highlight_rust_code() { + let highlighter = Highlighter::new(); + let code = "fn main() {\n println!(\"Hello, world!\");\n}"; + let highlighted = highlighter.highlight(code, "rs"); + + assert!(!highlighted.is_empty()); + assert!(highlighted.len() >= code.len()); + } + + #[test] + fn test_highlight_python_code() { + let highlighter = Highlighter::new(); + let code = "def hello():\n print('Hello, world!')"; + let highlighted = highlighter.highlight(code, "py"); + + assert!(!highlighted.is_empty()); + assert!(highlighted.len() >= code.len()); + } + + #[test] + fn test_highlight_unknown_extension() { + let highlighter = Highlighter::new(); + let code = "some random text"; + let highlighted = highlighter.highlight(code, "unknown_ext"); + + assert!(!highlighted.is_empty()); + } + + #[test] + fn test_is_grayscale() { + assert!(is_grayscale(Color { r: 128, g: 128, b: 128, a: 255 })); + assert!(is_grayscale(Color { r: 100, g: 105, b: 100, a: 255 })); + assert!(!is_grayscale(Color { r: 255, g: 0, b: 0, a: 255 })); + assert!(!is_grayscale(Color { r: 200, g: 100, b: 50, a: 255 })); + } + + #[test] + fn test_dominant_color_red() { + let color = Color { r: 255, g: 50, b: 50, a: 255 }; + matches!(dominant_color(color), ColorChannel::Red); + } + + #[test] + fn test_dominant_color_green() { + let color = Color { r: 50, g: 255, b: 50, a: 255 }; + matches!(dominant_color(color), ColorChannel::Green); + } + + #[test] + fn test_dominant_color_blue() { + let color = Color { r: 50, g: 50, b: 255, a: 255 }; + matches!(dominant_color(color), ColorChannel::Blue); + } + + #[test] + fn test_dominant_color_yellow() { + let color = Color { r: 200, g: 200, b: 50, a: 255 }; + matches!(dominant_color(color), ColorChannel::Yellow); + } + + #[test] + fn test_dominant_color_magenta() { + let color = Color { r: 200, g: 50, b: 200, a: 255 }; + matches!(dominant_color(color), ColorChannel::Magenta); + } + + #[test] + fn test_style_to_owo_preserves_text() { + let style = Style { + foreground: Color { r: 255, g: 255, b: 255, a: 255 }, + background: Color { r: 0, g: 0, b: 0, a: 255 }, + font_style: syntect::highlighting::FontStyle::empty(), + }; + let text = "test text"; + let styled = style_to_owo(&style, text); + assert!(styled.contains(text)); + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2d1a096..bc4e6e1 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,4 +1,5 @@ mod commands; +mod highlight; use anyhow::Result; use clap::{Parser, Subcommand}; @@ -40,6 +41,10 @@ enum Commands { /// Disable gitignore awareness #[arg(long)] no_gitignore: bool, + + /// Disable syntax highlighting for clone code blocks + #[arg(long)] + no_highlight: bool, }, /// Analyze cyclomatic complexity and LOC only @@ -86,6 +91,10 @@ enum Commands { /// Disable gitignore awareness #[arg(long)] no_gitignore: bool, + + /// Disable syntax highlighting for clone code blocks + #[arg(long)] + no_highlight: bool, }, /// Display current configuration @@ -104,17 +113,40 @@ fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Analyze { path, json, threshold, min_tokens, config, no_gitignore } => { - commands::analyze::run(path, json, threshold, Some(min_tokens), config, !no_gitignore) - } - - Commands::Complexity { path, json, threshold, config, no_gitignore } => { - commands::complexity::run(path, json, threshold, config, !no_gitignore) - } - - Commands::Clones { path, json, min_tokens, config, no_gitignore } => { - commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore) - } + Commands::Analyze { + path, + json, + threshold, + min_tokens, + config, + no_gitignore, + no_highlight, + } => commands::analyze::run( + path, + json, + threshold, + Some(min_tokens), + config, + !no_gitignore, + !no_highlight, + ), + + Commands::Complexity { + path, + json, + threshold, + config, + no_gitignore, + } => commands::complexity::run(path, json, threshold, config, !no_gitignore), + + Commands::Clones { + path, + json, + min_tokens, + config, + no_gitignore, + no_highlight, + } => commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore, !no_highlight), Commands::DumpConfig { config, output } => commands::dump_config::run(config, output), } diff --git a/docs/src/cli-reference.md b/docs/src/cli-reference.md index 22f3617..39bc32a 100644 --- a/docs/src/cli-reference.md +++ b/docs/src/cli-reference.md @@ -21,6 +21,7 @@ mccabre analyze [OPTIONS] [PATH] - `--min-tokens ` - Minimum tokens for clone detection (default: 30) - `-c, --config ` - Path to config file - `--no-gitignore` - Disable gitignore awareness +- `--no-highlight` - Disable syntax highlighting for code blocks **Examples:** @@ -82,6 +83,7 @@ mccabre clones [OPTIONS] [PATH] - `--min-tokens ` - Minimum tokens for detection (default: 30) - `-c, --config ` - Path to config file - `--no-gitignore` - Disable gitignore awareness +- `--no-highlight` - Disable syntax highlighting for code blocks **Examples:** diff --git a/docs/src/examples.md b/docs/src/examples.md index ccc1f0b..e8e7e9c 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -266,6 +266,18 @@ mccabre dump-config -c ../project-a/mccabre.toml -o ./mccabre.toml ## Tips and Tricks +### Syntax Highlighting + +By default, code blocks in `analyze` and `clones` output are syntax highlighted. To disable: + +```bash +# Disable syntax highlighting for cleaner output +mccabre analyze src/ --no-highlight + +# Useful for piping to files or when colors aren't supported +mccabre clones src/ --no-highlight > report.txt +``` + ### Incremental Analysis Analyze only changed files: