headson 0.17.0

Budget‑constrained JSON preview renderer
Documentation
use std::cmp::Ordering;
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::panic::{self, AssertUnwindSafe};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;

use git2::Repository;

const FRECENFILE_TOP_N: usize = 100;

#[derive(Debug, Clone)]
pub(crate) struct FrecencyContext {
    pub(crate) repo_root: PathBuf,
    pub(crate) ranks: HashMap<OsString, usize>,
}

impl FrecencyContext {
    pub(crate) fn rank_for(&self, rel: &Path) -> Option<usize> {
        self.ranks.get(rel.as_os_str()).copied()
    }
}

#[derive(Clone)]
struct PathOrderEntry {
    idx: usize,
    path: PathBuf,
    rank: Option<usize>,
    modified: Option<u64>,
}

pub(crate) fn sort_paths_for_fileset(paths: &[PathBuf]) -> Vec<PathBuf> {
    if paths.len() <= 1 {
        return paths.to_vec();
    }
    let Ok(cwd) = env::current_dir() else {
        return paths.to_vec();
    };
    let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
    let frecency = build_frecency_context(&canonical_cwd);
    sort_paths_with_context(paths, &canonical_cwd, frecency.as_ref())
}

pub(crate) fn sort_paths_with_context(
    paths: &[PathBuf],
    cwd: &Path,
    frecency: Option<&FrecencyContext>,
) -> Vec<PathBuf> {
    let mut entries: Vec<PathOrderEntry> = paths
        .iter()
        .enumerate()
        .map(|(idx, path)| {
            let rank = frecency.and_then(|ctx| {
                relative_path_in_repo(path, cwd, &ctx.repo_root)
                    .and_then(|rel| ctx.rank_for(&rel))
            });
            let modified = path_modified_timestamp(path);
            PathOrderEntry {
                idx,
                path: path.clone(),
                rank,
                modified,
            }
        })
        .collect();

    entries.sort_by(compare_path_order);
    if std::env::var_os("HEADSON_FRECEN_TRACE").is_some() {
        eprintln!(
            "frecen-sort input={:?} ranks={:?}",
            paths,
            frecency.map(|ctx| {
                ctx.ranks
                    .iter()
                    .map(|(k, v)| (k.clone(), *v))
                    .collect::<Vec<_>>()
            })
        );
        eprintln!(
            "frecen-sort sorted={:?}",
            entries
                .iter()
                .map(|e| (e.path.clone(), e.rank, e.modified))
                .collect::<Vec<_>>()
        );
    }
    entries.into_iter().map(|entry| entry.path).collect()
}

fn compare_path_order(a: &PathOrderEntry, b: &PathOrderEntry) -> Ordering {
    match (a.rank, b.rank) {
        (Some(ra), Some(rb)) => {
            let ord = ra.cmp(&rb);
            if ord != Ordering::Equal {
                return ord;
            }
        }
        (Some(_), None) => return Ordering::Less,
        (None, Some(_)) => return Ordering::Greater,
        (None, None) => {}
    }
    match (a.modified, b.modified) {
        (Some(ma), Some(mb)) => {
            let ord = mb.cmp(&ma);
            if ord != Ordering::Equal {
                return ord;
            }
        }
        (Some(_), None) => return Ordering::Less,
        (None, Some(_)) => return Ordering::Greater,
        (None, None) => {}
    }
    a.idx.cmp(&b.idx)
}

pub(crate) fn build_frecency_context(cwd: &Path) -> Option<FrecencyContext> {
    if std::env::var_os("HEADSON_SKIP_FRECEN").is_some() {
        return None;
    }
    let repo = Repository::discover(cwd).ok()?;
    let workdir = repo.workdir()?;
    let canonical_root = workdir
        .canonicalize()
        .unwrap_or_else(|_| workdir.to_path_buf());
    let ranks = load_frecenfile_ranks(&canonical_root)?;
    Some(FrecencyContext {
        repo_root: canonical_root,
        ranks,
    })
}

fn load_frecenfile_ranks(
    repo_root: &Path,
) -> Option<HashMap<OsString, usize>> {
    let default_hook = panic::take_hook();
    panic::set_hook(Box::new(|_| {}));
    let result = panic::catch_unwind(AssertUnwindSafe(|| {
        frecenfile::analyze_repo(repo_root, None, None)
    }));
    panic::set_hook(default_hook);
    let mut scores = match result {
        Ok(Ok(v)) => v,
        Ok(Err(e)) => {
            if std::env::var_os("HEADSON_FRECEN_TRACE").is_some() {
                eprintln!("frecenfile error: {e}");
            }
            return None;
        }
        Err(_) => {
            if std::env::var_os("HEADSON_FRECEN_TRACE").is_some() {
                eprintln!("frecenfile panicked; suppressing");
            }
            return None;
        }
    };
    scores.sort_by(|a, b| {
        b.1.partial_cmp(&a.1)
            .unwrap_or(Ordering::Equal)
            .then_with(|| a.0.cmp(&b.0))
    });
    scores.truncate(FRECENFILE_TOP_N);
    let mut ranks = HashMap::with_capacity(scores.len());
    for (idx, (path, _score)) in scores.into_iter().enumerate() {
        ranks.insert(path.into_os_string(), idx);
    }
    Some(ranks)
}

fn relative_path_in_repo(
    path: &Path,
    cwd: &Path,
    repo_root: &Path,
) -> Option<PathBuf> {
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        cwd.join(path)
    };
    let canonical = absolute.canonicalize().ok()?;
    canonical.strip_prefix(repo_root).ok().map(PathBuf::from)
}

fn path_modified_timestamp(path: &Path) -> Option<u64> {
    let meta = std::fs::metadata(path).ok()?;
    let modified = meta.modified().ok()?;
    let duration = modified.duration_since(UNIX_EPOCH).ok()?;
    Some(duration.as_secs())
}