use crate::path_info::{PathInfo, SegmentPriority};
use crate::platform;
use crate::strategy::fish::abbreviate_segment;
pub fn shrink_hybrid(
info: &PathInfo,
max_len: usize,
ellipsis: &str,
anchors: &[String],
) -> String {
if info.segments.is_empty() {
return info.reassemble(&[]);
}
let texts: Vec<String> = info.segments.iter().map(|s| s.text.clone()).collect();
let text_refs: Vec<&str> = texts.iter().map(|s| s.as_str()).collect();
let full = info.reassemble(&text_refs);
if full.len() <= max_len {
return full;
}
if info.filename.len() >= max_len {
return info.filename.clone();
}
let mut working: Vec<String> = texts;
let priorities: Vec<SegmentPriority> = info.segments.iter().map(|s| s.priority).collect();
for (i, priority) in priorities.iter().enumerate() {
if *priority == SegmentPriority::Expendable {
working[i] = abbreviate_segment(&info.segments[i].text, 1, anchors);
}
}
let refs: Vec<&str> = working.iter().map(|s| s.as_str()).collect();
let result = info.reassemble(&refs);
if result.len() <= max_len {
return result;
}
for (i, priority) in priorities.iter().enumerate() {
if *priority == SegmentPriority::Context {
working[i] = abbreviate_segment(&info.segments[i].text, 1, anchors);
}
}
let refs: Vec<&str> = working.iter().map(|s| s.as_str()).collect();
let result = info.reassemble(&refs);
if result.len() <= max_len {
return result;
}
let n = working.len();
if n > 2 {
let collapsed = collapse_middle(&working, &priorities, info, max_len, ellipsis);
if collapsed.len() <= max_len {
return collapsed;
}
}
for (i, priority) in priorities.iter().enumerate() {
if *priority == SegmentPriority::Identity {
working[i] = abbreviate_segment(&info.segments[i].text, 1, anchors);
}
}
let n = working.len();
if n > 2 {
let collapsed = collapse_middle(&working, &priorities, info, max_len, ellipsis);
if collapsed.len() <= max_len {
return collapsed;
}
}
let sep = platform::separator(info.style);
let sep_str = sep.to_string();
let prefix_sep = if info.prefix.is_empty() { "" } else { &sep_str };
let minimal = format!(
"{}{}{}{}{}",
info.prefix, prefix_sep, ellipsis, &sep_str, info.filename,
);
if minimal.len() <= max_len {
return minimal;
}
info.filename.clone()
}
fn collapse_middle(
working: &[String],
_priorities: &[SegmentPriority],
info: &PathInfo,
max_len: usize,
ellipsis: &str,
) -> String {
let n = working.len();
for keep_tail in (0..=n.min(3)).rev() {
for keep_head in (0..=n.min(3)).rev() {
if keep_head + keep_tail >= n {
continue;
}
let mut parts: Vec<&str> = Vec::new();
for w in working.iter().take(keep_head) {
parts.push(w);
}
parts.push(ellipsis);
for w in working.iter().skip(n - keep_tail) {
parts.push(w);
}
let result = info.reassemble(&parts);
if result.len() <= max_len {
return result;
}
}
}
let sep = platform::separator(info.style);
let sep_str = sep.to_string();
let prefix_sep = if info.prefix.is_empty() { "" } else { &sep_str };
format!(
"{}{}{}{}{}",
info.prefix, prefix_sep, ellipsis, &sep_str, info.filename,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn already_short() {
let info = PathInfo::parse("/home/user/file.txt", None);
assert_eq!(shrink_hybrid(&info, 50, "...", &[]), "/home/user/file.txt");
}
#[test]
fn phase1_fish_expendable() {
let info = PathInfo::parse("/home/john/projects/rust/myapp/src/main.rs", None);
let result = shrink_hybrid(&info, 35, "...", &[]);
assert!(result.len() <= 35, "got len {}: {}", result.len(), result);
assert!(result.ends_with("main.rs"));
assert!(
result.contains("john"),
"should preserve identity: {result}"
);
}
#[test]
fn phase3_collapse() {
let info = PathInfo::parse(
"/home/john/projects/rust/myapp/src/deep/nested/main.rs",
None,
);
let result = shrink_hybrid(&info, 30, "...", &[]);
assert!(result.len() <= 30, "got len {}: {}", result.len(), result);
assert!(result.ends_with("main.rs"));
}
#[test]
fn windows_hybrid() {
let info = PathInfo::parse(
"C:\\Users\\Admin\\AppData\\Local\\Temp\\deep\\file.txt",
None,
);
let result = shrink_hybrid(&info, 35, "...", &[]);
assert!(result.len() <= 35, "got len {}: {}", result.len(), result);
assert!(result.ends_with("file.txt"));
assert!(
result.contains("Admin") || result.contains("A"),
"should preserve identity somehow: {result}",
);
}
#[test]
fn tilde_hybrid() {
let info = PathInfo::parse("~/projects/rust/app/src/lib.rs", None);
let result = shrink_hybrid(&info, 25, "...", &[]);
assert!(result.len() <= 25, "got len {}: {}", result.len(), result);
assert!(result.ends_with("lib.rs"));
}
#[test]
fn filename_exceeds() {
let info = PathInfo::parse("/a/b/c/very_long_filename.txt", None);
let result = shrink_hybrid(&info, 10, "...", &[]);
assert_eq!(result, "very_long_filename.txt");
}
#[test]
fn macos_app_support() {
let info = PathInfo::parse(
"/Users/john/Library/Application Support/Code/User/settings.json",
None,
);
let result = shrink_hybrid(&info, 45, "...", &[]);
assert!(result.len() <= 45, "got len {}: {}", result.len(), result);
assert!(result.ends_with("settings.json"));
assert!(
result.contains("john"),
"should preserve identity: {result}"
);
}
#[test]
fn dot_backslash() {
let info = PathInfo::parse(
".\\Users\\Admin\\AppData\\Local\\Packages\\Microsoft.MicrosoftEdge\\file.txt",
None,
);
let result = shrink_hybrid(&info, 40, "...", &[]);
assert!(result.len() <= 40, "got len {}: {}", result.len(), result);
assert!(result.ends_with("file.txt"));
}
}