Files · Find and look: preview, recent files, search, duplicates and archives

FilePreview shows a file large: its picture in the host's shape, the first
lines of its text, or its kind's icon, over its name, kind, size and
change; the gallery opens it on Space as a quick look. RecentFiles lists
files newest first under a header a day, a list a day. FileSearch sits
over a field the host owns and lists what the host found with its folder.
DuplicateFinder marks copies to remove and keeps the last of each group,
its mark shut, with the bytes the marks free beside Remove. ArchiveViewer
draws an archive's entries as a tree with sizes and savings, from paths
gathered by the folder builder the file tree now shares. A checkbox's
label wraps inside its box instead of running past it. Favorites and the
hex viewer point to documents and debug.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 20:42:06 -04:00
1 parent 4717aad596
commit d76a489934
17 files changed
+1507 -22

No files matched your search

+311
View File
@@ -0,0 +1,311 @@
use gpui::{
AppContext as _, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
TestAppContext, VisualTestContext, Window, div, px,
};
use jiff::{SignedDuration, Timestamp, tz::TimeZone};
use super::{press, settle, setup, then};
use crate::{
files::{
ArchiveEntry, ArchiveViewer, DuplicateFinder, Duplicates, FilePreview, FileSearch,
FoundFile, RecentFile, RecentFiles, archive::saving, duplicates::freed, preview::kind,
recent::day_label,
},
forms::TextInput,
lists::DirEntry,
};
/// Moves focus to the `nth` Tab stop from none.
fn tab_to(nth: usize, cx: &mut VisualTestContext) {
cx.update(|window, _| {
window.blur();
for _ in 0..nth {
window.focus_next();
}
});
settle(cx);
}
#[test]
fn a_kind_reads_from_the_extension() {
assert_eq!(kind(&DirEntry::file("Brief.md", 1, then())), "MD");
assert_eq!(kind(&DirEntry::file("Makefile", 1, then())), "File");
assert_eq!(kind(&DirEntry::file(".env", 1, then())), "File");
assert_eq!(kind(&DirEntry::folder("Design", then())), "Folder");
}
#[test]
fn a_day_reads_as_today_yesterday_or_its_date() {
let today = jiff::civil::date(2026, 9, 22);
assert_eq!(day_label(today, today), "Today");
assert_eq!(
day_label(jiff::civil::date(2026, 9, 21), today),
"Yesterday"
);
assert_eq!(
day_label(jiff::civil::date(2026, 9, 20), today),
"Sunday, September 20"
);
}
#[test]
fn packing_saves_a_whole_percent_and_marks_free_their_bytes() {
assert_eq!(
(saving(1000, 250), saving(0, 0), saving(10, 20)),
(75, 0, 0)
);
let groups = [Duplicates {
name: "a.jpg".into(),
size: 100,
paths: vec!["x/a.jpg".into(), "y/a.jpg".into(), "z/a.jpg".into()],
}];
assert_eq!(freed(&groups, &["y/a.jpg".into(), "z/a.jpg".into()]), 200);
}
/// A preview of a picture four wide over three, 400 across.
struct Looking {
path: std::path::PathBuf,
}
impl Render for Looking {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let entry = DirEntry::file("a.png", 10, then());
let preview = FilePreview::new("preview", entry).picture(self.path.clone(), 4.0 / 3.0);
div().w(px(400.0)).child(preview)
}
}
#[gpui::test]
fn a_picture_keeps_the_hosts_shape(cx: &mut TestAppContext) {
setup(cx);
let path = std::env::temp_dir().join("ely-files-preview.png");
image::RgbaImage::from_pixel(40, 30, image::Rgba([90, 120, 150, 255]))
.save(&path)
.expect("the picture writes");
let (_, cx) = cx.add_window_view(|_, _| Looking { path });
settle(cx);
let stage = cx
.debug_bounds("file-preview-stage")
.expect("the stage draws");
let shape = stage.size.width / stage.size.height;
assert!((shape - 4.0 / 3.0).abs() < 0.02, "{stage:?}");
}
/// Files opened today and yesterday in UTC, and the ones it opened.
struct Recalling {
opened: Vec<SharedString>,
}
impl Render for Recalling {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let owner = cx.entity();
let midnight = Timestamp::now()
.to_zoned(TimeZone::UTC)
.start_of_day()
.expect("a day starts")
.timestamp();
let file = |name: &str, minutes: i64| RecentFile {
name: SharedString::from(name.to_string()),
folder: "Home".into(),
opened: midnight + SignedDuration::from_mins(minutes),
};
let files = [
file("early.md", 5),
file("late.md", -300),
file("later.md", 9),
];
div().w(px(320.0)).child(
RecentFiles::new("recent", files)
.zone(TimeZone::UTC)
.on_open(move |file, _, cx| {
owner.update(cx, |host, _| host.opened.push(file.name.clone()))
}),
)
}
}
#[gpui::test]
fn days_list_the_newest_first_and_enter_opens(cx: &mut TestAppContext) {
setup(cx);
let (host, cx) = cx.add_window_view(|_, _| Recalling { opened: Vec::new() });
cx.update(|window, _| window.activate_window());
settle(cx);
tab_to(1, cx);
press("enter", cx);
tab_to(2, cx);
press("enter", cx);
assert_eq!(
host.read_with(cx, |host, _| host.opened.clone()),
["later.md", "late.md"]
);
}
/// A file search over a field it owns, finding what the test says, and what it opened.
struct Seeking {
query: Entity<TextInput>,
found: Vec<FoundFile>,
opened: Vec<SharedString>,
}
impl Render for Seeking {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let owner = cx.entity();
div().w(px(320.0)).child(
FileSearch::new("search", &self.query, self.found.clone()).on_open(
move |file, _, cx| owner.update(cx, |host, _| host.opened.push(file.name.clone())),
),
)
}
}
fn seeking<'a>(
text: &str,
found: Vec<FoundFile>,
cx: &'a mut TestAppContext,
) -> (Entity<Seeking>, &'a mut VisualTestContext) {
setup(cx);
let text = text.to_string();
let (host, cx) = cx.add_window_view(move |window, cx| {
let query = cx.new(|cx| {
let mut field = TextInput::new(window, cx);
field.set_text(text, cx);
field
});
Seeking {
query,
found,
opened: Vec::new(),
}
});
cx.update(|window, _| window.activate_window());
settle(cx);
(host, cx)
}
#[gpui::test]
fn a_search_with_nothing_found_says_so(cx: &mut TestAppContext) {
let (_, cx) = seeking("zebra", Vec::new(), cx);
assert!(cx.debug_bounds("file-search-note").is_some());
}
#[gpui::test]
fn a_found_file_opens_on_enter(cx: &mut TestAppContext) {
let found = vec![FoundFile {
name: "Brief.md".into(),
folder: "Home/Atrium".into(),
}];
let (host, cx) = seeking("brief", found, cx);
tab_to(3, cx);
press("enter", cx);
assert_eq!(
host.read_with(cx, |host, _| host.opened.clone()),
["Brief.md"]
);
}
/// Two copies of a file with the second marked, and what it was told.
struct Sparing {
removed: Vec<SharedString>,
marks: Vec<(SharedString, bool)>,
}
impl Render for Sparing {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (remove, mark) = (cx.entity(), cx.entity());
let group = Duplicates {
name: "a.jpg".into(),
size: 100,
paths: vec!["x/a.jpg".into(), "y/a.jpg".into()],
};
div().w(px(320.0)).child(
DuplicateFinder::new("dupes", [group])
.marked(["y/a.jpg"])
.on_remove(move |paths, _, cx| {
remove.update(cx, |host, _| host.removed.extend(paths.iter().cloned()))
})
.on_mark(move |path, on, _, cx| {
mark.update(cx, |host, _| host.marks.push((path.clone(), on)))
}),
)
}
}
#[gpui::test]
fn the_last_copy_stays_and_remove_takes_the_marked(cx: &mut TestAppContext) {
setup(cx);
let (host, cx) = cx.add_window_view(|_, _| Sparing {
removed: Vec::new(),
marks: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
tab_to(1, cx);
press("space", cx);
tab_to(2, cx);
press("space", cx);
let heard = host.read_with(cx, |host, _| (host.removed.clone(), host.marks.clone()));
assert_eq!(
heard,
(vec!["y/a.jpg".into()], vec![("y/a.jpg".into(), false)]),
"x/a.jpg is kept, its mark shut"
);
}
#[test]
#[should_panic(expected = "every copy of a.jpg is marked")]
fn a_group_keeps_a_copy() {
let group = Duplicates {
name: "a.jpg".into(),
size: 1,
paths: vec!["x".into(), "y".into()],
};
let _ = DuplicateFinder::new("dupes", [group]).marked(["x", "y"]);
}
/// An archive of four files in two folders, and the files it handed over.
struct Unpacking {
opened: Vec<SharedString>,
}
impl Render for Unpacking {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let owner = cx.entity();
let entry = |path: &str| ArchiveEntry {
path: SharedString::from(path.to_string()),
size: 400,
packed: 100,
};
let entries = ["docs/readme.md", "docs/guide.md", "src/main.rs", "LICENSE"].map(entry);
div().w(px(320.0)).h(px(320.0)).child(
ArchiveViewer::new("archive", "site.zip", entries).on_open(move |entry, _, cx| {
owner.update(cx, |host, _| host.opened.push(entry.path.clone()))
}),
)
}
}
#[gpui::test]
fn a_file_in_an_open_folder_goes_to_the_host_on_enter(cx: &mut TestAppContext) {
setup(cx);
let (host, cx) = cx.add_window_view(|_, _| Unpacking { opened: Vec::new() });
cx.update(|window, _| window.activate_window());
settle(cx);
tab_to(1, cx);
press("down", cx);
press("enter", cx);
assert_eq!(
host.read_with(cx, |host, _| host.opened.clone()),
["docs/guide.md"]
);
}
#[test]
#[should_panic(expected = "LICENSE twice")]
fn an_archive_lists_a_path_once() {
let entry = ArchiveEntry {
path: "LICENSE".into(),
size: 1,
packed: 1,
};
let _ = ArchiveViewer::new("archive", "a.zip", [entry.clone(), entry]);
}
+1
View File
@@ -8,6 +8,7 @@ use super::{FileGrid, item::detail};
use crate::{lists::DirEntry, primitives::FocusNext, theme::Theme};
mod explorer;
mod find;
pub(super) fn setup(cx: &mut TestAppContext) {
cx.update(|cx| {