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