diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 5d349477..9a84c3f0 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -118,6 +118,18 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsLanguageEnglish => "English", L10nKey::SettingsLanguageChinese => "简体中文", L10nKey::SettingsLanguageJapanese => "日本語", + L10nKey::SettingsSearchAgentForwardingKeywords => { + "ssh agent forwarding key auth forward ForwardAgent" + } + L10nKey::SettingsSearchCustomShellPathKeywords => { + "shell binary path executable custom login program" + } + L10nKey::SettingsSearchImportAliasesKeywords => { + "ssh config alias host import openssh known hosts" + } + L10nKey::SettingsSearchJumpHostKeywords => { + "ssh bastion proxyjump jump host tunnel gateway hop" + } L10nKey::SettingsSearchLanguageKeywords => "language, locale, english, chinese", L10nKey::SettingsTransparency => "Transparency", L10nKey::SettingsOpacity => "Opacity", @@ -798,6 +810,9 @@ pub fn translate_en(key: L10nKey) -> &'static str { } L10nKey::SettingsSearchKeybindingsTitle => "Keybindings", L10nKey::SettingsSearchLineHeightKeywords => "typography leading spacing", + L10nKey::SettingsSearchMouseZoomKeywords => { + "wheel scroll zoom font size ctrl cmd modifier mouse" + } L10nKey::SettingsSearchNewTabPositionKeywords => "tabs order end after current", L10nKey::SettingsSearchNotifyOnCommandFinishKeywords => { "notification alert done osc desktop banner long command" @@ -808,6 +823,9 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsSearchOpacityKeywords => { "transparency translucent see through window alpha" } + L10nKey::SettingsSearchOpenFilesCommandKeywords => { + "link file open editor command external application click" + } L10nKey::SettingsSearchOpenFilesWithKeywords => { "links file editor command external app path line column" } @@ -818,6 +836,9 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::SettingsSearchOhMyPiKeywords => "agent integration extension install omp oh my pi", L10nKey::SettingsSearchGeminiKeywords => "agent integration hooks install gemini google", L10nKey::SettingsSearchDroidKeywords => "agent integration hooks install droid factory", + L10nKey::SettingsSearchPerPaneHistoryKeywords => { + "shell history per pane separate up arrow recall bash zsh" + } L10nKey::SettingsSearchQwenCodeKeywords => { "agent integration hooks install qwen code qwen-code" } @@ -842,9 +863,18 @@ pub fn translate_en(key: L10nKey) -> &'static str { "restore session previous tabs splits reopen launch startup layout" } L10nKey::SettingsSearchScrollSpeedKeywords => "mouse wheel multiplier scrolling", + L10nKey::SettingsSearchShellIntegrationKeywords => { + "ssh remote shell integration rc bashrc zshrc prompt marks" + } + L10nKey::SettingsSearchSkipBannerKeywords => { + "ssh login banner motd suppress quiet message of the day" + } L10nKey::SettingsSearchSmoothScrollKeywords => { "smooth animation ease wheel notch trackpad scrolling" } + L10nKey::SettingsSearchUiFontSizeKeywords => { + "interface ui chrome sidebar tab settings text bigger smaller zoom scale" + } L10nKey::SettingsSearchUpdateChannelKeywords => { "update channel stable nightly release prerelease build" } @@ -896,6 +926,9 @@ pub fn translate_en(key: L10nKey) -> &'static str { "ssh confirm close tab pane live session security" } L10nKey::SettingsSearchStartupWindowKeywords => "launch open maximized fullscreen normal", + L10nKey::SettingsSearchX11ForwardingKeywords => { + "ssh x11 forwarding display gui xquartz ForwardX11" + } L10nKey::SwitcherNoMatch => "No workspace or machine matches.", L10nKey::AddSshHost => "Add SSH Host…", L10nKey::ClickForNewWindow => "click for a new window", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 640748dd..daebe02e 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -123,6 +123,18 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsLanguageEnglish => "English", L10nKey::SettingsLanguageChinese => "简体中文", L10nKey::SettingsLanguageJapanese => "日本語", + L10nKey::SettingsSearchAgentForwardingKeywords => { + "SSH エージェント転送 鍵転送 ssh agent forwarding key forward" + } + L10nKey::SettingsSearchCustomShellPathKeywords => { + "シェル パス 実行ファイル ログインシェル shell binary path executable custom" + } + L10nKey::SettingsSearchImportAliasesKeywords => { + "エイリアス取り込み SSH 設定 ホスト ssh config alias host import openssh" + } + L10nKey::SettingsSearchJumpHostKeywords => { + "踏み台 ジャンプホスト 中継 ssh bastion proxyjump jump host gateway" + } L10nKey::SettingsSearchLanguageKeywords => { "言語 ロケール 英語 中国語 language locale english chinese" } @@ -837,6 +849,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchLineHeightKeywords => { "タイポグラフィ リーディング 行間 line height typography leading spacing" } + L10nKey::SettingsSearchMouseZoomKeywords => { + "ホイール ズーム 拡大 縮小 修飾キー wheel scroll zoom mouse font size modifier" + } L10nKey::SettingsSearchNewTabPositionKeywords => { "タブ 順序 末尾 現在のタブの隣 new tab position tabs order end after current" } @@ -849,6 +864,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchOpacityKeywords => { "透明度 半透明 透ける ウィンドウ alpha opacity transparency translucent window" } + L10nKey::SettingsSearchOpenFilesCommandKeywords => { + "ファイルを開く コマンド エディタ 外部アプリ リンク open file command editor external link" + } L10nKey::SettingsSearchOpenFilesWithKeywords => { "リンク ファイル エディタ コマンド 外部アプリ パス 行 列 open files with editor external app path line column" } @@ -867,6 +885,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchDroidKeywords => { "エージェント 統合 フック インストール droid factory agent integration hooks install" } + L10nKey::SettingsSearchPerPaneHistoryKeywords => { + "ペインごとの履歴 コマンド履歴 上矢印 bash zsh shell history per pane" + } L10nKey::SettingsSearchQwenCodeKeywords => { "エージェント 統合 フック インストール qwen code agent integration hooks install" } @@ -897,9 +918,18 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchScrollSpeedKeywords => { "マウス ホイール 倍率 スクロール scroll speed mouse wheel multiplier scrolling" } + L10nKey::SettingsSearchShellIntegrationKeywords => { + "シェル統合 リモート rc bashrc zshrc プロンプト shell integration remote" + } + L10nKey::SettingsSearchSkipBannerKeywords => { + "バナー抑制 motd ログインメッセージ ssh login banner suppress quiet" + } L10nKey::SettingsSearchSmoothScrollKeywords => { "スムーズ スクロール アニメーション ホイール トラックパッド smooth animation ease wheel trackpad" } + L10nKey::SettingsSearchUiFontSizeKeywords => { + "UI フォントサイズ インターフェイス 文字サイズ サイドバー タブ 拡大 縮小 interface ui font size text zoom" + } L10nKey::SettingsSearchUpdateChannelKeywords => { "更新 チャンネル 安定版 ナイトリー リリース update channel stable nightly release" } @@ -957,6 +987,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchStartupWindowKeywords => { "起動 開く 最大化 全画面 通常 startup window launch maximized fullscreen normal" } + L10nKey::SettingsSearchX11ForwardingKeywords => { + "X11 転送 GUI 転送 ディスプレイ ssh x11 forwarding display gui xquartz" + } L10nKey::SwitcherNoMatch => "一致するワークスペースまたはマシンがありません", L10nKey::AddSshHost => "SSH ホストを追加…", L10nKey::ClickForNewWindow => "クリックで新しいウィンドウを開く", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index c9d560f4..bcc88766 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -179,6 +179,10 @@ l10n_keys! { SettingsLanguageEnglish, SettingsLanguageChinese, SettingsLanguageJapanese, + SettingsSearchAgentForwardingKeywords, + SettingsSearchCustomShellPathKeywords, + SettingsSearchImportAliasesKeywords, + SettingsSearchJumpHostKeywords, SettingsSearchLanguageKeywords, SettingsTransparency, SettingsOpacity, @@ -585,6 +589,12 @@ l10n_keys! { SettingsSearchAboutKeywords, SettingsSearchAutoDownloadKeywords, SettingsSearchCheckUpdatesOnLaunchKeywords, + SettingsSearchMouseZoomKeywords, + SettingsSearchOpenFilesCommandKeywords, + SettingsSearchPerPaneHistoryKeywords, + SettingsSearchShellIntegrationKeywords, + SettingsSearchSkipBannerKeywords, + SettingsSearchUiFontSizeKeywords, SettingsSearchUpdateChannelKeywords, SettingsSearchAnsiColorsKeywords, SettingsSearchBackgroundImageKeywords, @@ -652,6 +662,7 @@ l10n_keys! { SettingsSearchVerifyHostKeysKeywords, SettingsSearchWarnBeforeClosingKeywords, SettingsSearchStartupWindowKeywords, + SettingsSearchX11ForwardingKeywords, SwitcherNoMatch, AddSshHost, ClickForNewWindow, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 9195125c..fbf4e6f2 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -105,6 +105,18 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsLanguageEnglish => "English", L10nKey::SettingsLanguageChinese => "简体中文", L10nKey::SettingsLanguageJapanese => "日本語", + L10nKey::SettingsSearchAgentForwardingKeywords => { + "SSH agent 转发 密钥转发 代理转发 ssh agent forwarding key forward" + } + L10nKey::SettingsSearchCustomShellPathKeywords => { + "自定义 shell 路径 可执行文件 登录程序 shell binary path executable custom" + } + L10nKey::SettingsSearchImportAliasesKeywords => { + "导入别名 SSH 配置 主机别名 ssh config alias host import openssh" + } + L10nKey::SettingsSearchJumpHostKeywords => { + "跳板机 堡垒机 中转 ssh bastion proxyjump jump host gateway" + } L10nKey::SettingsSearchLanguageKeywords => { "语言 区域设置 英文 中文 language locale english chinese" } @@ -744,6 +756,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchLineHeightKeywords => { "行高 行间距 行距 typography line height spacing leading" } + L10nKey::SettingsSearchMouseZoomKeywords => { + "滚轮缩放 鼠标缩放 字号 修饰键 wheel scroll zoom mouse font size modifier" + } L10nKey::SettingsSearchNewTabPositionKeywords => { "新标签页位置 标签页 顺序 末尾 当前之后 new tab position tabs order end after current" } @@ -756,6 +771,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchOpacityKeywords => { "不透明度 透明度 窗口 半透明 alpha opacity transparency translucent window" } + L10nKey::SettingsSearchOpenFilesCommandKeywords => { + "打开文件 命令 编辑器 外部程序 链接 open file command editor external link" + } L10nKey::SettingsSearchOpenFilesWithKeywords => { "打开文件 链接 编辑器 命令 外部应用 路径 行号 列号 open files editor command path line column" } @@ -774,6 +792,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchDroidKeywords => { "Droid agent 集成 钩子 安装 droid factory agent integration hooks install" } + L10nKey::SettingsSearchPerPaneHistoryKeywords => { + "每个窗格独立历史 命令历史 上方向键 bash zsh shell history per pane" + } L10nKey::SettingsSearchQwenCodeKeywords => { "Qwen Code 通义千问 agent 集成 钩子 安装 qwen code agent integration hooks install" } @@ -804,9 +825,18 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchScrollSpeedKeywords => { "滚动速度 鼠标滚轮 滚动倍率 scroll speed mouse wheel multiplier scrolling" } + L10nKey::SettingsSearchShellIntegrationKeywords => { + "Shell 集成 远程 rc 文件 bashrc zshrc 提示符标记 shell integration remote" + } + L10nKey::SettingsSearchSkipBannerKeywords => { + "跳过登录横幅 motd 登录消息 静默 ssh login banner suppress quiet" + } L10nKey::SettingsSearchSmoothScrollKeywords => { "平滑滚动 动画 缓动 滚轮 触控板 smooth animation ease wheel trackpad scrolling" } + L10nKey::SettingsSearchUiFontSizeKeywords => { + "界面字号 界面文字大小 侧边栏 标签页 缩放 interface ui font size text bigger smaller zoom" + } L10nKey::SettingsSearchUpdateChannelKeywords => { "更新通道 稳定版 每夜构建 预发布 版本 update channel stable nightly release" } @@ -864,6 +894,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::SettingsSearchStartupWindowKeywords => { "启动窗口 启动 最大化 全屏 普通 startup window launch maximized fullscreen normal" } + L10nKey::SettingsSearchX11ForwardingKeywords => { + "X11 转发 图形转发 显示 ssh x11 forwarding display gui xquartz" + } L10nKey::SwitcherNoMatch => "没有匹配的工作区或机器。", L10nKey::AddSshHost => "添加 SSH 主机…", L10nKey::ClickForNewWindow => "点击打开新窗口", diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 8658aa83..416c4a00 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -718,6 +718,61 @@ fn settings_search_entries() -> &'static [SearchEntry] { title: SettingsInstallCliOnPath, keywords: SettingsSearchCommandLineToolKeywords, }, + SearchEntry { + section: Appearance, + title: SettingsUiFontSize, + keywords: SettingsSearchUiFontSizeKeywords, + }, + SearchEntry { + section: Terminal, + title: SettingsCustomPath, + keywords: SettingsSearchCustomShellPathKeywords, + }, + SearchEntry { + section: Terminal, + title: SettingsMouseZoom, + keywords: SettingsSearchMouseZoomKeywords, + }, + SearchEntry { + section: Terminal, + title: SettingsOpenFilesCommand, + keywords: SettingsSearchOpenFilesCommandKeywords, + }, + SearchEntry { + section: Input, + title: SettingsPerPaneHistory, + keywords: SettingsSearchPerPaneHistoryKeywords, + }, + SearchEntry { + section: Ssh, + title: SettingsImportAliases, + keywords: SettingsSearchImportAliasesKeywords, + }, + SearchEntry { + section: Ssh, + title: SettingsJumpHost, + keywords: SettingsSearchJumpHostKeywords, + }, + SearchEntry { + section: Ssh, + title: SettingsAgentForwarding, + keywords: SettingsSearchAgentForwardingKeywords, + }, + SearchEntry { + section: Ssh, + title: SettingsX11Forwarding, + keywords: SettingsSearchX11ForwardingKeywords, + }, + SearchEntry { + section: Ssh, + title: SettingsShellIntegration, + keywords: SettingsSearchShellIntegrationKeywords, + }, + SearchEntry { + section: Ssh, + title: SettingsSkipBanner, + keywords: SettingsSearchSkipBannerKeywords, + }, ] } @@ -8247,3 +8302,150 @@ mod gpui_tests { assert!(tab_completion, "the completion menu comes back with it"); } } + +#[cfg(test)] +mod search_index_tests { + use super::*; + + /// The label key of every row `settings.rs` can render, read out of its own + /// source. + /// + /// Reading the source rather than keeping a list by hand is the point: a + /// list would need exactly the discipline the search index needs, and would + /// fall out of date the same way, so it could not catch the index doing it. + /// The label keys of every row `settings.rs` can render, read out of its + /// own source — one entry per row, holding every key that row's label can + /// resolve to. + /// + /// Reading the source rather than keeping a list by hand is the point: a + /// list would need exactly the discipline the search index needs, and would + /// fall out of date the same way, so it could not catch the index doing it. + /// + /// A label can name more than one key, because a few are chosen by + /// `cfg!(target_os = ...)` — the blur row is "Background material" on + /// Windows and "Blur" everywhere else. The index is gated the same way, so + /// the caller asks whether *any* of a row's keys is indexed rather than + /// picking the branch this build happens to be on. + fn rendered_row_label_keys() -> Vec> { + const SOURCE: &str = include_str!("settings.rs"); + + fn keys_in(text: &str) -> Vec { + let mut out = Vec::new(); + let mut rest = text; + while let Some(at) = rest.find("L10nKey::") { + rest = &rest[at + "L10nKey::".len()..]; + let end = rest + .find(|c: char| !c.is_alphanumeric() && c != '_') + .unwrap_or(rest.len()); + out.push(rest[..end].to_string()); + rest = &rest[end..]; + } + out + } + + let mut rows = Vec::new(); + for call in ["self.settings_row(", "self.settings_row_gated_when("] { + let mut from = 0; + while let Some(at) = SOURCE[from..].find(call) { + let start = from + at + call.len(); + from = start; + // The label is the first argument. Stop at the comma that ends + // it so the description's key cannot be read as the label's. + let arg = &SOURCE[start..SOURCE.len().min(start + 400)]; + let arg = match arg.find(',') { + Some(end) => &arg[..end], + None => arg, + }; + let keys = keys_in(arg); + if keys.is_empty() { + // A row labelled by a runtime string (an agent's display + // name, a caller's `label`). Nothing static to index. + continue; + } + rows.push(keys); + } + } + assert!( + rows.len() > 50, + "the source scan found only {} rows, so it has stopped matching how \ + a row is written", + rows.len() + ); + rows + } + + /// Every settings row is reachable from the search box. + /// + /// The two halves of search do not agree on their own: [`row_matches_query`] + /// accepts a bare label match, while [`section_match_count`] — which drives + /// the nav badges, [`best_matching_section`] and the "nothing matches" note + /// — counts only the index. A row missing from the index is therefore not + /// merely unhighlighted: typing its own title into the box makes the page + /// answer that nothing matches, while the row sits there on screen. Seven + /// rows did, including "Zoom with the wheel" and "Skip banner". + /// + /// The fix is the index, not the count: a row's label is written for a + /// reader, and the keywords are what carry the words they would actually + /// reach for. + #[test] + fn every_rendered_settings_row_is_findable_by_its_own_label() { + // Bare field labels inside the SSH profile form — "Name", "Host", + // "User", "Auth". They are indexed nowhere on purpose: each is one + // common word, and a one-word entry puts a match on the SSH badge for + // half the queries anyone types. The distinctive rows of that same form + // (jump host, agent forwarding, X11, shell integration, skip banner) + // *are* indexed, so the form is reachable by the things it is actually + // searched for. + const BARE_FIELD_LABELS: &[&str] = &[ + "SettingsName", + "SettingsHost", + "SettingsUser", + "SettingsAuth", + ]; + + // `L10nKey` is not `Hash`, and its `Debug` name is what the source scan + // yields, so the index is compared as those names. + let indexed: std::collections::HashSet = settings_search_entries() + .iter() + .map(|e| format!("{:?}", e.title)) + .collect(); + + let mut missing: Vec = rendered_row_label_keys() + .into_iter() + .filter(|keys| { + !keys + .iter() + .any(|k| BARE_FIELD_LABELS.contains(&k.as_str()) || indexed.contains(k)) + }) + .map(|keys| keys.join(" or ")) + .collect(); + missing.sort(); + missing.dedup(); + + assert!( + missing.is_empty(), + "these settings rows are rendered but not in the search index, so \ + searching for one by name answers that nothing matches: {missing:?}" + ); + } + + /// The exemptions above name rows that exist. + /// + /// Without this, renaming one of those keys would silently widen the + /// exemption to nothing and take a row out of the guard with it. + #[test] + fn the_search_index_exemptions_still_name_rendered_rows() { + let rendered = rendered_row_label_keys(); + for key in [ + "SettingsName", + "SettingsHost", + "SettingsUser", + "SettingsAuth", + ] { + assert!( + rendered.iter().any(|keys| keys.iter().any(|k| k == key)), + "{key} is exempt from the search index but no longer names a row" + ); + } + } +}