ElyMark: clear the recent list from the sidebar

A Clear Recent Files button under the list writes an empty recent.json
(ely_files::recent::clear); a failure is shown, not swallowed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 23:01:48 -04:00
1 parent 15d4c2066d
commit 98692cb80f
5 files changed
+76 -11

No files matched your search

+20
View File
@@ -82,6 +82,26 @@ fn a_file_opens_with_its_outline_and_joins_the_recent_list(cx: &mut TestAppConte
assert!(!kept.contains(r"\\\\?\\"), "plain paths only: {kept}");
}
#[gpui::test]
fn the_recent_list_clears_from_its_button(cx: &mut TestAppContext) {
let (data, files) = (
tempfile::tempdir().expect("data"),
tempfile::tempdir().expect("files"),
);
let path = files.path().join("a.md");
std::fs::write(&path, "Text.\n").expect("a file");
let (desk, cx) = start(cx, data.path());
open(&desk, &path, cx);
assert!(desk.read_with(cx, |desk, _| !desk.recent.is_empty()));
let button = cx.debug_bounds("mark-clear-recent").expect("the button");
cx.simulate_click(button.center(), gpui::Modifiers::none());
cx.run_until_parked();
assert!(desk.read_with(cx, |desk, _| desk.recent.is_empty()));
let kept = std::fs::read_to_string(data.path().join("recent.json")).expect("the list");
assert_eq!(kept, "[]");
assert!(cx.debug_bounds("mark-clear-recent").is_none(), "nothing left to clear");
}
#[gpui::test]
fn the_outline_follows_typing(cx: &mut TestAppContext) {
let (data, files) = (
+16
View File
@@ -108,6 +108,22 @@ impl Desk {
}
}
/// Empties the recent list.
pub fn clear_recent(&mut self, cx: &mut Context<Self>) {
match ely_files::recent::clear(&self.recent_file) {
Ok(()) => {
log::info!("mark: the recent list is cleared");
self.recent.clear();
cx.notify();
}
Err(error) => {
log::error!("mark: the recent list did not clear: {error}");
let said = text(cx, "office.error.engine", &[("error", &error.to_string())]);
self.toast(said, Severity::Danger, cx);
}
}
}
/// Line and column `at` in a tab or file.
pub fn open_at(
&mut self,
+27 -10
View File
@@ -200,7 +200,7 @@ impl Side {
.action(open)
.into_any_element();
}
let opener = desk.downgrade();
let (opener, clearer) = (desk.downgrade(), desk.downgrade());
let rows = read.recent.iter().map(|path| {
let name = path
.file_name()
@@ -214,15 +214,32 @@ impl Side {
);
(key, item)
});
rows.fold(
SelectableList::new("mark-recent").label(text(cx, "mark.recent", &[])),
|list, (key, item)| list.row(key, item),
)
.on_activate(move |key, _, cx| {
let path = PathBuf::from(key.as_ref());
opener.update(cx, |desk, cx| desk.open_path(path, cx)).ok();
})
.into_any_element()
let list = rows
.fold(
SelectableList::new("mark-recent").label(text(cx, "mark.recent", &[])),
|list, (key, item)| list.row(key, item),
)
.on_activate(move |key, _, cx| {
let path = PathBuf::from(key.as_ref());
opener.update(cx, |desk, cx| desk.open_path(path, cx)).ok();
});
let clear = Button::new("mark-clear-recent", text(cx, "mark.recent.clear", &[]))
.size(ControlSize::Sm)
.on_click(move |_, _, cx| {
clearer.update(cx, |desk, cx| desk.clear_recent(cx)).ok();
});
div()
.flex()
.flex_col()
.gap_2()
.size_full()
.child(div().flex_1().min_h_0().child(list))
.child(
div()
.debug_selector(|| "mark-clear-recent".into())
.child(clear),
)
.into_any_element()
}
fn outline(&self, desk: &Entity<Desk>, cx: &mut Context<Self>) -> AnyElement {
+11 -1
View File
@@ -39,6 +39,14 @@ pub fn forget(file: &Path, root: &Path) -> io::Result<()> {
.map(drop)
}
/// Empties the list.
pub fn clear(file: &Path) -> io::Result<()> {
if let Some(dir) = file.parent() {
std::fs::create_dir_all(dir)?;
}
crate::save_atomic(file, b"[]", 0).map(drop)
}
/// Puts `root` first in the list and writes it.
pub fn remember(file: &Path, root: &Path) -> io::Result<Vec<PathBuf>> {
let mut list = recent(file)?;
@@ -60,7 +68,7 @@ pub fn remember(file: &Path, root: &Path) -> io::Result<Vec<PathBuf>> {
mod tests {
use std::path::{Path, PathBuf};
use super::{forget, recent, remember};
use super::{clear, forget, recent, remember};
#[test]
fn the_list_puts_the_newest_first_and_keeps_twenty() {
@@ -82,6 +90,8 @@ mod tests {
let plain = [PathBuf::from(r"C:\r\old"), PathBuf::from(r"C:\r\new")];
assert_eq!(recent(&file).expect("reads"), plain);
}
clear(&file).expect("clears");
assert!(recent(&file).expect("reads").is_empty());
std::fs::write(&file, "not json").expect("writes");
assert!(
remember(&file, Path::new("/r/1")).is_err(),
+2
View File
@@ -6,6 +6,7 @@ pub(super) const EN: &[(&str, &str)] = &[
("mark.pane.search", "Search"),
("mark.recent", "Recent"),
("mark.recent.none", "Nothing opened yet"),
("mark.recent.clear", "Clear Recent Files"),
("mark.outline.none", "No headings"),
("mark.search", "Results"),
("mark.search.file", "This file"),
@@ -106,6 +107,7 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("mark.pane.search", "搜索"),
("mark.recent", "最近"),
("mark.recent.none", "还没有打开过文件"),
("mark.recent.clear", "清除最近文件"),
("mark.outline.none", "没有标题"),
("mark.search", "结果"),
("mark.search.file", "本文件"),