From 98692cb80f3a8b6324a97e7e18237e2c643c1624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 14 Oct 2026 23:01:48 -0400 Subject: [PATCH] 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) --- apps/ely-mark/src/desk/tests.rs | 20 ++++++++++++++++++ apps/ely-mark/src/places.rs | 16 ++++++++++++++ apps/ely-mark/src/side.rs | 37 ++++++++++++++++++++++++--------- crates/ely-files/src/recent.rs | 12 ++++++++++- crates/ely-ui/src/i18n/mark.rs | 2 ++ 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/apps/ely-mark/src/desk/tests.rs b/apps/ely-mark/src/desk/tests.rs index 23d3f70..08ec3c0 100644 --- a/apps/ely-mark/src/desk/tests.rs +++ b/apps/ely-mark/src/desk/tests.rs @@ -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) = ( diff --git a/apps/ely-mark/src/places.rs b/apps/ely-mark/src/places.rs index 5245b8a..b7b8e0c 100644 --- a/apps/ely-mark/src/places.rs +++ b/apps/ely-mark/src/places.rs @@ -108,6 +108,22 @@ impl Desk { } } + /// Empties the recent list. + pub fn clear_recent(&mut self, cx: &mut Context) { + 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, diff --git a/apps/ely-mark/src/side.rs b/apps/ely-mark/src/side.rs index 7613e1f..0bb1162 100644 --- a/apps/ely-mark/src/side.rs +++ b/apps/ely-mark/src/side.rs @@ -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, cx: &mut Context) -> AnyElement { diff --git a/crates/ely-files/src/recent.rs b/crates/ely-files/src/recent.rs index c8f2640..27586e8 100644 --- a/crates/ely-files/src/recent.rs +++ b/crates/ely-files/src/recent.rs @@ -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> { let mut list = recent(file)?; @@ -60,7 +68,7 @@ pub fn remember(file: &Path, root: &Path) -> io::Result> { 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(), diff --git a/crates/ely-ui/src/i18n/mark.rs b/crates/ely-ui/src/i18n/mark.rs index 7e7cbb5..c7bab86 100644 --- a/crates/ely-ui/src/i18n/mark.rs +++ b/crates/ely-ui/src/i18n/mark.rs @@ -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", "本文件"),