Record browsing history in core

This commit is contained in:
2026-05-07 21:09:00 -04:00
parent 3bcf4460ca
commit d26ebdfb4c
8 changed files with 226 additions and 2 deletions
@@ -32,6 +32,10 @@ pub(crate) fn tab_matches_query(tab: &BrowserTab, normalized_query: &str) -> boo
|| tab.display_url().to_lowercase().contains(normalized_query) || tab.display_url().to_lowercase().contains(normalized_query)
} }
pub(crate) fn records_history(url: &UrlText) -> bool {
Url::parse(url.as_str()).map(|parsed_url| parsed_url.scheme() != "ely").unwrap_or(false)
}
pub(crate) fn new_space_name(command: &str) -> Option<&str> { pub(crate) fn new_space_name(command: &str) -> Option<&str> {
command_argument(command, &["new-space ", "new space "]) command_argument(command, &["new-space ", "new space "])
} }
+7 -2
View File
@@ -1,13 +1,14 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use ely_domain::{ use ely_domain::{
ArchivedTab, BrowserTab, DomainError, Profile, ProfileId, ProfileKind, Space, SpaceId, TabId, ArchivedTab, BrowserTab, DomainError, HistoryEntry, Profile, ProfileId, ProfileKind, Space,
UrlText, SpaceId, TabId, UrlText,
}; };
use crate::CoreError; use crate::CoreError;
mod commands; mod commands;
mod history;
mod profiles; mod profiles;
mod tabs; mod tabs;
@@ -36,6 +37,7 @@ pub struct BrowserSnapshot {
pub favorites: Vec<BrowserTab>, pub favorites: Vec<BrowserTab>,
pub pinned_tabs: Vec<BrowserTab>, pub pinned_tabs: Vec<BrowserTab>,
pub archived_tabs: Vec<ArchivedTab>, pub archived_tabs: Vec<ArchivedTab>,
pub history_entries: Vec<HistoryEntry>,
pub spaces: Vec<Space>, pub spaces: Vec<Space>,
pub active_tab_id: TabId, pub active_tab_id: TabId,
pub active_space_id: SpaceId, pub active_space_id: SpaceId,
@@ -50,6 +52,7 @@ pub struct BrowserCore {
profiles: Vec<Profile>, profiles: Vec<Profile>,
tabs: Vec<BrowserTab>, tabs: Vec<BrowserTab>,
archived_tabs: Vec<ArchivedTab>, archived_tabs: Vec<ArchivedTab>,
history_entries: Vec<HistoryEntry>,
active_space_id: SpaceId, active_space_id: SpaceId,
active_profile_id: ProfileId, active_profile_id: ProfileId,
active_tab_id: TabId, active_tab_id: TabId,
@@ -90,6 +93,7 @@ impl BrowserCore {
profiles: vec![profile], profiles: vec![profile],
tabs: vec![tab], tabs: vec![tab],
archived_tabs: Vec::new(), archived_tabs: Vec::new(),
history_entries: Vec::new(),
command_query: String::new(), command_query: String::new(),
new_tab_url, new_tab_url,
}) })
@@ -175,6 +179,7 @@ impl BrowserCore {
favorites: self.favorites(), favorites: self.favorites(),
pinned_tabs: self.pinned_tabs(), pinned_tabs: self.pinned_tabs(),
archived_tabs: self.archived_tabs.clone(), archived_tabs: self.archived_tabs.clone(),
history_entries: self.visible_history(),
spaces: self.spaces.clone(), spaces: self.spaces.clone(),
tabs: self.visible_tabs(), tabs: self.visible_tabs(),
active_tab_id: self.active_tab_id.clone(), active_tab_id: self.active_tab_id.clone(),
@@ -44,6 +44,12 @@ impl BrowserCore {
self.command_query.clear(); self.command_query.clear();
} }
} }
CommandIntent::ScopedSearch { scope: CommandScope::History, query } => {
if let Some(url) = self.find_history_match(query) {
self.open_tab(url);
self.command_query.clear();
}
}
CommandIntent::ScopedSearch { scope: CommandScope::Settings, query } => { CommandIntent::ScopedSearch { scope: CommandScope::Settings, query } => {
if let Some(url) = settings_page_url(query)? { if let Some(url) = settings_page_url(query)? {
self.open_tab(url); self.open_tab(url);
@@ -0,0 +1,55 @@
use std::time::SystemTime;
use ely_domain::{BrowserTab, HistoryEntry, UrlText};
use crate::navigation::records_history;
use super::BrowserCore;
impl BrowserCore {
pub(super) fn record_history_entry(&mut self, tab: &BrowserTab) {
if !records_history(tab.url()) {
return;
}
self.history_entries.push(HistoryEntry::new(
tab.profile_id().clone(),
tab.space_id().clone(),
tab.title(),
tab.url().clone(),
SystemTime::now(),
));
}
pub(super) fn find_history_match(&self, query: &str) -> Option<UrlText> {
let normalized_query = query.trim().to_lowercase();
if normalized_query.is_empty() {
return None;
}
self.history_entries
.iter()
.rev()
.find(|entry| {
entry.profile_id() == &self.active_profile_id
&& entry.space_id() == &self.active_space_id
&& history_entry_matches_query(entry, &normalized_query)
})
.map(|entry| entry.url().clone())
}
pub(super) fn visible_history(&self) -> Vec<HistoryEntry> {
self.history_entries
.iter()
.filter(|entry| entry.profile_id() == &self.active_profile_id)
.filter(|entry| entry.space_id() == &self.active_space_id)
.cloned()
.collect()
}
}
fn history_entry_matches_query(entry: &HistoryEntry, normalized_query: &str) -> bool {
entry.title().to_lowercase().contains(normalized_query)
|| entry.url().as_str().to_lowercase().contains(normalized_query)
|| entry.url().display_url().to_lowercase().contains(normalized_query)
}
@@ -18,6 +18,7 @@ impl BrowserCore {
.iter() .iter()
.position(|existing| existing.id() == &self.active_tab_id) .position(|existing| existing.id() == &self.active_tab_id)
.map_or(self.tabs.len(), |index| index + 1); .map_or(self.tabs.len(), |index| index + 1);
self.record_history_entry(&tab);
self.tabs.insert(insert_index, tab); self.tabs.insert(insert_index, tab);
self.active_tab_id = tab_id.clone(); self.active_tab_id = tab_id.clone();
self.active_tabs_by_space.insert(self.active_space_id.clone(), tab_id.clone()); self.active_tabs_by_space.insert(self.active_space_id.clone(), tab_id.clone());
+101
View File
@@ -0,0 +1,101 @@
use std::error::Error;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{CommandIntent, CommandScope, ProfileKind, UrlText};
#[test]
fn navigation_records_profile_and_space_history() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let active_profile_id = core.active_tab()?.profile_id().clone();
let active_space_id = core.snapshot()?.active_space_id;
core.open_tab(UrlText::parse("https://example.com/research")?);
let snapshot = core.snapshot()?;
assert_eq!(snapshot.history_entries.len(), 1);
assert_eq!(snapshot.history_entries[0].profile_id(), &active_profile_id);
assert_eq!(snapshot.history_entries[0].space_id(), &active_space_id);
assert_eq!(snapshot.history_entries[0].title(), "example.com");
assert_eq!(snapshot.history_entries[0].url().as_str(), "https://example.com/research");
Ok(())
}
#[test]
fn internal_pages_are_omitted_from_history() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
core.open_tab(UrlText::parse("ely://history")?);
assert!(core.snapshot()?.history_entries.is_empty());
Ok(())
}
#[test]
fn history_scoped_search_opens_recent_matching_entry() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
core.open_tab(UrlText::parse("https://example.com/research")?);
core.set_command_query("@history example");
let intent = core.submit_command()?;
let snapshot = core.snapshot()?;
let active_tab = core.active_tab()?;
assert_eq!(
intent,
Some(CommandIntent::ScopedSearch {
scope: CommandScope::History,
query: "example".to_string()
})
);
assert_eq!(active_tab.url().as_str(), "https://example.com/research");
assert_eq!(snapshot.command_query, "");
assert_eq!(snapshot.history_entries.len(), 2);
Ok(())
}
#[test]
fn history_scoped_search_preserves_query_without_match() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let active_tab_id = core.open_tab(UrlText::parse("https://example.com/research")?);
core.set_command_query("@history absent");
let intent = core.submit_command()?;
let snapshot = core.snapshot()?;
assert_eq!(
intent,
Some(CommandIntent::ScopedSearch {
scope: CommandScope::History,
query: "absent".to_string()
})
);
assert_eq!(snapshot.active_tab_id, active_tab_id);
assert_eq!(snapshot.command_query, "@history absent");
Ok(())
}
#[test]
fn history_scoped_search_stays_with_active_profile() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let default_profile_id = core.active_tab()?.profile_id().clone();
let personal_profile_id = core.create_profile("Personal", 0xf54e00, ProfileKind::Standard)?;
core.open_tab(UrlText::parse("https://example.com/personal")?);
core.select_profile(&default_profile_id)?;
core.set_command_query("@history personal");
let intent = core.submit_command()?;
let snapshot = core.snapshot()?;
assert_eq!(
intent,
Some(CommandIntent::ScopedSearch {
scope: CommandScope::History,
query: "personal".to_string()
})
);
assert_eq!(core.active_tab()?.profile_id(), &default_profile_id);
assert_ne!(core.active_tab()?.profile_id(), &personal_profile_id);
assert_eq!(snapshot.command_query, "@history personal");
Ok(())
}
+50
View File
@@ -0,0 +1,50 @@
use std::time::SystemTime;
use crate::{ProfileId, SpaceId, UrlText};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HistoryEntry {
profile_id: ProfileId,
space_id: SpaceId,
title: String,
url: UrlText,
visited_at: SystemTime,
}
impl HistoryEntry {
#[must_use]
pub fn new(
profile_id: ProfileId,
space_id: SpaceId,
title: impl Into<String>,
url: UrlText,
visited_at: SystemTime,
) -> Self {
Self { profile_id, space_id, title: title.into(), url, visited_at }
}
#[must_use]
pub fn profile_id(&self) -> &ProfileId {
&self.profile_id
}
#[must_use]
pub fn space_id(&self) -> &SpaceId {
&self.space_id
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub fn url(&self) -> &UrlText {
&self.url
}
#[must_use]
pub fn visited_at(&self) -> SystemTime {
self.visited_at
}
}
+2
View File
@@ -1,6 +1,7 @@
mod archive; mod archive;
mod command; mod command;
mod error; mod error;
mod history;
mod identifiers; mod identifiers;
mod profile; mod profile;
mod space; mod space;
@@ -11,6 +12,7 @@ mod url_text;
pub use archive::{ArchiveSource, ArchivedTab}; pub use archive::{ArchiveSource, ArchivedTab};
pub use command::{CommandIntent, CommandScope}; pub use command::{CommandIntent, CommandScope};
pub use error::DomainError; pub use error::DomainError;
pub use history::HistoryEntry;
pub use identifiers::{ProfileId, SpaceId, SplitId, TabId, WebViewId}; pub use identifiers::{ProfileId, SpaceId, SplitId, TabId, WebViewId};
pub use profile::{Profile, ProfileKind}; pub use profile::{Profile, ProfileKind};
pub use space::{ArchivePolicy, Space}; pub use space::{ArchivePolicy, Space};