Add Bookmarks section to command overlay

Design's command switcher exposes Open tabs / History / Bookmarks /
Actions. The implementation now includes a Bookmarks section sourced
from snapshot.bookmarks: filtered by lowercase needle on title and
URL, capped at RESULT_LIMIT, rendered with brand glyphs, opening the
URL on click.

Match helpers (matching_tabs / matching_history / matching_bookmarks)
move into chrome::command_match so command_overlay.rs stays under the
500-line ceiling. Ask ELY remains skipped — no AI surface in domain
yet, no fabrication.
This commit is contained in:
2026-05-09 19:28:18 -04:00
parent 3903004ede
commit 6419609eba
3 changed files with 112 additions and 41 deletions
@@ -0,0 +1,68 @@
use ely_browser_core::BrowserSnapshot;
use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry};
pub(crate) const RESULT_LIMIT: usize = 4;
pub(crate) fn matching_tabs<'a>(
snapshot: &'a BrowserSnapshot,
needle: &str,
) -> Vec<&'a BrowserTab> {
if needle.is_empty() {
return snapshot.tabs.iter().take(RESULT_LIMIT).collect();
}
snapshot
.tabs
.iter()
.filter(|tab| matches_tab(tab, needle))
.take(RESULT_LIMIT)
.collect()
}
fn matches_tab(tab: &BrowserTab, needle: &str) -> bool {
tab.title().to_lowercase().contains(needle)
|| tab.url().as_str().to_lowercase().contains(needle)
}
pub(crate) fn matching_history<'a>(
snapshot: &'a BrowserSnapshot,
needle: &str,
) -> Vec<&'a HistoryEntry> {
if needle.is_empty() {
return snapshot.history_entries.iter().rev().take(RESULT_LIMIT).collect();
}
snapshot
.history_entries
.iter()
.rev()
.filter(|entry| matches_history(entry, needle))
.take(RESULT_LIMIT)
.collect()
}
fn matches_history(entry: &HistoryEntry, needle: &str) -> bool {
entry.title().to_lowercase().contains(needle)
|| entry.url().as_str().to_lowercase().contains(needle)
}
pub(crate) fn matching_bookmarks<'a>(
snapshot: &'a BrowserSnapshot,
needle: &str,
) -> Vec<&'a BookmarkEntry> {
if needle.is_empty() {
return snapshot.bookmarks.iter().take(RESULT_LIMIT).collect();
}
snapshot
.bookmarks
.iter()
.filter(|bookmark| matches_bookmark(bookmark, needle))
.take(RESULT_LIMIT)
.collect()
}
fn matches_bookmark(bookmark: &BookmarkEntry, needle: &str) -> bool {
bookmark.title().to_lowercase().contains(needle)
|| bookmark.url().as_str().to_lowercase().contains(needle)
}
@@ -1,6 +1,6 @@
use ely_browser_core::BrowserSnapshot; use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors; use ely_design_system::colors;
use ely_domain::{BrowserTab, HistoryEntry}; use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry};
use gpui::{ use gpui::{
AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement,
SharedString, StatefulInteractiveElement, Styled, div, hsla, point, px, rgb, rgba, SharedString, StatefulInteractiveElement, Styled, div, hsla, point, px, rgb, rgba,
@@ -9,10 +9,12 @@ use gpui_component::IconName;
use crate::shell::ElyShell; use crate::shell::ElyShell;
use crate::shell::chrome::command_footer::{render_command_footer, render_kbd}; use crate::shell::chrome::command_footer::{render_command_footer, render_kbd};
use crate::shell::chrome::command_match::{
matching_bookmarks, matching_history, matching_tabs,
};
use crate::shell::chrome::render_glyph_for; use crate::shell::chrome::render_glyph_for;
const COMMAND_PREFIX: &str = ">"; const COMMAND_PREFIX: &str = ">";
const RESULT_LIMIT: usize = 4;
pub(crate) fn render_command_overlay( pub(crate) fn render_command_overlay(
snapshot: &BrowserSnapshot, snapshot: &BrowserSnapshot,
@@ -114,6 +116,7 @@ fn render_results(
) -> AnyElement { ) -> AnyElement {
let tabs = matching_tabs(snapshot, needle); let tabs = matching_tabs(snapshot, needle);
let history = matching_history(snapshot, needle); let history = matching_history(snapshot, needle);
let bookmarks = matching_bookmarks(snapshot, needle);
let actions = matching_actions(needle); let actions = matching_actions(needle);
let mut sections: Vec<AnyElement> = Vec::new(); let mut sections: Vec<AnyElement> = Vec::new();
@@ -123,6 +126,12 @@ fn render_results(
if !history.is_empty() { if !history.is_empty() {
sections.push(render_section("History", render_history_rows(history, cx))); sections.push(render_section("History", render_history_rows(history, cx)));
} }
if !bookmarks.is_empty() {
sections.push(render_section(
"Bookmarks",
render_bookmark_rows(bookmarks, cx),
));
}
if !actions.is_empty() { if !actions.is_empty() {
sections.push(render_section("Actions", render_action_rows(actions, cx))); sections.push(render_section("Actions", render_action_rows(actions, cx)));
} }
@@ -420,46 +429,39 @@ fn render_empty_state() -> AnyElement {
.into_any_element() .into_any_element()
} }
fn matching_tabs<'a>( fn render_bookmark_rows(
snapshot: &'a BrowserSnapshot, entries: Vec<&BookmarkEntry>,
needle: &str, cx: &mut Context<ElyShell>,
) -> Vec<&'a BrowserTab> { ) -> AnyElement {
if needle.is_empty() { div()
return snapshot.tabs.iter().take(RESULT_LIMIT).collect(); .flex()
} .flex_col()
.children(entries.into_iter().enumerate().map(|(index, bookmark)| {
let url = bookmark.url().clone();
let title = bookmark.title().to_string();
let host = bookmark.url().host().map(|host| host.to_string());
let display = host
.clone()
.unwrap_or_else(|| bookmark.url().as_str().to_string());
let initial = title.chars().next().unwrap_or('?').to_string();
snapshot render_row_with_glyph(
.tabs CommandRowContent {
.iter() id: format!("cmd-bookmark-{index}"),
.filter(|tab| matches_tab(tab, needle)) title,
.take(RESULT_LIMIT) hint: Some(display),
.collect() keys: None,
} },
host.as_deref(),
fn matches_tab(tab: &BrowserTab, needle: &str) -> bool { &initial,
tab.title().to_lowercase().contains(needle) || tab.url().as_str().to_lowercase().contains(needle) cx,
} move |shell, window, cx| {
shell.open_internal_tab(url.as_str(), window, cx);
fn matching_history<'a>( shell.dismiss_command_mode(window, cx);
snapshot: &'a BrowserSnapshot, },
needle: &str, )
) -> Vec<&'a HistoryEntry> { }))
if needle.is_empty() { .into_any_element()
return snapshot.history_entries.iter().rev().take(RESULT_LIMIT).collect();
}
snapshot
.history_entries
.iter()
.rev()
.filter(|entry| matches_history(entry, needle))
.take(RESULT_LIMIT)
.collect()
}
fn matches_history(entry: &HistoryEntry, needle: &str) -> bool {
entry.title().to_lowercase().contains(needle)
|| entry.url().as_str().to_lowercase().contains(needle)
} }
const PANEL_BG: u32 = 0xfffffff5; const PANEL_BG: u32 = 0xfffffff5;
+1
View File
@@ -2,6 +2,7 @@ pub(crate) mod appearance_form;
pub(crate) mod appearance_layout_cards; pub(crate) mod appearance_layout_cards;
pub(crate) mod brand_glyph; pub(crate) mod brand_glyph;
pub(crate) mod command_footer; pub(crate) mod command_footer;
pub(crate) mod command_match;
pub(crate) mod command_overlay; pub(crate) mod command_overlay;
pub(crate) mod home; pub(crate) mod home;
pub(crate) mod plugin_detail_view; pub(crate) mod plugin_detail_view;