Files: recent and found files keep the cursor on the file, not its place
RecentFiles and FileSearch keyed their rows by place in the list, so when an open moved a file to the top, the cursor stayed at the place and the next Enter opened another file. Rows are keyed by the file's folder and name, asserted once a list, so the cursor follows the file. A dotfile's kind reads by its extension, as its icon does.
This commit is contained in:
1 parent
d76a489934
commit
b280929d07
5 files changed
+128
-8
No files matched your search
@@ -325,3 +325,4 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
| T27a | 2 | PASS | |
|
||||
| T27b | 1 | FAIL | the listing's fixed columns left no room for names at 280px; a column keyed by its depth handed its cursor to the next folder there |
|
||||
| T27b | 2 | PASS | |
|
||||
| T27c | 1 | FAIL | recent and found files were keyed by their place, so the cursor left a file an open moved |
|
||||
@@ -30,7 +30,7 @@ pub(crate) fn kind(entry: &DirEntry) -> String {
|
||||
return "Folder".into();
|
||||
}
|
||||
match entry.name().rsplit_once('.') {
|
||||
Some((stem, extension)) if !stem.is_empty() => extension.to_uppercase(),
|
||||
Some((_, extension)) if !extension.is_empty() => extension.to_uppercase(),
|
||||
_ => "File".into(),
|
||||
}
|
||||
}
|
||||
|
||||
+21
-3
@@ -23,6 +23,13 @@ pub struct RecentFile {
|
||||
pub opened: Timestamp,
|
||||
}
|
||||
|
||||
impl RecentFile {
|
||||
/// Where it lives: its folder and name.
|
||||
fn place(&self) -> SharedString {
|
||||
format!("{}/{}", self.folder, self.name).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The header over a day's files: Today, Yesterday, or the weekday and date.
|
||||
pub(crate) fn day_label(day: Date, today: Date) -> String {
|
||||
if day == today {
|
||||
@@ -45,9 +52,17 @@ pub struct RecentFiles {
|
||||
|
||||
impl RecentFiles {
|
||||
pub fn new(id: impl Into<ElementId>, files: impl IntoIterator<Item = RecentFile>) -> Self {
|
||||
let files: Vec<RecentFile> = files.into_iter().collect();
|
||||
for (ix, file) in files.iter().enumerate() {
|
||||
let place = file.place();
|
||||
assert!(
|
||||
!files[..ix].iter().any(|other| other.place() == place),
|
||||
"{place} twice"
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
files: files.into_iter().collect(),
|
||||
files,
|
||||
zone: None,
|
||||
on_open: None,
|
||||
}
|
||||
@@ -96,7 +111,7 @@ impl RenderOnce for RecentFiles {
|
||||
.leading(FileIcon::file(&file.name))
|
||||
.description(file.folder.clone())
|
||||
.trailing(time);
|
||||
list.row(SharedString::from(ix.to_string()), row)
|
||||
list.row(file.place(), row)
|
||||
},
|
||||
);
|
||||
let (id, files, on_open) = (self.id.clone(), files.clone(), self.on_open.clone());
|
||||
@@ -113,7 +128,10 @@ impl RenderOnce for RecentFiles {
|
||||
.child(day_label(day, today)),
|
||||
)
|
||||
.child(list.on_activate(move |key, window, cx| {
|
||||
let file = &files[key.parse::<usize>().expect("a row's place")];
|
||||
let file = files
|
||||
.iter()
|
||||
.find(|file| file.place() == *key)
|
||||
.expect("a listed file");
|
||||
log::info!("recent files {id:?}: open {}", file.name);
|
||||
if let Some(on_open) = &on_open {
|
||||
on_open(file, window, cx);
|
||||
|
||||
+21
-3
@@ -21,6 +21,13 @@ pub struct FoundFile {
|
||||
pub folder: SharedString,
|
||||
}
|
||||
|
||||
impl FoundFile {
|
||||
/// Where it lives: its folder and name.
|
||||
fn place(&self) -> SharedString {
|
||||
format!("{}/{}", self.folder, self.name).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A search over files: a field the host owns, and what it found, each file with its folder and a count above. The host searches as the text changes; Enter or a double press opens a file.
|
||||
#[derive(IntoElement)]
|
||||
pub struct FileSearch {
|
||||
@@ -36,10 +43,18 @@ impl FileSearch {
|
||||
query: &Entity<TextInput>,
|
||||
found: impl IntoIterator<Item = FoundFile>,
|
||||
) -> Self {
|
||||
let found: Vec<FoundFile> = found.into_iter().collect();
|
||||
for (ix, file) in found.iter().enumerate() {
|
||||
let place = file.place();
|
||||
assert!(
|
||||
!found[..ix].iter().any(|other| other.place() == place),
|
||||
"{place} twice"
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
query: query.clone(),
|
||||
found: found.into_iter().collect(),
|
||||
found,
|
||||
on_open: None,
|
||||
}
|
||||
}
|
||||
@@ -79,7 +94,7 @@ impl RenderOnce for FileSearch {
|
||||
)
|
||||
.leading(FileIcon::file(&file.name))
|
||||
.description(file.folder.clone());
|
||||
list.row(SharedString::from(ix.to_string()), row)
|
||||
list.row(file.place(), row)
|
||||
},
|
||||
);
|
||||
let (id, shown, on_open) = (self.id.clone(), found.clone(), self.on_open);
|
||||
@@ -93,7 +108,10 @@ impl RenderOnce for FileSearch {
|
||||
.gap_1()
|
||||
.child(quiet(count))
|
||||
.child(list.on_activate(move |key, window, cx| {
|
||||
let file = &shown[key.parse::<usize>().expect("a row's place")];
|
||||
let file = shown
|
||||
.iter()
|
||||
.find(|file| file.place() == *key)
|
||||
.expect("a listed file");
|
||||
log::info!("file search {id:?}: open {}", file.name);
|
||||
if let Some(on_open) = &on_open {
|
||||
on_open(file, window, cx);
|
||||
|
||||
+84
-1
@@ -30,7 +30,11 @@ fn tab_to(nth: usize, cx: &mut VisualTestContext) {
|
||||
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::file(".env", 1, then())),
|
||||
"ENV",
|
||||
"as the icon map reads it"
|
||||
);
|
||||
assert_eq!(kind(&DirEntry::folder("Design", then())), "Folder");
|
||||
}
|
||||
|
||||
@@ -309,3 +313,82 @@ fn an_archive_lists_a_path_once() {
|
||||
};
|
||||
let _ = ArchiveViewer::new("archive", "a.zip", [entry.clone(), entry]);
|
||||
}
|
||||
|
||||
/// Three files opened today; an open moves a file to the top, as a host records it.
|
||||
struct Reopening {
|
||||
files: Vec<RecentFile>,
|
||||
opened: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Render for Reopening {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let owner = cx.entity();
|
||||
div().w(px(320.0)).child(
|
||||
RecentFiles::new("recent", self.files.clone())
|
||||
.zone(TimeZone::UTC)
|
||||
.on_open(move |file, _, cx| {
|
||||
owner.update(cx, |host, cx| {
|
||||
host.opened.push(file.name.clone());
|
||||
let now = Timestamp::now();
|
||||
for kept in &mut host.files {
|
||||
if kept.name == file.name {
|
||||
kept.opened = now;
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_cursor_stays_on_a_file_an_open_moves(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
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 = vec![file("a.md", 3), file("b.md", 2), file("c.md", 1)];
|
||||
let (host, cx) = cx.add_window_view(move |_, _| Reopening {
|
||||
files,
|
||||
opened: Vec::new(),
|
||||
});
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
tab_to(1, cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(
|
||||
host.read_with(cx, |host, _| host.opened.clone()),
|
||||
["b.md", "b.md"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Home/a.md twice")]
|
||||
fn a_recent_file_is_listed_once() {
|
||||
let file = RecentFile {
|
||||
name: "a.md".into(),
|
||||
folder: "Home".into(),
|
||||
opened: then(),
|
||||
};
|
||||
let _ = RecentFiles::new("recent", [file.clone(), file]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[should_panic(expected = "Home/a.md twice")]
|
||||
fn a_found_file_is_listed_once(cx: &mut TestAppContext) {
|
||||
let file = FoundFile {
|
||||
name: "a.md".into(),
|
||||
folder: "Home".into(),
|
||||
};
|
||||
let _ = seeking("a", vec![file.clone(), file], cx);
|
||||
}
|
||||
Reference in new issue
Block a user