use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ops::Range;
use std::rc::Rc;
use crate::classify::{Classified, classify};
use crate::diff::{Hunk, RowKind, diff, filter_formatting};
use crate::highlight::annotate;
use crate::lang::Lang;
use crate::project::{LayerMode, project};
use crate::store::fingerprint;
use crate::test_files::is_test_file;
pub const CONTEXT: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeStatus {
Added,
Deleted,
Modified,
Renamed,
}
impl ChangeStatus {
pub fn letter(self) -> char {
match self {
ChangeStatus::Added => 'A',
ChangeStatus::Deleted => 'D',
ChangeStatus::Modified => 'M',
ChangeStatus::Renamed => 'R',
}
}
}
#[derive(Debug, Clone)]
pub struct FileChange {
pub path: String,
pub old_path: Option<String>,
pub status: ChangeStatus,
pub old: Option<String>,
pub new: Option<String>,
pub binary: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Detection {
Parsed(Lang),
Partial(Lang, usize),
Unsupported,
Binary,
}
impl Detection {
pub fn label(self) -> String {
match self {
Detection::Parsed(lang) => lang.name().to_string(),
Detection::Partial(lang, line) => {
format!("{}, partial parse near line {line}", lang.name())
}
Detection::Unsupported => "no grammar".to_string(),
Detection::Binary => "binary".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanLayer {
Comments,
Imports,
Logging,
Tests,
}
impl SpanLayer {
pub const ALL: [SpanLayer; 4] = [
SpanLayer::Comments,
SpanLayer::Imports,
SpanLayer::Logging,
SpanLayer::Tests,
];
pub fn adjective(self) -> &'static str {
match self {
SpanLayer::Comments => "comment",
SpanLayer::Imports => "import",
SpanLayer::Logging => "logging",
SpanLayer::Tests => "test",
}
}
fn spans(self, classified: &Classified) -> &[Range<usize>] {
match self {
SpanLayer::Comments => &classified.comments,
SpanLayer::Imports => &classified.imports,
SpanLayer::Logging => &classified.logging,
SpanLayer::Tests => &classified.tests,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DiffModes {
pub comments: LayerMode,
pub imports: LayerMode,
pub logging: LayerMode,
pub formatting: LayerMode,
pub tests: LayerMode,
}
impl DiffModes {
pub const SHOWN: DiffModes = DiffModes {
comments: LayerMode::Shown,
imports: LayerMode::Shown,
logging: LayerMode::Shown,
formatting: LayerMode::Shown,
tests: LayerMode::Shown,
};
pub fn mode(self, layer: SpanLayer) -> LayerMode {
match layer {
SpanLayer::Comments => self.comments,
SpanLayer::Imports => self.imports,
SpanLayer::Logging => self.logging,
SpanLayer::Tests => self.tests,
}
}
pub fn effective(self) -> (LayerMode, Vec<SpanLayer>) {
for mode in [LayerMode::Only, LayerMode::Hidden] {
let layers: Vec<SpanLayer> = SpanLayer::ALL
.into_iter()
.filter(|&layer| self.mode(layer) == mode)
.collect();
if !layers.is_empty() {
return (mode, layers);
}
}
(LayerMode::Shown, Vec::new())
}
pub fn outcome(self) -> (LayerMode, String) {
for mode in [LayerMode::Only, LayerMode::Hidden] {
let mut names: Vec<&str> = SpanLayer::ALL
.into_iter()
.filter(|&layer| self.mode(layer) == mode)
.map(SpanLayer::adjective)
.collect();
if self.formatting == mode {
names.push("formatting");
}
if !names.is_empty() {
return (mode, names.join("/"));
}
}
(LayerMode::Shown, String::new())
}
pub fn adjective(self) -> String {
self.outcome().1
}
}
impl From<LayerMode> for DiffModes {
fn from(comments: LayerMode) -> DiffModes {
DiffModes {
comments,
..DiffModes::SHOWN
}
}
}
impl From<Layers> for DiffModes {
fn from(layers: Layers) -> DiffModes {
DiffModes {
comments: layers.comments,
imports: layers.imports,
logging: layers.logging,
formatting: layers.formatting,
tests: layers.tests,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ModeView {
pub hunks: Vec<Hunk>,
pub hidden_hunks: usize,
}
#[derive(Debug, Clone)]
pub struct FileReview {
pub path: String,
pub old_path: Option<String>,
pub status: ChangeStatus,
pub detection: Detection,
pub is_test: bool,
pub fingerprint: u64,
old: String,
new: String,
old_layers: Classified,
new_layers: Classified,
views: RefCell<HashMap<DiffModes, Rc<ModeView>>>,
}
impl FileReview {
pub fn new(change: FileChange) -> FileReview {
let old = change.old.unwrap_or_default();
let new = change.new.unwrap_or_default();
let (detection, old_layers, new_layers) = if change.binary {
(
Detection::Binary,
Classified::default(),
Classified::default(),
)
} else {
match Lang::from_path(&change.path)
.and_then(|lang| Some((lang, classify(lang, &old)?, classify(lang, &new)?)))
{
Some((lang, old_c, new_c)) => {
let detection = match new_c.error_line.or(old_c.error_line) {
Some(line) => Detection::Partial(lang, line),
None => Detection::Parsed(lang),
};
(detection, old_c, new_c)
}
None => (
Detection::Unsupported,
Classified::default(),
Classified::default(),
),
}
};
let is_test = is_test_file(&change.path, if new.is_empty() { &old } else { &new });
let fingerprint = fingerprint(&[&change.path, &old, &new]);
FileReview {
path: change.path,
old_path: change.old_path,
status: change.status,
detection,
is_test,
fingerprint,
old,
new,
old_layers,
new_layers,
views: RefCell::new(HashMap::new()),
}
}
pub fn view(&self, modes: impl Into<DiffModes>) -> Rc<ModeView> {
let modes = modes.into();
if let Some(view) = self.views.borrow().get(&modes) {
return Rc::clone(view);
}
let hunks = self.hunks_for(modes);
let hidden_hunks = if modes == DiffModes::SHOWN {
0
} else {
count_hidden(&self.view(DiffModes::SHOWN).hunks, &hunks)
};
let view = Rc::new(ModeView {
hunks,
hidden_hunks,
});
self.views.borrow_mut().insert(modes, Rc::clone(&view));
view
}
fn hunks_for(&self, modes: DiffModes) -> Vec<Hunk> {
let modes = if self.is_test {
DiffModes {
tests: LayerMode::Shown,
..modes
}
} else {
modes
};
let (mode, layers) = modes.effective();
let lang = match self.detection {
Detection::Parsed(lang) | Detection::Partial(lang, _) => Some(lang),
_ => None,
};
let side = |src: &str, classified: &Classified| {
let mut projection = project(src, &union(classified, &layers), mode);
if let Some(lang) = lang {
annotate(&mut projection, lang);
}
projection
};
let mut hunks = diff(
&side(&self.old, &self.old_layers),
&side(&self.new, &self.new_layers),
CONTEXT,
);
let indentation_matters = !matches!(
lang,
Some(
Lang::Swift
| Lang::TypeScript
| Lang::Tsx
| Lang::JavaScript
| Lang::Go
| Lang::Rust
| Lang::Kotlin
)
);
filter_formatting(&mut hunks, modes.formatting, indentation_matters);
if mode == LayerMode::Hidden {
for hunk in &mut hunks {
hunk.rows
.retain(|row| row.kind == RowKind::Context || !row.text.trim().is_empty());
}
hunks.retain(|hunk| hunk.changed().next().is_some());
}
hunks
}
pub fn total_hunks(&self) -> usize {
self.view(DiffModes::SHOWN).hunks.len()
}
pub fn is_visible(&self, tests: LayerMode) -> bool {
match tests {
LayerMode::Shown => true,
LayerMode::Hidden => !self.is_test,
LayerMode::Only => self.is_test || self.has_test_blocks(),
}
}
pub fn has_test_blocks(&self) -> bool {
!self.old_layers.tests.is_empty() || !self.new_layers.tests.is_empty()
}
pub fn tag(&self) -> String {
let detection = self.detection.label();
if self.is_test {
format!("{detection}, test")
} else {
detection
}
}
}
fn union(classified: &Classified, layers: &[SpanLayer]) -> Vec<Range<usize>> {
let mut spans: Vec<Range<usize>> = layers
.iter()
.flat_map(|layer| layer.spans(classified).iter().cloned())
.collect();
spans.sort_by_key(|span| span.start);
let mut merged: Vec<Range<usize>> = Vec::with_capacity(spans.len());
for span in spans {
match merged.last_mut() {
Some(last) if span.start <= last.end => last.end = last.end.max(span.end),
_ => merged.push(span),
}
}
merged
}
fn count_hidden(full: &[Hunk], visible: &[Hunk]) -> usize {
let key = |kind: RowKind, old: Option<usize>, new: Option<usize>| match kind {
RowKind::Removed => old.map(|line| (false, line)),
_ => new.map(|line| (true, line)),
};
let still_changed: HashSet<(bool, usize)> = visible
.iter()
.flat_map(Hunk::changed)
.filter_map(|row| key(row.kind, row.old_line, row.new_line))
.collect();
full.iter()
.filter(|hunk| {
!hunk.changed().any(|row| {
key(row.kind, row.old_line, row.new_line)
.is_some_and(|k| still_changed.contains(&k))
})
})
.count()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Layers {
pub comments: LayerMode,
pub tests: LayerMode,
pub imports: LayerMode,
pub logging: LayerMode,
pub formatting: LayerMode,
}
impl Default for Layers {
fn default() -> Layers {
Layers {
comments: LayerMode::Hidden,
tests: LayerMode::Shown,
imports: LayerMode::Shown,
logging: LayerMode::Shown,
formatting: LayerMode::Shown,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Summary {
pub files: usize,
pub total_hunks: usize,
pub visible_hunks: usize,
pub hidden_hunks: usize,
pub fully_hidden_files: usize,
pub filtered_files: usize,
pub unsupported_files: usize,
pub partial_files: usize,
pub test_block_files: usize,
}
impl Summary {
pub fn new(files: &[FileReview], layers: Layers) -> Summary {
let mut summary = Summary::default();
for file in files {
if !file.is_visible(layers.tests) {
summary.filtered_files += 1;
continue;
}
let view = file.view(layers);
summary.files += 1;
summary.total_hunks += file.total_hunks();
summary.visible_hunks += view.hunks.len();
summary.hidden_hunks += view.hidden_hunks;
if file.total_hunks() > 0 && view.hunks.is_empty() {
summary.fully_hidden_files += 1;
}
if file.has_test_blocks() {
summary.test_block_files += 1;
}
match file.detection {
Detection::Unsupported => summary.unsupported_files += 1,
Detection::Partial(..) => summary.partial_files += 1,
_ => {}
}
}
summary
}
pub fn status_line(&self, layers: Layers) -> String {
let states: Vec<String> = [
("comments", layers.comments),
("tests", layers.tests),
("imports", layers.imports),
("logging", layers.logging),
("formatting", layers.formatting),
]
.into_iter()
.filter(|(_, mode)| *mode != LayerMode::Shown)
.map(|(name, mode)| format!("{name}: {}", mode.label()))
.collect();
let mut parts = if states.is_empty() {
vec!["all layers shown".to_string()]
} else {
states
};
let mut modes = DiffModes::from(layers);
if self.test_block_files == 0 {
modes.tests = LayerMode::Shown;
}
let adjective = modes.adjective();
match modes.outcome().0 {
LayerMode::Shown => parts.push(format!(
"{} {} in {} {}",
self.total_hunks,
plural(self.total_hunks, "hunk"),
self.files,
plural(self.files, "file")
)),
LayerMode::Hidden => {
parts.push(format!(
"showing {} of {} hunks",
self.visible_hunks, self.total_hunks
));
parts.push(format!(
"{} {adjective}-only {} hidden",
self.hidden_hunks,
plural(self.hidden_hunks, "hunk")
));
if self.fully_hidden_files > 0 {
parts.push(format!(
"{} {adjective}-only {}",
self.fully_hidden_files,
plural(self.fully_hidden_files, "file")
));
}
}
LayerMode::Only => {
parts.push(format!(
"{} {} with {adjective} changes",
self.visible_hunks,
plural(self.visible_hunks, "hunk")
));
parts.push(format!(
"{} other {} hidden",
self.hidden_hunks,
plural(self.hidden_hunks, "hunk")
));
}
}
match layers.tests {
LayerMode::Shown => {}
LayerMode::Hidden => parts.push(format!(
"{} test {} hidden",
self.filtered_files,
plural(self.filtered_files, "file")
)),
LayerMode::Only => parts.push(format!(
"{} non-test {} hidden",
self.filtered_files,
plural(self.filtered_files, "file")
)),
}
if self.unsupported_files > 0 {
parts.push(format!(
"no grammar for {} {}",
self.unsupported_files,
plural(self.unsupported_files, "file")
));
}
if self.partial_files > 0 {
parts.push(format!(
"{} {} partially parsed",
self.partial_files,
plural(self.partial_files, "file")
));
}
parts.join(" ยท ")
}
}
fn plural(count: usize, word: &str) -> String {
if count == 1 {
word.to_string()
} else {
format!("{word}s")
}
}