code complexity & repetition analysis tool

feat: syntax highlighting with syntect

+597 -16
+223
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "adler2" 7 + version = "2.0.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 + 11 + [[package]] 6 12 name = "aho-corasick" 7 13 version = "1.1.4" 8 14 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 68 74 checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 69 75 70 76 [[package]] 77 + name = "base64" 78 + version = "0.22.1" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 81 + 82 + [[package]] 83 + name = "bincode" 84 + version = "1.3.3" 85 + source = "registry+https://github.com/rust-lang/crates.io-index" 86 + checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 87 + dependencies = [ 88 + "serde", 89 + ] 90 + 91 + [[package]] 71 92 name = "bitflags" 72 93 version = "2.10.0" 73 94 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 84 105 ] 85 106 86 107 [[package]] 108 + name = "cc" 109 + version = "1.2.45" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" 112 + dependencies = [ 113 + "find-msvc-tools", 114 + "shlex", 115 + ] 116 + 117 + [[package]] 87 118 name = "cfg-if" 88 119 version = "1.0.4" 89 120 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 136 167 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 137 168 138 169 [[package]] 170 + name = "crc32fast" 171 + version = "1.5.0" 172 + source = "registry+https://github.com/rust-lang/crates.io-index" 173 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 174 + dependencies = [ 175 + "cfg-if", 176 + ] 177 + 178 + [[package]] 139 179 name = "crossbeam-deque" 140 180 version = "0.8.6" 141 181 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 161 201 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 162 202 163 203 [[package]] 204 + name = "deranged" 205 + version = "0.5.5" 206 + source = "registry+https://github.com/rust-lang/crates.io-index" 207 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 208 + dependencies = [ 209 + "powerfmt", 210 + ] 211 + 212 + [[package]] 164 213 name = "equivalent" 165 214 version = "1.0.2" 166 215 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 183 232 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 184 233 185 234 [[package]] 235 + name = "find-msvc-tools" 236 + version = "0.1.4" 237 + source = "registry+https://github.com/rust-lang/crates.io-index" 238 + checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 239 + 240 + [[package]] 241 + name = "flate2" 242 + version = "1.1.5" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 245 + dependencies = [ 246 + "crc32fast", 247 + "miniz_oxide", 248 + ] 249 + 250 + [[package]] 251 + name = "fnv" 252 + version = "1.0.7" 253 + source = "registry+https://github.com/rust-lang/crates.io-index" 254 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 255 + 256 + [[package]] 186 257 name = "getrandom" 187 258 version = "0.3.4" 188 259 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 264 335 checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 265 336 266 337 [[package]] 338 + name = "linked-hash-map" 339 + version = "0.5.6" 340 + source = "registry+https://github.com/rust-lang/crates.io-index" 341 + checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 342 + 343 + [[package]] 267 344 name = "linux-raw-sys" 268 345 version = "0.11.0" 269 346 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 284 361 "mccabre-core", 285 362 "owo-colors", 286 363 "serde_json", 364 + "syntect", 287 365 ] 288 366 289 367 [[package]] ··· 307 385 checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 308 386 309 387 [[package]] 388 + name = "miniz_oxide" 389 + version = "0.8.9" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 392 + dependencies = [ 393 + "adler2", 394 + "simd-adler32", 395 + ] 396 + 397 + [[package]] 398 + name = "num-conv" 399 + version = "0.1.0" 400 + source = "registry+https://github.com/rust-lang/crates.io-index" 401 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 402 + 403 + [[package]] 310 404 name = "once_cell" 311 405 version = "1.21.3" 312 406 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 319 413 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 320 414 321 415 [[package]] 416 + name = "onig" 417 + version = "6.5.1" 418 + source = "registry+https://github.com/rust-lang/crates.io-index" 419 + checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" 420 + dependencies = [ 421 + "bitflags", 422 + "libc", 423 + "once_cell", 424 + "onig_sys", 425 + ] 426 + 427 + [[package]] 428 + name = "onig_sys" 429 + version = "69.9.1" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" 432 + dependencies = [ 433 + "cc", 434 + "pkg-config", 435 + ] 436 + 437 + [[package]] 322 438 name = "owo-colors" 323 439 version = "4.2.3" 324 440 source = "registry+https://github.com/rust-lang/crates.io-index" 325 441 checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 326 442 327 443 [[package]] 444 + name = "pkg-config" 445 + version = "0.3.32" 446 + source = "registry+https://github.com/rust-lang/crates.io-index" 447 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 448 + 449 + [[package]] 450 + name = "plist" 451 + version = "1.8.0" 452 + source = "registry+https://github.com/rust-lang/crates.io-index" 453 + checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" 454 + dependencies = [ 455 + "base64", 456 + "indexmap", 457 + "quick-xml", 458 + "serde", 459 + "time", 460 + ] 461 + 462 + [[package]] 463 + name = "powerfmt" 464 + version = "0.2.0" 465 + source = "registry+https://github.com/rust-lang/crates.io-index" 466 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 467 + 468 + [[package]] 328 469 name = "proc-macro2" 329 470 version = "1.0.103" 330 471 source = "registry+https://github.com/rust-lang/crates.io-index" 331 472 checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 332 473 dependencies = [ 333 474 "unicode-ident", 475 + ] 476 + 477 + [[package]] 478 + name = "quick-xml" 479 + version = "0.38.4" 480 + source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" 482 + dependencies = [ 483 + "memchr", 334 484 ] 335 485 336 486 [[package]] ··· 446 596 ] 447 597 448 598 [[package]] 599 + name = "shlex" 600 + version = "1.3.0" 601 + source = "registry+https://github.com/rust-lang/crates.io-index" 602 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 603 + 604 + [[package]] 605 + name = "simd-adler32" 606 + version = "0.3.7" 607 + source = "registry+https://github.com/rust-lang/crates.io-index" 608 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 609 + 610 + [[package]] 449 611 name = "strsim" 450 612 version = "0.11.1" 451 613 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 463 625 ] 464 626 465 627 [[package]] 628 + name = "syntect" 629 + version = "5.3.0" 630 + source = "registry+https://github.com/rust-lang/crates.io-index" 631 + checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" 632 + dependencies = [ 633 + "bincode", 634 + "flate2", 635 + "fnv", 636 + "once_cell", 637 + "onig", 638 + "plist", 639 + "regex-syntax", 640 + "serde", 641 + "serde_derive", 642 + "serde_json", 643 + "thiserror", 644 + "walkdir", 645 + "yaml-rust", 646 + ] 647 + 648 + [[package]] 466 649 name = "tempfile" 467 650 version = "3.23.0" 468 651 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 493 676 "proc-macro2", 494 677 "quote", 495 678 "syn", 679 + ] 680 + 681 + [[package]] 682 + name = "time" 683 + version = "0.3.44" 684 + source = "registry+https://github.com/rust-lang/crates.io-index" 685 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 686 + dependencies = [ 687 + "deranged", 688 + "itoa", 689 + "num-conv", 690 + "powerfmt", 691 + "serde", 692 + "time-core", 693 + "time-macros", 694 + ] 695 + 696 + [[package]] 697 + name = "time-core" 698 + version = "0.1.6" 699 + source = "registry+https://github.com/rust-lang/crates.io-index" 700 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 701 + 702 + [[package]] 703 + name = "time-macros" 704 + version = "0.2.24" 705 + source = "registry+https://github.com/rust-lang/crates.io-index" 706 + checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 707 + dependencies = [ 708 + "num-conv", 709 + "time-core", 496 710 ] 497 711 498 712 [[package]] ··· 670 884 version = "0.46.0" 671 885 source = "registry+https://github.com/rust-lang/crates.io-index" 672 886 checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 887 + 888 + [[package]] 889 + name = "yaml-rust" 890 + version = "0.4.5" 891 + source = "registry+https://github.com/rust-lang/crates.io-index" 892 + checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 893 + dependencies = [ 894 + "linked-hash-map", 895 + ]
+1
crates/cli/Cargo.toml
··· 9 9 anyhow = "1.0" 10 10 serde_json = "1.0" 11 11 mccabre-core = { path = "../core" } 12 + syntect = "5.2"
+39 -4
crates/cli/src/commands/analyze.rs
··· 3 3 cloner::CloneDetector, 4 4 complexity::{CyclomaticMetrics, LocMetrics}, 5 5 config::Config, 6 - loader::FileLoader, 6 + loader::{FileLoader, SourceFile}, 7 7 reporter::{FileReport, Report}, 8 8 }; 9 9 use owo_colors::OwoColorize; 10 + use std::collections::HashMap; 10 11 use std::path::PathBuf; 11 12 13 + use crate::highlight::Highlighter; 14 + 12 15 pub fn run( 13 16 path: PathBuf, json: bool, threshold: Option<usize>, min_tokens: Option<usize>, config_path: Option<PathBuf>, 14 - respect_gitignore: bool, 17 + respect_gitignore: bool, highlight: bool, 15 18 ) -> Result<()> { 16 19 let config = if let Some(config_path) = config_path { 17 20 Config::from_file(config_path)? ··· 53 56 if json { 54 57 println!("{}", report.to_json()?); 55 58 } else { 56 - print_pretty_report(&report, &config); 59 + print_pretty_report(&report, &config, &files, highlight); 57 60 } 58 61 59 62 Ok(()) 60 63 } 61 64 62 - fn print_pretty_report(report: &Report, config: &Config) { 65 + fn print_pretty_report(report: &Report, config: &Config, files: &[SourceFile], highlight: bool) { 63 66 println!("{}", "=".repeat(80).cyan()); 64 67 println!("{}", "MCCABRE CODE ANALYSIS REPORT".cyan().bold()); 65 68 println!("{}", "=".repeat(80).cyan()); ··· 136 139 println!("{}", "DETECTED CLONES".green().bold()); 137 140 println!("{}", "-".repeat(80).cyan()); 138 141 142 + let file_map: HashMap<_, _> = files.iter().map(|f| (&f.path, f)).collect(); 143 + let highlighter = if highlight { Some(Highlighter::new()) } else { None }; 144 + 139 145 for clone in &report.clones { 140 146 println!( 141 147 "{} {} {} {} {} {}", ··· 154 160 loc.file.display(), 155 161 format!("{}-{}", loc.start_line, loc.end_line).dimmed() 156 162 ); 163 + 164 + if highlight && let Some(source_file) = file_map.get(&loc.file) { 165 + let code_block = extract_lines(&source_file.content, loc.start_line, loc.end_line); 166 + 167 + if let Some(ref hl) = highlighter { 168 + let ext = source_file.path.extension().and_then(|e| e.to_str()).unwrap_or("txt"); 169 + let highlighted = hl.highlight(&code_block, ext); 170 + 171 + println!("{}", " ┌─────".dimmed()); 172 + for line in highlighted.lines() { 173 + println!(" │ {}", line); 174 + } 175 + println!("{}", " └─────".dimmed()); 176 + } 177 + } 157 178 } 158 179 println!(); 159 180 } ··· 161 182 162 183 println!("{}", "=".repeat(80).cyan()); 163 184 } 185 + 186 + /// Extract lines from source code by line numbers (1-indexed) 187 + fn extract_lines(source: &str, start_line: usize, end_line: usize) -> String { 188 + source 189 + .lines() 190 + .enumerate() 191 + .filter(|(idx, _)| { 192 + let line_num = idx + 1; 193 + line_num >= start_line && line_num <= end_line 194 + }) 195 + .map(|(_, line)| line) 196 + .collect::<Vec<_>>() 197 + .join("\n") 198 + }
+44 -3
crates/cli/src/commands/clones.rs
··· 1 1 use anyhow::Result; 2 - use mccabre_core::{cloner::CloneDetector, config::Config, loader::FileLoader, reporter::Report}; 2 + use mccabre_core::{ 3 + cloner::CloneDetector, 4 + config::Config, 5 + loader::{FileLoader, SourceFile}, 6 + reporter::Report, 7 + }; 3 8 use owo_colors::OwoColorize; 9 + use std::collections::HashMap; 4 10 use std::path::PathBuf; 5 11 12 + use crate::highlight::Highlighter; 13 + 6 14 pub fn run( 7 15 path: PathBuf, json: bool, min_tokens: Option<usize>, config_path: Option<PathBuf>, respect_gitignore: bool, 16 + highlight: bool, 8 17 ) -> Result<()> { 9 18 let config = if let Some(config_path) = config_path { 10 19 Config::from_file(config_path)? ··· 33 42 if json { 34 43 println!("{}", report.to_json()?); 35 44 } else { 36 - print_clones_report(&report); 45 + print_clones_report(&report, &files, highlight); 37 46 } 38 47 39 48 Ok(()) 40 49 } 41 50 42 - fn print_clones_report(report: &Report) { 51 + fn print_clones_report(report: &Report, files: &[SourceFile], highlight: bool) { 43 52 println!("{}", "=".repeat(80).cyan()); 44 53 println!("{}", "CLONE DETECTION REPORT".cyan().bold()); 45 54 println!("{}\n", "=".repeat(80).cyan()); ··· 54 63 "clone groups".green().bold() 55 64 ); 56 65 println!(); 66 + 67 + let file_map: HashMap<_, _> = files.iter().map(|f| (&f.path, f)).collect(); 68 + let highlighter = if highlight { Some(Highlighter::new()) } else { None }; 57 69 58 70 for clone in &report.clones { 59 71 println!( ··· 73 85 loc.file.display(), 74 86 format!("{}-{}", loc.start_line, loc.end_line).dimmed() 75 87 ); 88 + 89 + if highlight && let Some(source_file) = file_map.get(&loc.file) { 90 + let code_block = extract_lines(&source_file.content, loc.start_line, loc.end_line); 91 + 92 + if let Some(ref hl) = highlighter { 93 + let ext = source_file.path.extension().and_then(|e| e.to_str()).unwrap_or("txt"); 94 + let highlighted = hl.highlight(&code_block, ext); 95 + 96 + println!("{}", " ┌─────".dimmed()); 97 + for line in highlighted.lines() { 98 + println!(" │ {}", line); 99 + } 100 + println!("{}", " └─────".dimmed()); 101 + } 102 + } 76 103 } 77 104 println!(); 78 105 } ··· 80 107 81 108 println!("{}", "=".repeat(80).cyan()); 82 109 } 110 + 111 + /// Extract lines from source code by line numbers (1-indexed) 112 + fn extract_lines(source: &str, start_line: usize, end_line: usize) -> String { 113 + source 114 + .lines() 115 + .enumerate() 116 + .filter(|(idx, _)| { 117 + let line_num = idx + 1; 118 + line_num >= start_line && line_num <= end_line 119 + }) 120 + .map(|(_, line)| line) 121 + .collect::<Vec<_>>() 122 + .join("\n") 123 + }
+235
crates/cli/src/highlight.rs
··· 1 + use owo_colors::OwoColorize; 2 + use syntect::easy::HighlightLines; 3 + use syntect::highlighting::{Color, Style, ThemeSet}; 4 + use syntect::parsing::SyntaxSet; 5 + use syntect::util::LinesWithEndings; 6 + 7 + pub struct Highlighter { 8 + syntax_set: SyntaxSet, 9 + theme_set: ThemeSet, 10 + } 11 + 12 + impl Highlighter { 13 + pub fn new() -> Self { 14 + Self { syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: ThemeSet::load_defaults() } 15 + } 16 + 17 + /// Highlight code with syntax highlighting 18 + pub fn highlight(&self, code: &str, file_extension: &str) -> String { 19 + let syntax = self 20 + .syntax_set 21 + .find_syntax_by_extension(file_extension) 22 + .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); 23 + 24 + let theme = &self.theme_set.themes["base16-ocean.dark"]; 25 + 26 + let mut highlighter = HighlightLines::new(syntax, theme); 27 + let mut output = String::new(); 28 + 29 + for line in LinesWithEndings::from(code) { 30 + let ranges = highlighter.highlight_line(line, &self.syntax_set).unwrap_or_default(); 31 + 32 + for (style, text) in ranges { 33 + output.push_str(&style_to_owo(&style, text)); 34 + } 35 + } 36 + 37 + output 38 + } 39 + } 40 + 41 + impl Default for Highlighter { 42 + fn default() -> Self { 43 + Self::new() 44 + } 45 + } 46 + 47 + /// Convert syntect Style to owo-colors styled text 48 + fn style_to_owo(style: &Style, text: &str) -> String { 49 + let fg = style.foreground; 50 + 51 + let colored = if is_grayscale(fg) { 52 + if fg.r < 100 { 53 + text.bright_black().to_string() 54 + } else if fg.r < 180 { 55 + text.white().to_string() 56 + } else { 57 + text.bright_white().to_string() 58 + } 59 + } else { 60 + match dominant_color(fg) { 61 + ColorChannel::Red => { 62 + if fg.r > 200 { 63 + text.bright_red().to_string() 64 + } else { 65 + text.red().to_string() 66 + } 67 + } 68 + ColorChannel::Green => { 69 + if fg.g > 200 { 70 + text.bright_green().to_string() 71 + } else { 72 + text.green().to_string() 73 + } 74 + } 75 + ColorChannel::Blue => { 76 + if fg.b > 200 { 77 + text.bright_cyan().to_string() 78 + } else { 79 + text.cyan().to_string() 80 + } 81 + } 82 + ColorChannel::Yellow => { 83 + if fg.r > 200 && fg.g > 200 { 84 + text.bright_yellow().to_string() 85 + } else { 86 + text.yellow().to_string() 87 + } 88 + } 89 + ColorChannel::Magenta => { 90 + if fg.r > 200 && fg.b > 200 { 91 + text.bright_magenta().to_string() 92 + } else { 93 + text.magenta().to_string() 94 + } 95 + } 96 + } 97 + }; 98 + 99 + if style.font_style.contains(syntect::highlighting::FontStyle::BOLD) { 100 + colored.bold().to_string() 101 + } else { 102 + colored 103 + } 104 + } 105 + 106 + enum ColorChannel { 107 + Red, 108 + Green, 109 + Blue, 110 + Yellow, 111 + Magenta, 112 + } 113 + 114 + fn is_grayscale(color: Color) -> bool { 115 + let max_diff = color.r.abs_diff(color.g).max(color.g.abs_diff(color.b)); 116 + max_diff < 30 117 + } 118 + 119 + fn dominant_color(color: Color) -> ColorChannel { 120 + let r = color.r as u16; 121 + let g = color.g as u16; 122 + let b = color.b as u16; 123 + 124 + if r > 150 && g > 150 && b < 100 { 125 + return ColorChannel::Yellow; 126 + } 127 + if r > 150 && b > 150 && g < 100 { 128 + return ColorChannel::Magenta; 129 + } 130 + 131 + if r >= g && r >= b { 132 + ColorChannel::Red 133 + } else if g >= r && g >= b { 134 + ColorChannel::Green 135 + } else { 136 + ColorChannel::Blue 137 + } 138 + } 139 + 140 + #[cfg(test)] 141 + mod tests { 142 + use super::*; 143 + 144 + #[test] 145 + fn test_highlighter_creation() { 146 + let highlighter = Highlighter::new(); 147 + assert!(!highlighter.syntax_set.syntaxes().is_empty()); 148 + assert!(!highlighter.theme_set.themes.is_empty()); 149 + } 150 + 151 + #[test] 152 + fn test_default_highlighter() { 153 + let highlighter = Highlighter::default(); 154 + assert!(!highlighter.syntax_set.syntaxes().is_empty()); 155 + } 156 + 157 + #[test] 158 + fn test_highlight_rust_code() { 159 + let highlighter = Highlighter::new(); 160 + let code = "fn main() {\n println!(\"Hello, world!\");\n}"; 161 + let highlighted = highlighter.highlight(code, "rs"); 162 + 163 + assert!(!highlighted.is_empty()); 164 + assert!(highlighted.len() >= code.len()); 165 + } 166 + 167 + #[test] 168 + fn test_highlight_python_code() { 169 + let highlighter = Highlighter::new(); 170 + let code = "def hello():\n print('Hello, world!')"; 171 + let highlighted = highlighter.highlight(code, "py"); 172 + 173 + assert!(!highlighted.is_empty()); 174 + assert!(highlighted.len() >= code.len()); 175 + } 176 + 177 + #[test] 178 + fn test_highlight_unknown_extension() { 179 + let highlighter = Highlighter::new(); 180 + let code = "some random text"; 181 + let highlighted = highlighter.highlight(code, "unknown_ext"); 182 + 183 + assert!(!highlighted.is_empty()); 184 + } 185 + 186 + #[test] 187 + fn test_is_grayscale() { 188 + assert!(is_grayscale(Color { r: 128, g: 128, b: 128, a: 255 })); 189 + assert!(is_grayscale(Color { r: 100, g: 105, b: 100, a: 255 })); 190 + assert!(!is_grayscale(Color { r: 255, g: 0, b: 0, a: 255 })); 191 + assert!(!is_grayscale(Color { r: 200, g: 100, b: 50, a: 255 })); 192 + } 193 + 194 + #[test] 195 + fn test_dominant_color_red() { 196 + let color = Color { r: 255, g: 50, b: 50, a: 255 }; 197 + matches!(dominant_color(color), ColorChannel::Red); 198 + } 199 + 200 + #[test] 201 + fn test_dominant_color_green() { 202 + let color = Color { r: 50, g: 255, b: 50, a: 255 }; 203 + matches!(dominant_color(color), ColorChannel::Green); 204 + } 205 + 206 + #[test] 207 + fn test_dominant_color_blue() { 208 + let color = Color { r: 50, g: 50, b: 255, a: 255 }; 209 + matches!(dominant_color(color), ColorChannel::Blue); 210 + } 211 + 212 + #[test] 213 + fn test_dominant_color_yellow() { 214 + let color = Color { r: 200, g: 200, b: 50, a: 255 }; 215 + matches!(dominant_color(color), ColorChannel::Yellow); 216 + } 217 + 218 + #[test] 219 + fn test_dominant_color_magenta() { 220 + let color = Color { r: 200, g: 50, b: 200, a: 255 }; 221 + matches!(dominant_color(color), ColorChannel::Magenta); 222 + } 223 + 224 + #[test] 225 + fn test_style_to_owo_preserves_text() { 226 + let style = Style { 227 + foreground: Color { r: 255, g: 255, b: 255, a: 255 }, 228 + background: Color { r: 0, g: 0, b: 0, a: 255 }, 229 + font_style: syntect::highlighting::FontStyle::empty(), 230 + }; 231 + let text = "test text"; 232 + let styled = style_to_owo(&style, text); 233 + assert!(styled.contains(text)); 234 + } 235 + }
+41 -9
crates/cli/src/main.rs
··· 1 1 mod commands; 2 + mod highlight; 2 3 3 4 use anyhow::Result; 4 5 use clap::{Parser, Subcommand}; ··· 40 41 /// Disable gitignore awareness 41 42 #[arg(long)] 42 43 no_gitignore: bool, 44 + 45 + /// Disable syntax highlighting for clone code blocks 46 + #[arg(long)] 47 + no_highlight: bool, 43 48 }, 44 49 45 50 /// Analyze cyclomatic complexity and LOC only ··· 86 91 /// Disable gitignore awareness 87 92 #[arg(long)] 88 93 no_gitignore: bool, 94 + 95 + /// Disable syntax highlighting for clone code blocks 96 + #[arg(long)] 97 + no_highlight: bool, 89 98 }, 90 99 91 100 /// Display current configuration ··· 104 113 let cli = Cli::parse(); 105 114 106 115 match cli.command { 107 - Commands::Analyze { path, json, threshold, min_tokens, config, no_gitignore } => { 108 - commands::analyze::run(path, json, threshold, Some(min_tokens), config, !no_gitignore) 109 - } 116 + Commands::Analyze { 117 + path, 118 + json, 119 + threshold, 120 + min_tokens, 121 + config, 122 + no_gitignore, 123 + no_highlight, 124 + } => commands::analyze::run( 125 + path, 126 + json, 127 + threshold, 128 + Some(min_tokens), 129 + config, 130 + !no_gitignore, 131 + !no_highlight, 132 + ), 110 133 111 - Commands::Complexity { path, json, threshold, config, no_gitignore } => { 112 - commands::complexity::run(path, json, threshold, config, !no_gitignore) 113 - } 134 + Commands::Complexity { 135 + path, 136 + json, 137 + threshold, 138 + config, 139 + no_gitignore, 140 + } => commands::complexity::run(path, json, threshold, config, !no_gitignore), 114 141 115 - Commands::Clones { path, json, min_tokens, config, no_gitignore } => { 116 - commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore) 117 - } 142 + Commands::Clones { 143 + path, 144 + json, 145 + min_tokens, 146 + config, 147 + no_gitignore, 148 + no_highlight, 149 + } => commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore, !no_highlight), 118 150 119 151 Commands::DumpConfig { config, output } => commands::dump_config::run(config, output), 120 152 }
+2
docs/src/cli-reference.md
··· 21 21 - `--min-tokens <N>` - Minimum tokens for clone detection (default: 30) 22 22 - `-c, --config <FILE>` - Path to config file 23 23 - `--no-gitignore` - Disable gitignore awareness 24 + - `--no-highlight` - Disable syntax highlighting for code blocks 24 25 25 26 **Examples:** 26 27 ··· 82 83 - `--min-tokens <N>` - Minimum tokens for detection (default: 30) 83 84 - `-c, --config <FILE>` - Path to config file 84 85 - `--no-gitignore` - Disable gitignore awareness 86 + - `--no-highlight` - Disable syntax highlighting for code blocks 85 87 86 88 **Examples:** 87 89
+12
docs/src/examples.md
··· 266 266 267 267 ## Tips and Tricks 268 268 269 + ### Syntax Highlighting 270 + 271 + By default, code blocks in `analyze` and `clones` output are syntax highlighted. To disable: 272 + 273 + ```bash 274 + # Disable syntax highlighting for cleaner output 275 + mccabre analyze src/ --no-highlight 276 + 277 + # Useful for piping to files or when colors aren't supported 278 + mccabre clones src/ --no-highlight > report.txt 279 + ``` 280 + 269 281 ### Incremental Analysis 270 282 271 283 Analyze only changed files: