feat(macos): replace git log filter menus with bounded IDEA-style popovers - #326
feat(macos): replace git log filter menus with bounded IDEA-style popovers#326Wz58luck wants to merge 3 commits into
Conversation
…overs The Branch and User filters in the Git Log filter bar used native NSMenu menus that grew to screen height on repositories with many references or authors, covering the whole workbench (issue 1lck#302). Replace them with custom SwiftUI popovers anchored under their filter chips: - Branch: a two-level menu with the All Branches reset entry, starred shortcuts for the current branch and its upstream, and group rows (Local, per-remote, Tags) that open a bounded flyout column; typing falls back to a flat filtered list. - User: a flat bounded list with pinned reset entries and stable author ordering. Both popovers are searchable, cap their height with internal scrolling, and reuse the existing selectGitReference action without any data-layer or shared-contract changes.
511c8b0 to
fef5b00Compare
1lck
left a comment
There was a problem hiding this comment.
审查结论:请求修改
方向和分层都对——纯 macOS Views 层改动,筛选语义零改动,把分组逻辑抽成可测的纯函数(GitLogFilterList)正是这个 codebase 应有的做法。问题集中在新增代码自身的建模与视图层,以下 7 项在本 PR 职责范围内,建议合并前修掉。
另外 PR 描述里纠正 issue #302 中「基于 Webview 的显示」这个误判是对的——macOS 端 Git 视图从未使用 WebKit,就是 NSMenu 在大量条目下的默认行为。
验证情况(在 worktree 实跑 fef5b00):
./scripts/test-macos.sh— 688 tests / 80 suites 通过(含新增 7 个GitLogFilterListTests)./scripts/verify-service-boundaries.sh— 通过- Rust Core / 共享契约 / Windows 零改动,
verify-rust-core.sh、verify-shared-contracts.sh、verify-windows-boundaries.sh无需运行 - 第 7 项按 SwiftUI 布局语义推导,未跑 GUI 截图,建议作者手工确认
范围内 · 建议合并前修复
1. 建模:GitLogBranchFilterItem 一个类型担了三种角色
reference: GitReference? 为 nil 时是重置项,非 nil 且 isStarred 时是收藏行,否则是普通引用行,区分靠 id 里的魔法串 "all-branches"。这个串还和 branchSections 的 section id 撞名,两处硬编码。
同一个文件里的 GitLogAuthorFilterItem 已经用 enum Kind { allUsers, currentUser, author } 显式建模了。同一个 PR 里两种风格,照后者改即可。
这条是下面第 2、5 项的共同根因,建议先做。
2. 两种模式的规则不一致
设计文档把「两级浏览 + 搜索扁平化」定为方案,形态本身没问题。但同一个分支在敲下第一个字符前后表现不同:
浏览模式 (branchMenu) | 搜索模式 (branchSections) | |
|---|---|---|
| remote 的 detail | nil | "Remote" |
| 本地分支显示名 | 完整 feature/login | 叶子名 login |
| ⭐ 当前分支 / upstream 行 | 有 | 消失 |
排查代价:以后收到「分支在筛选里显示不对」这类反馈,第一步得先问「你当时输了搜索词吗」。
这里要求的是规则统一,不是把两条代码路径合并成一条——两级结构是设计文档认可的形态。
3. menuBuilder() 在热路径重复求值 5 次
menu 是 computed property 包着注入的闭包,body 路径上 menu.reset、menu.starred(L639、L647)、menu.groups、expandedGroup 各访问一次,每次都对全部 references 重跑一遍 filter + sort。
这个弹层的立项理由就是「100+ 分支」,而它在自己的热路径上把 O(n log n) 做了 5 遍,hover 换行、滚动、展开分组都会触发。改成传数据而非传构建闭包,或缓存进 @State 即可。
4. isPinned 靠 title == nil 这个巧合推断
分隔线条件 是 section.title == nil && index < count - 1,而「无命名空间的本地分支」这一组 title 也是 nil,所以它后面也会多画一条分隔线。作者列表恰好符合预期纯属巧合。
GitLogFilterSection 加一个显式 isPinned 字段——修完这条它才是可测的。
5. 本地化:2 处拿不到已有译文 + 5 处新增缺译
TextField(placeholder, text:) 和 Text(emptyText) 传的是 String 变量,会命中 S: StringProtocol 那个 overload 走 verbatim。而 "Search branches"(zh-Hans.lproj/Localizable.strings:475)和 "No matching branches"(同文件 :762)译文已经在仓库里,中文用户现在看到的是英文。
Text(item.rowTitle) 让 All Branches / All Users / Me 也走 verbatim,这三个 key 目前尚无中文。需补齐:All Branches、All Users、Me、Search users、No matching users。
改成 LocalizedStringKey 即可,仓库内既有写法:ProjectSidebarView.swift:591、GitHubPullRequestsView.swift:996。分组标题那里 已经写对了,只是搜索栏和空状态漏了。
注意 rowTitle 需要区分「数据名(分支名 / 作者名,不译)」和「固定文案(要译)」——第 1 项改成 enum 后自然解决。顺带一提,remote 分组标题 "origin/…" 是数据派生的,却也走了 LocalizedStringKey,靠找不到 key 回落到字面量才碰巧正确,属同一处建模混淆。
6. 选中判定写在 view body 的闭包字面量里
isItemSelected:{ item inguardlet reference = item.reference else{return model.selectedGitReference ==nil}return model.selectedGitReference?.id == reference.id
}「什么算选中」是业务规则,写在 SwiftUI body 的闭包字面量里不可测、不可复用,而且和 User 筛选那段几乎同构却无法共享。建议落到 GitLogFilterList 或 item 类型上(例如 item.matches(selection:)),让 view 只做装配。
7. 布局:首帧约 336pt 空白 + 输入时宽度跳变
固定 560pt 宽,但 flyout 列 只在点开某个分组行后才渲染。首次打开时 HStack 自然宽度只有 224pt,VStack 默认 .center 对齐,结果是两侧各约 168pt 空白——这正是用户打开 Branch 筛选看到的第一眼。
同一行的 width: normalizedQuery.isEmpty ? 560 : 340 让宽度由 query 驱动:敲第一个字符时宽度突变 220pt,NSPopover 会跟着重算位置并平移,清空回删再抖一次。
这两条是按 SwiftUI 布局语义推导的(我只跑了编译和测试,没跑 GUI 截图),麻烦手工确认一下首帧观感和输入时的抖动。
测试覆盖
7 个测试全部只打 GitLogFilterList 的三个纯函数,确定性、无 sleep,符合 write-stable-tests,这点没问题。
但 branchMenuBuildsStarredShortcutsAndFlyoutGroups 和 branchSectionsGroupAndOrderReferences 把第 2 项的不一致当成正确行为写进了断言(前者只对 locals 断言 rowDetail,后者断言 remote 的 detail 是 "Remote"),统一规则时这两条要一起更新。
第 1、2、4 项改完后可测面积会明显扩大——现在测不了,是因为语义还没被表达出来。
范围外 · 仅供参考,不作为合并门槛
- 让
BranchSwitcherPopover反过来消费GitLogFilterList。branchSections的分组键、kindOrder、displayTitle基本是BranchSwitcherPopover.swift:245-302的逐行搬运,代码里也写了// Mirror BranchSwitcherPopover。但这是重构本 PR 未碰的文件,属独立任务。同理kind → SF Symbol映射现在有 3 份实现(前两份 PR 之前就存在),收敛它们也是独立任务。 GitLogFilterPopover<Row>泛型 + 5 个闭包目前只有 1 个消费者。 评价合理但要求返工力度过大,把第 3 项修掉即可,泛型留到真有第二个消费者时再说。rowIsStarred把渲染指令混进了 protocol。 默认实现确实被 author item 用着,不是死代码。但「用强调色画图标」这类关注点以后会长出rowIsDeleted,语义标签或rowTint: Color?更耐扩展。可留后。- Date 筛选仍是 NSMenu。 requirements 文档已明确列为非目标,理由(条目固定且少)成立,不拦。
- 两份共 206 行的规格文档配一个视图层改动偏重。
docs/superpowers/specs/已有 3 份同类文档,是既有惯例,不拦。
做得好的部分
- 筛选语义零改动:
selectGitReference、exactAuthor、gitLogQuery、清除按钮路径全部原样复用,这点很干净。 - 迁移无残骸:
GitLogAuthorSelection/GitLogAuthorOption从GitLogView.swift移出后没留残骸,gitLogMenuItem和referenceIcon仍被 Date 筛选和引用树使用,未误删。 - 空 query 短路处理正确:
query.isEmpty ||显式短路避免了localizedCaseInsensitiveContains("")恒为 false 清空列表这个坑,注释也写明了原因。 - 分层正确:不碰 AppModel / Services / Core / Rust,弹层是纯展示组件、不持有
AppModel,符合develop-lithe的 Views 层边界。
建议修改顺序
1 → 2 → 4(建模三连,做完可测面积最大)→ 3 → 6 → 5 → 7
Apply the maintainer review on PR 1lck#326 (items 1-7, in the suggested order): - Model GitLogBranchFilterItem roles with an explicit Kind enum (allBranches / starred / reference) instead of a nil reference plus the "all-branches" sentinel string. - Unify per-row rules across browse and search modes: full short names everywhere, upstream-only detail (no kind labels), and the pinned reset + starred shortcuts survive into search results filtered by title. - Add an explicit isPinned flag to GitLogFilterSection; the trailing divider now follows pinned regions only instead of guessing from a nil title. - Inject the branch menu as data instead of a builder closure so body re-evaluations stop rebuilding it on every row interaction. - Move selection matching onto the item types (matches(selected:)) so the views only assemble. - Localize fixed labels via rowTitleKey/titleKey and render data-derived names verbatim; placeholders and empty-state texts now take LocalizedStringKey; add the missing zh-Hans entries (All Branches, All Users, Me, Search users, No matching users). - Open the branch popover at the compact 224pt width so the first frame has no dead space; width only changes on explicit user actions (opening a group flyout or typing a query). Tests extended to 11 cases covering mode-consistency and matches() regressions; full macOS suite passes (692 tests / 80 suites).
Wz58luck
commented
Aug 30, 2026
| // An empty search string never matches `localizedCaseInsensitiveContains`, | ||
| // so treat it as "match everything" explicitly. | ||
| func titleMatches(_ title: String) -> Bool { |
There was a problem hiding this comment.
这里搜索用的是硬编码的英文标题,但界面显示的是本地化后的文本。中文环境下搜“全部分支”时,All Branches 不会匹配;User 里的 All Users / Me 也有同样问题。建议给固定项提供统一的本地化标题和搜索关键词(例如同时匹配本地化文本与英文 key),不要让 title 和 rowTitleKey 各维护一份。顺手补一条 zh-Hans 的搜索测试,后面改文案时更不容易漏。
…overs The Branch and User filters in the Git Log filter bar used native NSMenu menus that grew to screen height on repositories with many references or authors, covering the whole workbench (issue 1lck#302). Replace them with custom SwiftUI popovers anchored under their filter chips: - Branch: a two-level menu with the All Branches reset entry, starred shortcuts for the current branch and its upstream, and group rows (Local, per-remote, Tags) that open a bounded flyout column; typing falls back to a flat filtered list. - User: a flat bounded list with pinned reset entries and stable author ordering. Both popovers are searchable, cap their height with internal scrolling, and reuse the existing selectGitReference action without any data-layer or shared-contract changes. Review feedback on this PR is folded in: fixed rows are modeled with an explicit Kind enum; browse and search modes follow identical per-row rules (full names, upstream-only detail, pinned entries surviving search); sections carry an explicit isPinned flag; the branch menu is injected as data instead of a builder closure; selection matching lives on the item types; fixed labels render and match through a single GitLogFilterFixedLabel source so localized text and the English key both find them; the popover opens at its compact width without dead space; zh-Hans gains the missing translations with a table test and bilingual matching test.
d0041b0 to
48113a0Compare
Closes#302
问题
Git Log 筛选栏的 Branch / User 筛选使用原生 NSMenu,分支或作者较多时会展开成
铺满整屏的无界列表(issue 截图),并非 WebView 渲染——macOS 端 Git 视图从未
使用 WebKit。
方案
纯 macOS 视图层改动(不涉及 Rust Core / 共享契约 / Windows):
GitLogBranchFilterPopover:对齐 IDEA 的两级结构——All Branches 重置项、⭐ 当前分支与 upstream 收藏行、分组行(Local / origin/… / Tags)点击后右侧展开
有界可滚动的 flyout 列表;输入搜索词后切换为扁平过滤模式。
GitLogFilterPopover:User 筛选的扁平有界列表(置顶项 + 可搜索 + 稳定排序)。修改前:

修改后:
