From c74ec87ed5813280f23047adc536e79d7cb97ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 7 May 2026 19:16:37 -0400 Subject: [PATCH] Select tabs from command scope --- crates/ely_app/src/shell.rs | 26 ++++++++-- crates/ely_browser_core/src/state.rs | 75 +++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/crates/ely_app/src/shell.rs b/crates/ely_app/src/shell.rs index b6514af..9d5d154 100644 --- a/crates/ely_app/src/shell.rs +++ b/crates/ely_app/src/shell.rs @@ -32,8 +32,14 @@ impl ElyShell { let command_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search or enter address")); - let command_subscription = - cx.subscribe(&command_input, |shell: &mut Self, input, event: &InputEvent, cx| { + let command_subscription = cx.subscribe_in( + &command_input, + window, + |shell: &mut Self, input, event: &InputEvent, window, cx| { + let mut submitted_intent = None; + let mut sync_address = false; + let submitted = matches!(event, InputEvent::PressEnter { .. }); + let ShellState::Ready(core) = &mut shell.state else { return; }; @@ -41,12 +47,22 @@ impl ElyShell { let value = input.read(cx).value().to_string(); core.set_command_query(value); - if matches!(event, InputEvent::PressEnter { .. }) { - shell.last_intent = core.submit_command().ok().flatten(); + if submitted { + submitted_intent = core.submit_command().ok().flatten(); + sync_address = core.command_query().is_empty(); + } + + if submitted { + shell.last_intent = submitted_intent; + } + + if sync_address { + shell.sync_address_input(window, cx); } cx.notify(); - }); + }, + ); let state = match InitialBrowserConfig::ely_defaults().and_then(|config| { BrowserCore::new(config).map_err(|error| match error { diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index f554bbc..1f1b5c0 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -1,6 +1,6 @@ use ely_domain::{ - BrowserTab, CommandIntent, DomainError, Profile, ProfileId, ProfileKind, Space, SpaceId, TabId, - UrlText, + BrowserTab, CommandIntent, CommandScope, DomainError, Profile, ProfileId, ProfileKind, Space, + SpaceId, TabId, UrlText, }; use crate::CoreError; @@ -140,6 +140,11 @@ impl BrowserCore { self.command_query = query.into(); } + #[must_use] + pub fn command_query(&self) -> &str { + &self.command_query + } + pub fn submit_command(&mut self) -> Result, CoreError> { let query = self.command_query.trim(); if query.is_empty() { @@ -147,9 +152,18 @@ impl BrowserCore { } let intent = CommandIntent::parse(query)?; - if let CommandIntent::Navigate(url) = &intent { - self.open_tab(url.clone()); - self.command_query.clear(); + match &intent { + CommandIntent::Navigate(url) => { + self.open_tab(url.clone()); + self.command_query.clear(); + } + CommandIntent::ScopedSearch { scope: CommandScope::Tabs, query } => { + if let Some(tab_id) = self.find_tab_match(query) { + self.select_tab(&tab_id)?; + self.command_query.clear(); + } + } + _ => {} } Ok(Some(intent)) @@ -199,6 +213,14 @@ impl BrowserCore { .ok_or(CoreError::MissingActiveTab) } + fn find_tab_match(&self, query: &str) -> Option { + let normalized_query = query.trim().to_lowercase(); + self.tabs + .iter() + .find(|tab| tab_matches_query(tab, &normalized_query)) + .map(|tab| tab.id().clone()) + } + fn build_tab(&self, url: UrlText) -> BrowserTab { let title = tab_title(&url); BrowserTab::new( @@ -219,11 +241,17 @@ fn tab_title(url: &UrlText) -> String { url.display_host() } +fn tab_matches_query(tab: &BrowserTab, normalized_query: &str) -> bool { + tab.title().to_lowercase().contains(normalized_query) + || tab.url().as_str().to_lowercase().contains(normalized_query) + || tab.display_url().to_lowercase().contains(normalized_query) +} + #[cfg(test)] mod tests { use std::error::Error; - use ely_domain::UrlText; + use ely_domain::{CommandIntent, CommandScope, UrlText}; use super::{BrowserCore, InitialBrowserConfig}; use crate::CoreError; @@ -311,4 +339,39 @@ mod tests { assert_eq!(wrapped_tab_id, third_tab_id); Ok(()) } + + #[test] + fn tab_scoped_search_selects_matching_open_tab() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let first_tab_id = core.active_tab()?.id().clone(); + core.open_tab(UrlText::parse("https://example.com")?); + let servo_tab_id = core.open_tab(UrlText::parse("https://servo.org")?); + + core.select_tab(&first_tab_id)?; + core.set_command_query("@tabs servo"); + let intent = core.submit_command()?; + + let snapshot = core.snapshot()?; + assert!(matches!( + intent, + Some(CommandIntent::ScopedSearch { scope: CommandScope::Tabs, query }) if query == "servo" + )); + assert_eq!(snapshot.active_tab_id, servo_tab_id); + assert_eq!(snapshot.command_query, ""); + Ok(()) + } + + #[test] + fn tab_scoped_search_preserves_query_without_match() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let active_tab_id = core.active_tab()?.id().clone(); + + core.set_command_query("@tabs absent"); + core.submit_command()?; + + let snapshot = core.snapshot()?; + assert_eq!(snapshot.active_tab_id, active_tab_id); + assert_eq!(snapshot.command_query, "@tabs absent"); + Ok(()) + } }