diff --git a/crates/engine/src/script/folder.rs b/crates/engine/src/script/folder.rs index 4453953d8..d071195df 100644 --- a/crates/engine/src/script/folder.rs +++ b/crates/engine/src/script/folder.rs @@ -10,8 +10,8 @@ //! //! The values here are VectorCraft's own limits: [`MAX_ENTRIES`], [`MAX_SCRIPTS`], [`MAX_FOLDERS`], //! [`MAX_DEPTH`], [`MAX_NAME_BYTES`], [`MAX_LABEL_CHARS`] and [`MAX_SCAN`]. A listing that reaches -//! one of them, or that cannot read a subfolder, keeps what it found and says so -//! ([`Listing::truncated`]). +//! one of them, or that cannot read a subfolder or an entry of a folder, keeps what it found and +//! says so ([`Listing::truncated`]). use std::io::Read; use std::path::{Component, Path, PathBuf}; @@ -157,7 +157,10 @@ impl Scan<'_> { continue; }; let Ok(name) = entry.file_name().into_string() else { continue }; - let Ok(kind) = entry.file_type() else { continue }; + let Ok(kind) = entry.file_type() else { + self.truncated = true; + continue; + }; if name.len() > MAX_NAME_BYTES || name.starts_with('.') || kind.is_symlink() || hidden(&entry) { continue; } @@ -173,14 +176,26 @@ impl Scan<'_> { found.push(Found { label: shown(stem), name, folder: false }); } } - // A script whose label another row of this folder has shows its whole file name. - let mut count = std::collections::HashMap::::new(); - for f in &found { - *count.entry(f.label.clone()).or_default() += 1; - } - for f in found.iter_mut().filter(|f| !f.folder) { - if count.get(&f.label).is_some_and(|n| *n > 1) { - f.label = shown(&f.name); + // A script whose label another row of this folder has shows its whole file name, until no + // label is shared. Each pass gives at least one more script its whole file name, and names + // are unique within a folder, so this ends within `found.len()` passes. + loop { + let mut count = std::collections::HashMap::::new(); + for f in &found { + *count.entry(f.label.clone()).or_default() += 1; + } + let mut renamed = false; + for f in found.iter_mut().filter(|f| !f.folder) { + if count.get(&f.label).is_some_and(|n| *n > 1) { + let whole = shown(&f.name); + if f.label != whole { + f.label = whole; + renamed = true; + } + } + } + if !renamed { + break; } } found.sort_by(|a, b| (a.label.to_lowercase(), &a.label, &a.name).cmp(&(b.label.to_lowercase(), &b.label, &b.name))); diff --git a/crates/engine/src/tests/script_folder.rs b/crates/engine/src/tests/script_folder.rs index 24ef85ddf..b176b4f14 100644 --- a/crates/engine/src/tests/script_folder.rs +++ b/crates/engine/src/tests/script_folder.rs @@ -77,10 +77,11 @@ fn scripts_and_subfolders_are_listed_by_name_without_the_extension() { fn a_script_whose_label_another_row_has_shows_its_whole_file_name() { let dir = folder("same"); let root = dir.join("Scripts"); - for name in ["tidy.js", "tidy.jsx", "tidy/x.js", "alone.jsx"] { + for name in ["tidy.js", "tidy.jsx", "tidy.js.js", "tidy/x.js", "alone.jsx"] { touch(&root, name); } - assert_eq!(rows(&folder::list(&root).entries), "alone, tidy/[x], tidy.js, tidy.jsx"); + // `tidy.js.js` is labeled `tidy.js` until `tidy.js` takes that label, then shows its whole name. + assert_eq!(rows(&folder::list(&root).entries), "alone, tidy/[x], tidy.js, tidy.js.js, tidy.jsx"); let _ = std::fs::remove_dir_all(dir); }