oxilean-cli 0.1.2

OxiLean command-line interface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
//! Auto-generated module
//!
//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)

use super::functions::*;
use std::collections::{HashMap, VecDeque};

/// A single line change within a hunk.
#[derive(Debug, Clone)]
pub struct LineChange {
    /// The kind of change.
    pub kind: ChangeKind,
    /// The line content (without trailing newline).
    pub content: String,
    /// Line number in the old file (for Removed/Unchanged), or 0.
    pub old_lineno: usize,
    /// Line number in the new file (for Added/Unchanged), or 0.
    pub new_lineno: usize,
}
impl LineChange {
    /// Create a new line change.
    pub fn new(kind: ChangeKind, content: String, old_lineno: usize, new_lineno: usize) -> Self {
        Self {
            kind,
            content,
            old_lineno,
            new_lineno,
        }
    }
    /// Return the unified-diff prefix character for this change.
    pub fn prefix_char(&self) -> char {
        match self.kind {
            ChangeKind::Removed => '-',
            ChangeKind::Added => '+',
            ChangeKind::Unchanged => ' ',
        }
    }
    /// Convert to DiffLine.
    pub fn to_diff_line(&self) -> DiffLine {
        let (old, new) = match self.kind {
            ChangeKind::Removed => (Some(self.old_lineno), None),
            ChangeKind::Added => (None, Some(self.new_lineno)),
            ChangeKind::Unchanged => (Some(self.old_lineno), Some(self.new_lineno)),
        };
        DiffLine {
            kind: self.kind.clone().into(),
            content: self.content.clone(),
            old_lineno: old,
            new_lineno: new,
        }
    }
}
/// Extended diff line kind (compatible variant of ChangeKind).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffLineKind {
    /// Line was added.
    Added,
    /// Line was removed.
    Removed,
    /// Unchanged context line.
    Context,
}
/// A simplified representation of a top-level declaration for structural comparison.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclSummary {
    /// The kind of declaration (def, theorem, axiom, etc.).
    pub kind: String,
    /// The name of the declaration.
    pub name: String,
    /// The type signature (if visible).
    pub type_sig: String,
}
#[allow(dead_code)]
pub struct DiffWindow {
    diffs: std::collections::VecDeque<(String, DiffResult)>,
    max_size: usize,
}
#[allow(dead_code)]
impl DiffWindow {
    pub fn new(max_size: usize) -> Self {
        Self {
            diffs: std::collections::VecDeque::new(),
            max_size,
        }
    }
    pub fn push(&mut self, label: &str, result: DiffResult) {
        if self.diffs.len() >= self.max_size {
            self.diffs.pop_front();
        }
        self.diffs.push_back((label.to_string(), result));
    }
    pub fn total_additions(&self) -> usize {
        self.diffs.iter().map(|(_, r)| r.additions).sum()
    }
    pub fn total_deletions(&self) -> usize {
        self.diffs.iter().map(|(_, r)| r.deletions).sum()
    }
    pub fn len(&self) -> usize {
        self.diffs.len()
    }
    pub fn is_empty(&self) -> bool {
        self.diffs.is_empty()
    }
    pub fn labels(&self) -> Vec<&str> {
        self.diffs.iter().map(|(l, _)| l.as_str()).collect()
    }
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FilePatchHunk {
    pub old_start: usize,
    pub old_count: usize,
    pub new_start: usize,
    pub new_count: usize,
    pub lines: Vec<FilePatchLine>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DeclItem {
    pub name: String,
    pub kind: DeclItemKind,
    pub line: usize,
    pub body_hash: u64,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FilePatch {
    pub old_file: String,
    pub new_file: String,
    pub hunks: Vec<FilePatchHunk>,
}
#[allow(dead_code)]
impl FilePatch {
    pub fn to_unified_string(&self) -> String {
        let mut out = format!("--- {}\n+++ {}\n", self.old_file, self.new_file);
        for hunk in &self.hunks {
            out.push_str(&format!(
                "@@ -{},{} +{},{} @@\n",
                hunk.old_start, hunk.old_count, hunk.new_start, hunk.new_count
            ));
            for line in &hunk.lines {
                let prefix = match line.kind {
                    FilePatchLineKind::Context => ' ',
                    FilePatchLineKind::Added => '+',
                    FilePatchLineKind::Removed => '-',
                };
                out.push(prefix);
                out.push_str(&line.content);
                out.push('\n');
            }
        }
        out
    }
    pub fn net_change(&self) -> i64 {
        self.hunks
            .iter()
            .map(|h| {
                let adds = h
                    .lines
                    .iter()
                    .filter(|l| l.kind == FilePatchLineKind::Added)
                    .count() as i64;
                let rems = h
                    .lines
                    .iter()
                    .filter(|l| l.kind == FilePatchLineKind::Removed)
                    .count() as i64;
                adds - rems
            })
            .sum()
    }
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MultiDiffEntry {
    pub file_path: String,
    pub result: DiffResult,
}
#[allow(dead_code)]
pub struct KeywordFilter {
    pub(super) keyword: String,
}
#[allow(dead_code)]
impl KeywordFilter {
    pub fn new(keyword: &str) -> Self {
        Self {
            keyword: keyword.to_string(),
        }
    }
}
/// The type of change for a single line in a diff.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChangeKind {
    /// Line exists only in the old text (deletion).
    Removed,
    /// Line exists only in the new text (insertion).
    Added,
    /// Line is the same in both texts (context).
    Unchanged,
}
/// Extended diff line with optional line numbers.
#[derive(Debug, Clone)]
pub struct DiffLine {
    /// Kind of change.
    pub kind: DiffLineKind,
    /// Content of this line.
    pub content: String,
    /// Line number in the old file, if applicable.
    pub old_lineno: Option<usize>,
    /// Line number in the new file, if applicable.
    pub new_lineno: Option<usize>,
}
impl DiffLine {
    /// Create a new diff line.
    pub fn new(
        kind: DiffLineKind,
        content: String,
        old_lineno: Option<usize>,
        new_lineno: Option<usize>,
    ) -> Self {
        Self {
            kind,
            content,
            old_lineno,
            new_lineno,
        }
    }
    /// Return the unified-diff prefix character.
    pub fn prefix_char(&self) -> char {
        match self.kind {
            DiffLineKind::Removed => '-',
            DiffLineKind::Added => '+',
            DiffLineKind::Context => ' ',
        }
    }
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum OxiDiffToken {
    Keyword(String),
    Ident(String),
    Number(String),
    StringLit(String),
    Punct(String),
    Whitespace(String),
    Other(String),
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum WordToken {
    Equal(String),
    Added(String),
    Removed(String),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BlameSource {
    pub file: String,
    pub lines: Vec<BlameLine>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BlameLine {
    pub line_no: usize,
    pub content: String,
    pub commit: String,
    pub author: String,
}
/// A contiguous group of changes with surrounding context.
#[derive(Debug, Clone)]
pub struct DiffHunk {
    /// Starting line number in the old text (1-based).
    pub old_start: usize,
    /// Number of lines from the old text in this hunk.
    pub old_count: usize,
    /// Starting line number in the new text (1-based).
    pub new_start: usize,
    /// Number of lines from the new text in this hunk.
    pub new_count: usize,
    /// The individual line changes in this hunk.
    pub lines: Vec<DiffLine>,
    /// Precomputed hunk header string.
    pub header: String,
}
impl DiffHunk {
    /// Build the hunk header in unified diff style: `@@ -old_start,old_count +new_start,new_count @@`
    pub fn make_header(
        old_start: usize,
        old_count: usize,
        new_start: usize,
        new_count: usize,
    ) -> String {
        format!(
            "@@ -{},{} +{},{} @@",
            old_start, old_count, new_start, new_count
        )
    }
    /// Return true if this hunk contains at least one actual change.
    pub fn has_changes(&self) -> bool {
        self.lines.iter().any(|l| l.kind != DiffLineKind::Context)
    }
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum CharDiffToken {
    Equal(char),
    Added(char),
    Removed(char),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FilePatchLine {
    pub kind: FilePatchLineKind,
    pub content: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AnnotatedDiffLine {
    pub line: DiffLine,
    pub function_context: Option<String>,
    pub is_in_proof: bool,
    pub indent_level: usize,
}
#[allow(dead_code)]
pub struct MultiDiff {
    pub entries: Vec<MultiDiffEntry>,
}
#[allow(dead_code)]
impl MultiDiff {
    pub fn new() -> Self {
        Self {
            entries: Vec::new(),
        }
    }
    pub fn add(&mut self, path: &str, old: &str, new: &str) {
        let config = DiffConfig::new();
        let result = line_diff(old, new, &config);
        self.entries.push(MultiDiffEntry {
            file_path: path.to_string(),
            result,
        });
    }
    pub fn total_additions(&self) -> usize {
        self.entries.iter().map(|e| e.result.additions).sum()
    }
    pub fn total_deletions(&self) -> usize {
        self.entries.iter().map(|e| e.result.deletions).sum()
    }
    pub fn changed_files(&self) -> Vec<&str> {
        self.entries
            .iter()
            .filter(|e| e.result.additions > 0 || e.result.deletions > 0)
            .map(|e| e.file_path.as_str())
            .collect()
    }
    pub fn summary(&self) -> String {
        format!(
            "{} files: +{} -{} lines",
            self.entries.len(),
            self.total_additions(),
            self.total_deletions()
        )
    }
}
#[allow(dead_code)]
pub struct DiffSummaryReport {
    pub files_checked: usize,
    pub files_changed: usize,
    pub total_additions: usize,
    pub total_deletions: usize,
    pub largest_diff_file: Option<String>,
}
#[allow(dead_code)]
impl DiffSummaryReport {
    pub fn from_multi_diff(md: &MultiDiff) -> Self {
        let total_additions = md.total_additions();
        let total_deletions = md.total_deletions();
        let files_changed = md.changed_files().len();
        let largest = md
            .entries
            .iter()
            .max_by_key(|e| e.result.additions + e.result.deletions)
            .map(|e| e.file_path.clone());
        Self {
            files_checked: md.entries.len(),
            files_changed,
            total_additions,
            total_deletions,
            largest_diff_file: largest,
        }
    }
    pub fn to_string_report(&self) -> String {
        format!(
            "Files: {}/{} changed | Lines: +{} -{} | Largest: {}",
            self.files_changed,
            self.files_checked,
            self.total_additions,
            self.total_deletions,
            self.largest_diff_file.as_deref().unwrap_or("none")
        )
    }
}
/// Configuration for diff operations.
#[derive(Debug, Clone)]
pub struct DiffConfig {
    /// Number of context lines to show around each change.
    pub context_lines: usize,
    /// Whether to ignore differences in whitespace (leading/trailing/multiple).
    pub ignore_whitespace: bool,
    /// Whether to emit ANSI color codes in output.
    pub color_output: bool,
    /// Whether to show line numbers in the output.
    pub show_line_numbers: bool,
    /// Label for the "old" side of the diff.
    pub old_label: String,
    /// Label for the "new" side of the diff.
    pub new_label: String,
}
impl DiffConfig {
    /// Create a config with sensible defaults.
    pub fn new() -> Self {
        Self {
            context_lines: 3,
            ignore_whitespace: false,
            color_output: false,
            show_line_numbers: true,
            old_label: "a".to_string(),
            new_label: "b".to_string(),
        }
    }
    /// Set context lines (builder pattern).
    pub fn with_context(mut self, n: usize) -> Self {
        self.context_lines = n;
        self
    }
    /// Enable/disable whitespace ignoring (builder pattern).
    pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
        self.ignore_whitespace = ignore;
        self
    }
    /// Enable/disable color output (builder pattern).
    pub fn with_color(mut self, color: bool) -> Self {
        self.color_output = color;
        self
    }
    /// Set labels for old and new sides (builder pattern).
    pub fn with_labels(mut self, old: impl Into<String>, new: impl Into<String>) -> Self {
        self.old_label = old.into();
        self.new_label = new.into();
        self
    }
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum ChangeClass {
    ProofChange,
    TypeChange,
    CommentChange,
    WhitespaceChange,
    StructureChange,
    ImportChange,
    Other,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum DeclItemKind {
    Theorem,
    Def,
    Lemma,
    Axiom,
    Structure,
    Class,
    Instance,
    Other,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StructDiff {
    pub added: Vec<DeclItem>,
    pub removed: Vec<DeclItem>,
    pub changed: Vec<(DeclItem, DeclItem)>,
    pub unchanged: Vec<DeclItem>,
}
#[allow(dead_code)]
#[derive(Debug, Default)]
pub struct DiffStatistics {
    pub total_lines: usize,
    pub added_lines: usize,
    pub removed_lines: usize,
    pub context_lines: usize,
    pub hunks: usize,
    pub files_changed: usize,
}
#[allow(dead_code)]
impl DiffStatistics {
    pub fn similarity_percent(&self) -> f64 {
        if self.total_lines == 0 {
            return 100.0;
        }
        let changed = self.added_lines + self.removed_lines;
        let sim = 1.0 - (changed as f64 / (2.0 * self.total_lines as f64));
        (sim * 100.0).max(0.0)
    }
}
#[allow(dead_code)]
pub enum DiffDisplayTarget {
    Terminal,
    Html,
    Json,
    Csv,
    Compact,
}
/// The complete result of diffing two texts.
#[derive(Debug, Clone)]
pub struct DiffResult {
    /// The hunks comprising the diff.
    pub hunks: Vec<DiffHunk>,
    /// Total number of lines added.
    pub additions: usize,
    /// Total number of lines removed.
    pub deletions: usize,
}
impl DiffResult {
    /// Return true if there are no differences.
    pub fn is_empty(&self) -> bool {
        self.hunks.is_empty()
    }
    /// Total number of changed lines (additions + deletions).
    pub fn total_changes(&self) -> usize {
        self.additions + self.deletions
    }
}
/// The result of structurally comparing two OxiLean sources.
#[derive(Debug, Clone)]
pub struct StructuralDiffResult {
    /// Declarations present only in the old source.
    pub removed: Vec<DeclSummary>,
    /// Declarations present only in the new source.
    pub added: Vec<DeclSummary>,
    /// Declarations present in both but with different type signatures.
    pub modified: Vec<(DeclSummary, DeclSummary)>,
    /// Declarations that are identical in both.
    pub unchanged: Vec<DeclSummary>,
}
impl StructuralDiffResult {
    /// True if there are no structural differences.
    pub fn is_empty(&self) -> bool {
        self.removed.is_empty() && self.added.is_empty() && self.modified.is_empty()
    }
    /// Format as a human-readable summary.
    pub fn summary(&self) -> String {
        let mut out = String::new();
        if !self.removed.is_empty() {
            out.push_str(&format!("Removed ({}):\n", self.removed.len()));
            for d in &self.removed {
                out.push_str(&format!("  - {d}\n"));
            }
        }
        if !self.added.is_empty() {
            out.push_str(&format!("Added ({}):\n", self.added.len()));
            for d in &self.added {
                out.push_str(&format!("  + {d}\n"));
            }
        }
        if !self.modified.is_empty() {
            out.push_str(&format!("Modified ({}):\n", self.modified.len()));
            for (old, new) in &self.modified {
                out.push_str(&format!(
                    "  ~ {} : {} -> {}\n",
                    old.name, old.type_sig, new.type_sig
                ));
            }
        }
        if out.is_empty() {
            out.push_str("No structural differences.\n");
        }
        out
    }
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum FilePatchLineKind {
    Context,
    Added,
    Removed,
}
#[allow(dead_code)]
pub struct DiffCache {
    cache: std::collections::HashMap<(u64, u64), DiffResult>,
}
#[allow(dead_code)]
impl DiffCache {
    pub fn new() -> Self {
        Self {
            cache: std::collections::HashMap::new(),
        }
    }
    pub fn get_or_compute(&mut self, old: &str, new: &str) -> &DiffResult {
        let key = (hash_str(old), hash_str(new));
        self.cache
            .entry(key)
            .or_insert_with(|| line_diff(old, new, &DiffConfig::new()))
    }
    pub fn invalidate(&mut self, old: &str, new: &str) {
        let key = (hash_str(old), hash_str(new));
        self.cache.remove(&key);
    }
    pub fn size(&self) -> usize {
        self.cache.len()
    }
}