use log::error;
use regex::{Regex, RegexBuilder};
use std::env;
use std::path::{Path, PathBuf};
pub(crate) fn default_cache_dir() -> PathBuf {
#[cfg(windows)]
{
windows_cache_dir()
}
#[cfg(not(windows))]
{
unix_cache_dir()
}
}
#[cfg(windows)]
pub(crate) fn windows_cache_dir() -> PathBuf {
if let Some(dir) = dirs::data_local_dir() {
return dir.join("codebook").join("cache");
}
if let Some(dir) = dirs::data_dir() {
return dir.join("codebook").join("cache");
}
if let Some(home) = dirs::home_dir() {
return home
.join("AppData")
.join("Local")
.join("codebook")
.join("cache");
}
env::temp_dir().join("codebook").join("cache")
}
#[cfg(not(windows))]
pub(crate) fn unix_cache_dir() -> PathBuf {
if let Some(xdg_data_home) = env::var_os("XDG_DATA_HOME")
&& !xdg_data_home.is_empty()
{
return PathBuf::from(xdg_data_home).join("codebook").join("cache");
}
if let Some(home) = dirs::home_dir() {
return home
.join(".local")
.join("share")
.join("codebook")
.join("cache");
}
env::temp_dir().join("codebook").join("cache")
}
pub(crate) fn build_ignore_regexes(patterns: &[String]) -> Vec<Regex> {
patterns
.iter()
.filter_map(
|pattern| match RegexBuilder::new(pattern).multi_line(true).build() {
Ok(regex) => Some(regex),
Err(e) => {
error!("Ignoring invalid regex pattern '{pattern}': {e}");
None
}
},
)
.collect()
}
pub(crate) fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
let p = path_user_input.as_ref();
if !p.starts_with("~") {
return Some(p.to_path_buf());
}
if p == Path::new("~") {
return dirs::home_dir();
}
dirs::home_dir().map(|mut h| {
if h == Path::new("/") {
p.strip_prefix("~").unwrap().to_path_buf()
} else {
h.push(p.strip_prefix("~/").unwrap());
h
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
use std::sync::{Mutex, MutexGuard};
#[cfg(not(windows))]
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[cfg(not(windows))]
fn lock_env_and_set_xdg(value: Option<&str>) -> (MutexGuard<'static, ()>, Option<OsString>) {
let guard = ENV_MUTEX.lock().unwrap();
let previous = env::var_os("XDG_DATA_HOME");
unsafe {
match value {
Some(val) => env::set_var("XDG_DATA_HOME", val),
None => env::remove_var("XDG_DATA_HOME"),
}
}
(guard, previous)
}
#[cfg(not(windows))]
fn restore_xdg(previous: Option<OsString>) {
unsafe {
match previous {
Some(val) => env::set_var("XDG_DATA_HOME", val),
None => env::remove_var("XDG_DATA_HOME"),
}
}
}
#[cfg(not(windows))]
#[test]
fn unix_cache_dir_uses_xdg_data_home() {
let (guard, previous) = lock_env_and_set_xdg(Some("/tmp/codebook-xdg"));
let expected = PathBuf::from("/tmp/codebook-xdg")
.join("codebook")
.join("cache");
assert_eq!(unix_cache_dir(), expected);
restore_xdg(previous);
drop(guard);
}
#[cfg(not(windows))]
#[test]
fn unix_cache_dir_falls_back_to_home() {
let (guard, previous) = lock_env_and_set_xdg(Some(""));
let home = dirs::home_dir().expect("home directory must be available for the test");
let expected = home
.join(".local")
.join("share")
.join("codebook")
.join("cache");
assert_eq!(unix_cache_dir(), expected);
restore_xdg(previous);
drop(guard);
}
#[cfg(not(windows))]
#[test]
fn default_cache_dir_matches_unix_on_non_windows() {
let (guard, previous) = lock_env_and_set_xdg(Some("/tmp/codebook-xdg-default"));
assert_eq!(default_cache_dir(), unix_cache_dir());
restore_xdg(previous);
drop(guard);
}
#[test]
fn test_build_ignore_regexes_valid_patterns() {
let patterns = vec![r"\b[A-Z]{2,}\b".to_string(), r"TODO:.*".to_string()];
let compiled = build_ignore_regexes(&patterns);
assert_eq!(compiled.len(), 2);
assert!(compiled[0].is_match("HTML"));
assert!(compiled[1].is_match("TODO: fix this"));
}
#[test]
fn test_build_ignore_regexes_invalid_pattern_skipped() {
let patterns = vec![
r"valid.*".to_string(),
r"[invalid".to_string(), r"also_valid".to_string(),
];
let compiled = build_ignore_regexes(&patterns);
assert_eq!(compiled.len(), 2);
}
#[test]
fn test_build_ignore_regexes_multiline_mode() {
let patterns = vec![r"^vim\..*".to_string()];
let compiled = build_ignore_regexes(&patterns);
let text = "let x = 1\nvim.opt.showmode = false\nlet y = 2";
assert!(compiled[0].is_match(text));
let m = compiled[0].find(text).unwrap();
assert_eq!(m.as_str(), "vim.opt.showmode = false");
}
}