From 26d0e4346c62ffd038766e7d8eb662e1b26759fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 06:03:43 -0400 Subject: [PATCH] Add bookmark metadata updates --- crates/ely_browser_core/src/error.rs | 7 +- .../ely_browser_core/src/state/bookmarks.rs | 39 ++++++++ crates/ely_browser_core/tests/bookmarks.rs | 91 ++++++++++++++++++- crates/ely_domain/src/bookmark.rs | 26 ++++++ 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/crates/ely_browser_core/src/error.rs b/crates/ely_browser_core/src/error.rs index 1861c64..3196313 100644 --- a/crates/ely_browser_core/src/error.rs +++ b/crates/ely_browser_core/src/error.rs @@ -1,4 +1,6 @@ -use ely_domain::{DomainError, DownloadId, PluginId, ProfileId, SpaceId, SplitId, TabId}; +use ely_domain::{ + BookmarkId, DomainError, DownloadId, PluginId, ProfileId, SpaceId, SplitId, TabId, +}; use thiserror::Error; #[derive(Clone, Debug, Error, Eq, PartialEq)] @@ -9,6 +11,9 @@ pub enum CoreError { #[error("tab not found: {id}")] TabNotFound { id: TabId }, + #[error("bookmark not found: {id}")] + BookmarkNotFound { id: BookmarkId }, + #[error("space not found: {id}")] SpaceNotFound { id: SpaceId }, diff --git a/crates/ely_browser_core/src/state/bookmarks.rs b/crates/ely_browser_core/src/state/bookmarks.rs index e0c9843..c316daa 100644 --- a/crates/ely_browser_core/src/state/bookmarks.rs +++ b/crates/ely_browser_core/src/state/bookmarks.rs @@ -31,6 +31,38 @@ impl BrowserCore { Ok(bookmark_id) } + pub fn set_bookmark_collection_name( + &mut self, + bookmark_id: &BookmarkId, + collection_name: impl Into, + ) -> Result<(), CoreError> { + self.bookmark_mut(bookmark_id)?.set_collection_name(collection_name)?; + Ok(()) + } + + pub fn set_bookmark_tags( + &mut self, + bookmark_id: &BookmarkId, + tags: Vec, + ) -> Result<(), CoreError> { + self.bookmark_mut(bookmark_id)?.set_tags(tags)?; + Ok(()) + } + + pub fn set_bookmark_note( + &mut self, + bookmark_id: &BookmarkId, + note: impl Into, + ) -> Result<(), CoreError> { + self.bookmark_mut(bookmark_id)?.set_note(note)?; + Ok(()) + } + + pub fn clear_bookmark_note(&mut self, bookmark_id: &BookmarkId) -> Result<(), CoreError> { + self.bookmark_mut(bookmark_id)?.clear_note(); + Ok(()) + } + pub(super) fn find_bookmark_match(&self, query: &str) -> Option { let normalized_query = query.trim().to_lowercase(); if normalized_query.is_empty() { @@ -52,6 +84,13 @@ impl BrowserCore { .cloned() .collect() } + + fn bookmark_mut(&mut self, bookmark_id: &BookmarkId) -> Result<&mut BookmarkEntry, CoreError> { + self.bookmarks + .iter_mut() + .find(|bookmark| bookmark.id() == bookmark_id) + .ok_or_else(|| CoreError::BookmarkNotFound { id: bookmark_id.clone() }) + } } fn bookmark_matches_query(bookmark: &BookmarkEntry, normalized_query: &str) -> bool { diff --git a/crates/ely_browser_core/tests/bookmarks.rs b/crates/ely_browser_core/tests/bookmarks.rs index f8c96b9..4a010df 100644 --- a/crates/ely_browser_core/tests/bookmarks.rs +++ b/crates/ely_browser_core/tests/bookmarks.rs @@ -1,7 +1,7 @@ use std::error::Error; -use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{CommandIntent, CommandScope, ProfileKind, UrlText}; +use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig}; +use ely_domain::{BookmarkId, CommandIntent, CommandScope, DomainError, ProfileKind, UrlText}; #[test] fn bookmark_active_tab_records_current_context() -> Result<(), Box> { @@ -41,6 +41,68 @@ fn bookmark_active_tab_reuses_existing_bookmark() -> Result<(), Box> Ok(()) } +#[test] +fn bookmark_metadata_updates_collection_tags_and_note() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.open_tab(UrlText::parse("https://example.com/research")?); + let bookmark_id = core.bookmark_active_tab()?; + + core.set_bookmark_collection_name(&bookmark_id, "Research")?; + core.set_bookmark_tags(&bookmark_id, vec![" rust ".to_string(), "gpui".to_string()])?; + core.set_bookmark_note(&bookmark_id, " Read with Servo notes ")?; + let snapshot = core.snapshot()?; + let [bookmark] = snapshot.bookmarks.as_slice() else { + return Err(format!("expected 1 bookmark, got {}", snapshot.bookmarks.len()).into()); + }; + + assert_eq!(bookmark.collection_name(), "Research"); + assert_eq!(bookmark.tags(), &["rust".to_string(), "gpui".to_string()]); + assert_eq!(bookmark.note(), Some("Read with Servo notes")); + + core.clear_bookmark_note(&bookmark_id)?; + let snapshot = core.snapshot()?; + assert_eq!(snapshot.bookmarks[0].note(), None); + Ok(()) +} + +#[test] +fn bookmark_metadata_rejects_empty_fields() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.open_tab(UrlText::parse("https://example.com/research")?); + let bookmark_id = core.bookmark_active_tab()?; + + let Err(collection_error) = core.set_bookmark_collection_name(&bookmark_id, " ") else { + return Err("expected empty bookmark collection error".into()); + }; + assert_eq!( + collection_error, + CoreError::Domain(DomainError::EmptyField { field: "bookmark collection" }) + ); + let Err(tag_error) = + core.set_bookmark_tags(&bookmark_id, vec!["rust".to_string(), " ".to_string()]) + else { + return Err("expected empty bookmark tag error".into()); + }; + assert_eq!(tag_error, CoreError::Domain(DomainError::EmptyField { field: "bookmark tag" })); + let Err(note_error) = core.set_bookmark_note(&bookmark_id, " ") else { + return Err("expected empty bookmark note error".into()); + }; + assert_eq!(note_error, CoreError::Domain(DomainError::EmptyField { field: "bookmark note" })); + Ok(()) +} + +#[test] +fn bookmark_metadata_requires_known_bookmark() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let unknown_bookmark_id = BookmarkId::new(); + + let Err(bookmark_error) = core.set_bookmark_note(&unknown_bookmark_id, "Read later") else { + return Err("expected unknown bookmark error".into()); + }; + assert_eq!(bookmark_error, CoreError::BookmarkNotFound { id: unknown_bookmark_id }); + Ok(()) +} + #[test] fn bookmarks_scoped_search_opens_matching_bookmark() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; @@ -64,6 +126,31 @@ fn bookmarks_scoped_search_opens_matching_bookmark() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.open_tab(UrlText::parse("https://example.com/research")?); + let bookmark_id = core.bookmark_active_tab()?; + + core.set_bookmark_collection_name(&bookmark_id, "Research")?; + core.set_bookmark_tags(&bookmark_id, vec!["gpui".to_string()])?; + core.set_bookmark_note(&bookmark_id, "Servo embed reference")?; + + core.set_command_query("@bookmarks gpui"); + let intent = core.submit_command()?; + let active_tab = core.active_tab()?; + + assert_eq!( + intent, + Some(CommandIntent::ScopedSearch { + scope: CommandScope::Bookmarks, + query: "gpui".to_string() + }) + ); + assert_eq!(active_tab.url().as_str(), "https://example.com/research"); + Ok(()) +} + #[test] fn bookmarks_stay_with_active_profile() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/crates/ely_domain/src/bookmark.rs b/crates/ely_domain/src/bookmark.rs index 46e59f6..44794b0 100644 --- a/crates/ely_domain/src/bookmark.rs +++ b/crates/ely_domain/src/bookmark.rs @@ -89,6 +89,28 @@ impl BookmarkEntry { pub fn added_at(&self) -> SystemTime { self.added_at } + + pub fn set_collection_name( + &mut self, + collection_name: impl Into, + ) -> Result<(), DomainError> { + self.collection_name = non_empty_text("bookmark collection", collection_name.into())?; + Ok(()) + } + + pub fn set_tags(&mut self, tags: Vec) -> Result<(), DomainError> { + self.tags = normalize_tags(tags)?; + Ok(()) + } + + pub fn set_note(&mut self, note: impl Into) -> Result<(), DomainError> { + self.note = Some(non_empty_text("bookmark note", note.into())?); + Ok(()) + } + + pub fn clear_note(&mut self) { + self.note = None; + } } fn non_empty_text(field: &'static str, value: String) -> Result { @@ -98,3 +120,7 @@ fn non_empty_text(field: &'static str, value: String) -> Result) -> Result, DomainError> { + tags.into_iter().map(|tag| non_empty_text("bookmark tag", tag)).collect() +}