From a06ab459110d78d72e09c01625d236c5176c17db Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 10 Nov 2026 18:48:59 -0800 Subject: [PATCH] Scripts: every script label in a Scripts folder level stays distinct A script renamed to its whole file name could take the label another script kept, as with tidy.js, tidy.jsx and tidy.js.js in one folder. The rename now repeats until no label is shared. A folder entry whose type cannot be read now marks the listing as incomplete. --- crates/engine/src/script/folder.rs | 37 +++++++++++++++++------- crates/engine/src/tests/script_folder.rs | 5 ++-- 2 files changed, 29 insertions(+), 13 deletions(-) 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); }