//! The settings search: every searchable row, its page and the words people
//! may remember it by, ranked like T3 Code's (`settingsSearch.ts`).
use super::{Page, keybindings};
/// One search result: a row (or section) the search can jump to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Item {
/// The row's anchor.
pub id: &'static str,
pub title: &'static str,
pub page: Page,
/// Descriptions, option labels and aliases.
pub terms: &'static str,
/// The folded section the row sits in, opened on the jump.
pub fold: Option<&'static str>,
/// Sorts after every other match, like keybinding commands.
pub secondary: bool,
/// Where to jump instead when the row isn't shown, such as a row only
/// one choice of another reveals.
pub fallback: Option<&'static str>,
}
const fn item(id: &'static str, title: &'static str, page: Page, terms: &'static str) -> Item {
Item { id, title, page, terms, fold: None, secondary: false, fallback: None }
}
/// A row that's only shown sometimes; the jump falls back to `fallback`.
const fn revealed(id: &'static str, title: &'static str, page: Page, terms: &'static str, fallback: &'static str) -> Item {
Item { id, title, page, terms, fold: None, secondary: false, fallback: Some(fallback) }
}
const fn folded(id: &'static str, title: &'static str, page: Page, terms: &'static str, fold: &'static str) -> Item {
Item { id, title, page, terms, fold: Some(fold), secondary: false, fallback: None }
}
/// Searchable settings, in result order.
pub const ITEMS: &[Item] = &[
item("storage-worktrees-location", "Worktree location", Page::Storage, "worktree location folder directory path drive external disk"),
item("storage-worktrees-deleted", "Delete worktrees with deleted threads", Page::Storage, "worktree cleanup remove delete thread automatic"),
item("storage-worktrees-inactive", "Delete inactive worktrees", Page::Storage, "worktree cleanup remove inactive days retention automatic"),
item("storage-worktrees-merged", "Delete merged worktrees", Page::Storage, "worktree cleanup remove merged pull request automatic"),
item("storage-worktrees-unchanged", "Delete unchanged worktrees", Page::Storage, "worktree cleanup remove unchanged commits default branch automatic"),
item("storage-files", "Files", Page::Storage, "settings threads scratch data folder open"),
item("default-model", "Default model", Page::General, "new thread project provider reasoning effort"),
item(
"default-permissions",
"Permissions",
Page::General,
"new thread default runtime mode supervised approvals auto accept edits full access plan",
),
item("color-scheme", "Color scheme", Page::Appearance, "appearance light dark system mode"),
item("theme", "Themes", Page::Appearance, "appearance colors palette bone graphite tokyo night catppuccin github nord gruvbox rose pine solarized dracula one"),
item("diff-color-scheme", "Diff colors", Page::Appearance, "red green blue orange additions deletions changes counts palette colorblind"),
item("chat-width", "Chat width", Page::Appearance, "wide full width column layout messages composer monitor"),
item("composer-context", "Composer context", Page::Appearance, "branch worktree controls strip below composer"),
item("start-wallpaper", "Start wallpaper", Page::Appearance, "wallpaper background image picture photo new thread start screen composer"),
item("thread-cards", "Thread cards", Page::Appearance, "sidebar compact comfortable density"),
item("project-grouping", "Project grouping", Page::General, "combine matching repositories environments sidebar remote devices"),
item("project-order", "Project order", Page::General, "sort projects sidebar name created recent"),
item("snooze-limited-threads", "Snooze limited threads", Page::General, "usage quota rate limit reset wake recover continue"),
item("auto-resume-limited-threads", "Auto-resume limited threads", Page::General, "usage quota rate limit reset recover continue"),
item("working-shelf", "Working section (beta)", Page::General, "hide fold running monitoring threads inbox sidebar shelf"),
item("auto-settle-inactive-threads", "Auto-settle inactive threads", Page::General, "sidebar inactivity days no activity automatically"),
item("auto-settle-merged-threads", "Auto-settle merged threads", Page::General, "pull request merge closed automatically sidebar"),
revealed(
"days-before-auto-settle",
"Days of inactivity before auto-settle",
Page::General,
"thread timeout activity sidebar",
"auto-settle-inactive-threads",
),
item("thread-notifications", "Thread notifications", Page::General, "notification sound alert completion input approval desktop"),
item("in-app-notifications", "In-app notifications", Page::General, "notification toast popup completion input approval failure"),
item("time-format", "Time format", Page::General, "timestamp clock locale system os 12 hour 24 hour"),
item("response-streaming", "Response streaming", Page::General, "output token paragraph buffered wait turn"),
item("fold-turns", "Fold finished turns", Page::General, "worked for tool calls collapse transcript"),
item("show-costs", "Show costs", Page::General, "price dollars spend turn thread"),
item("hide-whitespace-changes", "Hide whitespace changes", Page::General, "diff ignore spaces edits default"),
item("default-diff-file-state", "Default diff file state", Page::General, "collapsed expanded collapse expand files"),
item("diff-layout", "Diff layout", Page::General, "stacked split side by side unified inline view"),
item("proactive-panels", "Proactive panels", Page::General, "automatically open diff right panel agent completion"),
item("skills-in-slash-menu", "Show skills in slash menu", Page::General, "command menu dollar $ slash /"),
item("composer-rich-text", "Rich text composer", Page::General, "composer rich text bold italic markdown styled wysiwyg"),
item("inline-ui", "Inline diagrams and UI", Page::General, "mermaid openui charts tables forms svg html preview artifacts render answers"),
item("composer-collapse", "Collapse composer on scroll", Page::General, "composer rest resting scroll wheel conversation shrink minimize"),
item("send-shortcut", "Send shortcut", Page::General, "enter return ctrl multiline prompt new line composer"),
item("follow-up-behavior", "Follow-up behavior", Page::General, "queue steer running turn send default behavior composer"),
item("usage-bar", "Usage bar", Page::General, "limits plan session weekly 5h codex claude chatgpt context tokens speed tok/s composer"),
item("provider-update-checks", "Provider update checks", Page::General, "installed cli versions newer available claude pi jcode"),
item(
"continue-threads-after-server-update",
"Continue threads after restarts",
Page::General,
"resume running active interrupted work restart reboot machine crash update automatically",
),
item("background-activity", "Background activity", Page::General, "balanced performance battery saver git provider health refresh power idle policy"),
item("stop-idle-agents", "Stop idle agents", Page::General, "memory ram process idle agent stop free resume minutes"),
item("new-threads", "Workspace", Page::General, "new threads default workspace mode local worktree checkout"),
item("worktree-submodules", "Submodules", Page::General, "git submodule init recursive top-level none worktree"),
item("start-from-origin", "Start from origin", Page::General, "new worktrees latest matching remote branch local"),
item("add-project-starts-in", "Add project starts in", Page::General, "base directory folder browser path home"),
item("unpin-confirmation", "Unpin confirmation", Page::General, "ask before thread pinned section"),
item("archive-confirmation", "Archive confirmation", Page::General, "ask before thread second click inline action settle"),
item("delete-confirmation", "Delete confirmation", Page::General, "ask before thread chat history"),
item("quit-confirmation", "Quit shortcut", Page::General, "confirmation exit direct hold double press twice ctrl q"),
item("text-generation-model", "Text generation model", Page::General, "generated thread titles default provider haiku"),
item("version", "Version", Page::General, "about release update vectrum code"),
item("diagnostics", "Diagnostics", Page::General, "logs processes resource cpu memory kill"),
item("open-source-licenses", "Open source licenses", Page::General, "notices dependencies fonts geist crates ofl mit apache"),
folded("legacy-plan-mode", "Plan mode (legacy)", Page::General, "build plan composer old shift tab", "legacy"),
folded("legacy-context-window-indicator", "Context window indicator (legacy)", Page::General, "composer meter usage tokens circle old", "legacy"),
folded("legacy-sidebar", "Sidebar (legacy)", Page::General, "project thread tree old flat list group", "legacy"),
item("keybindings", "Keybindings", Page::Keybindings, "keyboard shortcuts hotkeys commands bindings"),
item(
"providers",
"Providers",
Page::Providers,
"agents cli claude pi jcode sign in install subscription instances authentication api key models configuration binary path config directory arguments environment variables display name accent color custom favorite hidden auto compact",
),
item("usage-providers", "Usage providers", Page::Providers, "usage sources CLIProxyAPI CLI proxy hub quota subscription limits management key add remove"),
item("provider-health-check-interval", "Health check interval", Page::Providers, "refresh availability versions auth state models background probes seconds off"),
item("agent-browser-access", "Agent browser access", Page::Integrations, "allow disable enable open drive preview tools sessions"),
item("browser-auto-show-floating-preview", "Auto-show floating preview", Page::Integrations, "agent opens device simulator emulator pop into view float hide"),
item("device-hub", "Device hub", Page::Integrations, "simulator emulator ios android install start devices"),
revealed("device-platform-support", "Simulator support", Page::Integrations, "xcode android studio sdk avd runtime emulator", "device-hub"),
item("agent-device-access", "Agent device access", Page::Integrations, "allow simulator emulator ios android drive tools sessions agent-device"),
item("open-in", "Open in", Page::Integrations, "editor zed vs code cursor vscodium sublime kate file manager"),
item("source-control", "Source control", Page::SourceControl, "version control git github hosting integrations credentials gh"),
item("worktree-branch-naming", "Worktree branch naming", Page::SourceControl, "static semantic prefix custom prompt instructions feat fix refactor chore"),
item(
"source-control-writing-style",
"Source control writing style",
Page::SourceControl,
"commit message pull request repository conventions conventional commits custom instructions",
),
item(
"follow-change-request-templates",
"Follow change request templates",
Page::SourceControl,
"pull request template description markdown",
),
revealed("branch-prefix", "Branch prefix", Page::SourceControl, "worktree branch name prefix vectrum", "worktree-branch-naming"),
revealed(
"branch-naming-instructions",
"Branch naming instructions",
Page::SourceControl,
"worktree branch custom prompt model",
"worktree-branch-naming",
),
item(
"connections-environment",
"This machine",
Page::Connections,
"connections server remote access pairing code key port allow network",
),
item("remote-environments", "Environments", Page::Connections, "add pair backend host code devices remote connect"),
item("archive", "Archived threads", Page::Archive, "restore reopen settled deleted history projects unarchive"),
];
/// Lowercases and keeps letters, digits and spaces: T3 Code's
/// `normalizeSearchText`.
fn normalize(text: &str) -> String {
let mapped: String =
text.chars().map(|c| if c.is_alphanumeric() || matches!(c, '$' | '/' | '+') { c.to_ascii_lowercase() } else { ' ' }).collect();
mapped.split_whitespace().collect::<Vec<_>>().join(" ")
}
/// The items matching `query`, best first: every word must appear in the
/// title, the page name or the terms; exact and leading title matches rank
/// highest.
pub fn search(query: &str) -> Vec<Item> {
let query = normalize(query);
if query.is_empty() {
return Vec::new();
}
let tokens: Vec<&str> = query.split(' ').collect();
let all = ITEMS.iter().copied().chain(keybindings::search_items());
let mut ranked: Vec<(usize, u8, Item)> = all
.enumerate()
.filter_map(|(index, item)| {
let title = normalize(item.title);
let fields = [title.clone(), normalize(item.page.label()), normalize(item.terms)];
if !tokens.iter().all(|token| fields.iter().any(|field| field.contains(token))) {
return None;
}
let rank = if title == query {
5
} else if title.starts_with(&query) {
4
} else if title.contains(&query) {
3
} else if tokens.iter().all(|token| title.contains(token)) {
2
} else if fields.iter().any(|field| field.contains(&query)) {
1
} else {
0
};
Some((index, rank, item))
})
.collect();
ranked.sort_by(|a, b| a.2.secondary.cmp(&b.2.secondary).then(b.1.cmp(&a.1)).then(a.0.cmp(&b.0)));
ranked.into_iter().map(|(_, _, item)| item).collect()
}
#[cfg(test)]
mod tests {
use super::{ITEMS, search};
#[test]
fn titles_rank_first() {
let results = search("model");
assert_eq!(results.first().map(|item| item.id), Some("default-model"));
assert_eq!(search("time format")[0].id, "time-format");
assert!(search("zzzz-nothing").is_empty());
assert!(search(" ").is_empty());
}
#[test]
fn terms_and_pages_match() {
assert!(search("colorblind").iter().any(|item| item.id == "diff-color-scheme"));
// Every word has to match somewhere.
assert!(search("appearance width").iter().any(|item| item.id == "chat-width"));
assert!(!search("appearance hold").iter().any(|item| item.id == "quit-confirmation"));
}
#[test]
fn keybindings_sort_last() {
let results = search("sidebar");
let first_secondary = results.iter().position(|item| item.secondary).unwrap_or(results.len());
assert!(results[first_secondary..].iter().all(|item| item.secondary));
assert!(first_secondary > 0);
}
#[test]
fn ids_are_unique() {
for (ix, item) in ITEMS.iter().enumerate() {
assert!(ITEMS[ix + 1..].iter().all(|other| other.id != item.id), "{} twice", item.id);
}
}
}