mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(sidebar): group a tab by its folder when its cwd is not a repo (#631)
`sidebar_grouping` gains a third, opt-in mode, `repo-or-directory`: group by repository home as before, and when the repo probe has landed and answered "not a repo", group under the cwd itself instead of filing every such tab under Scratch. A probe that has not run yet resolves to no decision, so a tab keeps the group it already has rather than bouncing through Scratch mid-probe. The decision lives in one `resolved_group` free function shared by the per-frame key derivation and spawn-time seeding. The default (`repo`) and flat modes behave exactly as before, and an unknown value in an existing config still degrades to `repo`. Knock-on: `machine_mirror::subject_path_of` names a window after its most common group, so in the new mode a window of plain shells takes its name from the most common directory rather than from the first pane's cwd. Closes #620.
This commit is contained in:
@@ -419,6 +419,9 @@ pub enum WindowBackdrop {
|
||||
pub enum SidebarGrouping {
|
||||
#[default]
|
||||
Repo,
|
||||
/// By repository where there is one; a tab whose cwd is known not to be
|
||||
/// in a repo groups under that cwd instead of falling to Scratch.
|
||||
RepoOrDirectory,
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -1201,6 +1204,27 @@ mod tests {
|
||||
assert!(!back.sidebar_diff_preview);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_grouping_defaults_and_round_trips_leniently() {
|
||||
assert_eq!(Config::default().sidebar_grouping, SidebarGrouping::Repo);
|
||||
|
||||
let text = serde_json::to_string(&Config {
|
||||
sidebar_grouping: SidebarGrouping::RepoOrDirectory,
|
||||
..Config::default()
|
||||
})
|
||||
.unwrap();
|
||||
assert!(text.contains("\"sidebar_grouping\":\"repo-or-directory\""));
|
||||
let back: Config = serde_json::from_str(&text).unwrap();
|
||||
assert_eq!(back.sidebar_grouping, SidebarGrouping::RepoOrDirectory);
|
||||
|
||||
let flat: Config = serde_json::from_str(r#"{"sidebar_grouping":"none"}"#).unwrap();
|
||||
assert_eq!(flat.sidebar_grouping, SidebarGrouping::None);
|
||||
|
||||
// Unknown values fall back to Repo instead of rejecting the whole config.
|
||||
let lenient: Config = serde_json::from_str(r#"{"sidebar_grouping":"folders"}"#).unwrap();
|
||||
assert_eq!(lenient.sidebar_grouping, SidebarGrouping::Repo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dim_inactive_panes_defaults_on_and_round_trips() {
|
||||
assert!(Config::default().dim_inactive_panes);
|
||||
|
||||
@@ -69,7 +69,7 @@ their id from the file name. [More about themes →](/customization/themes)
|
||||
|---|---|---|---|
|
||||
| `tab_bar_position` | enum | `"left"` | `left` (sidebar) or `top` (strip). |
|
||||
| `new_tab_position` | enum | `"after-current"` | Or `end`. |
|
||||
| `sidebar_grouping` | enum | `"repo"` | Or `none` for a flat list. |
|
||||
| `sidebar_grouping` | enum | `"repo"` | Or `repo-or-directory` to group non-repo tabs by their folder, or `none` for a flat list. |
|
||||
| `sidebar_diff_preview` | bool | `true` | Clicking a row's `+N −M` opens the diff overlay. |
|
||||
| `sidebar_width` | number | `220` | Pixels (100–2000). |
|
||||
| `sidebar_collapsed` | bool | `false` | |
|
||||
|
||||
@@ -22,7 +22,10 @@ not its history — switching branches or `cd`-ing around inside a repository
|
||||
never moves a row out from under its header.
|
||||
|
||||
**Settings → Window & Tabs → Sidebar grouping** switches between *By repo* (the
|
||||
default) and *Flat*.
|
||||
default), *By repo or folder*, and *Flat*. The default collects every non-repo
|
||||
tab in Scratch; *By repo or folder* groups those tabs under their working
|
||||
directory instead — for agents running in plain folders — leaving Scratch to
|
||||
tabs whose shell has not reported a directory yet.
|
||||
|
||||
## What a row tells you
|
||||
|
||||
@@ -52,9 +55,8 @@ they simply stop opening the overlay.
|
||||
## Rearranging
|
||||
|
||||
Drag a row to reorder it within its group, or drag a whole group header to move
|
||||
the group. A row cannot be dragged into a different group: with the default repo
|
||||
grouping a tab's group comes from its working directory, so `cd` is what moves
|
||||
it.
|
||||
the group. A row cannot be dragged into a different group: a tab's group comes
|
||||
from its working directory, so `cd` is what moves it.
|
||||
|
||||
## Naming
|
||||
|
||||
|
||||
+3
-2
@@ -547,7 +547,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
}
|
||||
L10nKey::SettingsSidebarGrouping => "Sidebar grouping",
|
||||
L10nKey::SettingsSidebarGroupingDesc => {
|
||||
"Group sidebar tabs under a header per git repository, with non-repo tabs in a Scratch section. Only applies to the left sidebar."
|
||||
"Group sidebar tabs under a header per git repository. Non-repo tabs collect in a Scratch section, or under their working directory with \"By repo or folder\". Only applies to the left sidebar."
|
||||
}
|
||||
L10nKey::SettingsDiffPreviewFromCounts => "Open diff preview from sidebar counts",
|
||||
L10nKey::SettingsDiffPreviewFromCountsDesc => {
|
||||
@@ -574,6 +574,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsTop => "Top",
|
||||
L10nKey::SettingsLeft => "Left",
|
||||
L10nKey::SettingsByRepo => "By repo",
|
||||
L10nKey::SettingsByRepoOrFolder => "By repo or folder",
|
||||
L10nKey::SettingsFlat => "Flat",
|
||||
L10nKey::SettingsPreset => "Preset",
|
||||
L10nKey::SettingsPresetDesc => {
|
||||
@@ -834,7 +835,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
"tray menu bar status item agent attention system icon"
|
||||
}
|
||||
L10nKey::SettingsSearchSidebarGroupingKeywords => {
|
||||
"tabs group repo repository git scratch header sidebar flat"
|
||||
"tabs group repo repository git scratch header sidebar flat folder directory cwd"
|
||||
}
|
||||
L10nKey::SettingsSearchSmartSelectionKeywords => {
|
||||
"double click word url path select semantic bracket email"
|
||||
|
||||
+3
-2
@@ -554,7 +554,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
}
|
||||
L10nKey::SettingsSidebarGrouping => "サイドバーのグループ化",
|
||||
L10nKey::SettingsSidebarGroupingDesc => {
|
||||
"git リポジトリごとにサイドバータブをまとめ、リポジトリ外のタブはスクラッチセクションに置きます。左サイドバーにのみ適用"
|
||||
"git リポジトリごとにサイドバータブをまとめます。リポジトリ外のタブはスクラッチセクションに置くか、「リポジトリ/フォルダ別」なら作業ディレクトリごとにまとめます。左サイドバーにのみ適用"
|
||||
}
|
||||
L10nKey::SettingsDiffPreviewFromCounts => "サイドバーのカウントから Diff プレビューを開く",
|
||||
L10nKey::SettingsDiffPreviewFromCountsDesc => {
|
||||
@@ -579,6 +579,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsTop => "上部",
|
||||
L10nKey::SettingsLeft => "左側",
|
||||
L10nKey::SettingsByRepo => "リポジトリ別",
|
||||
L10nKey::SettingsByRepoOrFolder => "リポジトリ/フォルダ別",
|
||||
L10nKey::SettingsFlat => "フラット表示",
|
||||
L10nKey::SettingsPreset => "プリセット",
|
||||
L10nKey::SettingsPresetDesc => {
|
||||
@@ -880,7 +881,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
"トレイ メニューバー ステータス アイコン エージェント 通知 システム tray icon menu bar status system attention"
|
||||
}
|
||||
L10nKey::SettingsSearchSidebarGroupingKeywords => {
|
||||
"タブ グループ リポジトリ git スクラッチ ヘッダー サイドバー フラット sidebar grouping tabs repo repository git scratch header flat"
|
||||
"タブ グループ リポジトリ git スクラッチ ヘッダー サイドバー フラット フォルダ ディレクトリ sidebar grouping tabs repo repository git scratch header flat folder directory"
|
||||
}
|
||||
L10nKey::SettingsSearchSmartSelectionKeywords => {
|
||||
"ダブルクリック 単語 url パス 選択 セマンティック 括弧 メール smart selection double click word url path bracket email"
|
||||
|
||||
@@ -478,6 +478,7 @@ l10n_keys! {
|
||||
SettingsTop,
|
||||
SettingsLeft,
|
||||
SettingsByRepo,
|
||||
SettingsByRepoOrFolder,
|
||||
SettingsFlat,
|
||||
SettingsPreset,
|
||||
SettingsPresetDesc,
|
||||
|
||||
+3
-2
@@ -480,7 +480,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsTabBarPositionDesc => "将标签页显示为顶部横向条或左侧垂直侧栏。",
|
||||
L10nKey::SettingsSidebarGrouping => "侧栏分组",
|
||||
L10nKey::SettingsSidebarGroupingDesc => {
|
||||
"按 git 仓库在标题下对侧栏标签页分组,非仓库标签页放在“草稿”分组。仅适用于左侧栏。"
|
||||
"按 git 仓库在标题下对侧栏标签页分组;非仓库标签页放在“草稿”分组,选“按仓库或文件夹”时则按其工作目录分组。仅适用于左侧栏。"
|
||||
}
|
||||
L10nKey::SettingsDiffPreviewFromCounts => "从侧栏计数打开 diff 预览",
|
||||
L10nKey::SettingsDiffPreviewFromCountsDesc => {
|
||||
@@ -503,6 +503,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsTop => "顶部",
|
||||
L10nKey::SettingsLeft => "左侧",
|
||||
L10nKey::SettingsByRepo => "按仓库",
|
||||
L10nKey::SettingsByRepoOrFolder => "按仓库或文件夹",
|
||||
L10nKey::SettingsFlat => "平铺",
|
||||
L10nKey::SettingsPreset => "预设",
|
||||
L10nKey::SettingsPresetDesc => {
|
||||
@@ -786,7 +787,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
"显示托盘图标 托盘 菜单栏 状态 图标 show tray icon menu bar status"
|
||||
}
|
||||
L10nKey::SettingsSearchSidebarGroupingKeywords => {
|
||||
"侧栏分组 标签页 分组 仓库 git 侧栏 sidebar grouping tabs repo repository"
|
||||
"侧栏分组 标签页 分组 仓库 git 侧栏 文件夹 目录 sidebar grouping tabs repo repository folder directory"
|
||||
}
|
||||
L10nKey::SettingsSearchSmartSelectionKeywords => {
|
||||
"智能选择 双击 选择 单词 URL 路径 邮箱 括号 smart selection double click"
|
||||
|
||||
+11
-6
@@ -5774,7 +5774,8 @@ impl Tty7App {
|
||||
let sidebar_diff_preview = cfg.sidebar_diff_preview;
|
||||
let sidebar_grouping_idx = match cfg.sidebar_grouping {
|
||||
crate::core::config::SidebarGrouping::Repo => 0,
|
||||
crate::core::config::SidebarGrouping::None => 1,
|
||||
crate::core::config::SidebarGrouping::RepoOrDirectory => 1,
|
||||
crate::core::config::SidebarGrouping::None => 2,
|
||||
};
|
||||
let notify_idx = match cfg.notify_on_command_finish {
|
||||
NotifyMode::Never => 0,
|
||||
@@ -5885,14 +5886,18 @@ impl Tty7App {
|
||||
.into_any_element();
|
||||
let sidebar_grouping_radio = self.segmented(
|
||||
"wt-sidebar-grouping",
|
||||
&[t(L10nKey::SettingsByRepo), t(L10nKey::SettingsFlat)],
|
||||
&[
|
||||
t(L10nKey::SettingsByRepo),
|
||||
t(L10nKey::SettingsByRepoOrFolder),
|
||||
t(L10nKey::SettingsFlat),
|
||||
],
|
||||
sidebar_grouping_idx,
|
||||
cx,
|
||||
|this, ix, _w, cx| {
|
||||
let grouping = if ix == 0 {
|
||||
crate::core::config::SidebarGrouping::Repo
|
||||
} else {
|
||||
crate::core::config::SidebarGrouping::None
|
||||
let grouping = match ix {
|
||||
0 => crate::core::config::SidebarGrouping::Repo,
|
||||
1 => crate::core::config::SidebarGrouping::RepoOrDirectory,
|
||||
_ => crate::core::config::SidebarGrouping::None,
|
||||
};
|
||||
this.set_sidebar_grouping(grouping, cx);
|
||||
},
|
||||
|
||||
+63
-10
@@ -1176,21 +1176,22 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
fn sidebar_group_keys(&self, cx: &gpui::App) -> Vec<Option<PathBuf>> {
|
||||
let grouping = cx.global::<Config>().sidebar_grouping == SidebarGrouping::Repo;
|
||||
let grouping = cx.global::<Config>().sidebar_grouping;
|
||||
self.tabs
|
||||
.iter()
|
||||
.map(|tab| {
|
||||
if !grouping {
|
||||
if grouping == SidebarGrouping::None {
|
||||
return None;
|
||||
}
|
||||
let cwd = tab.pane.first_leaf().and_then(|leaf| {
|
||||
let view = leaf.terminal()?.read(cx);
|
||||
Some((view.host_id(), view.git_status_cwd()?.to_path_buf()))
|
||||
});
|
||||
if let Some(known) =
|
||||
cwd.and_then(|(id, cwd)| cx.global::<GitStatusCache>().known_repo_for(id, &cwd))
|
||||
{
|
||||
*tab.sidebar_group.borrow_mut() = known;
|
||||
if let Some((id, cwd)) = cwd {
|
||||
let known = cx.global::<GitStatusCache>().known_repo_for(id, &cwd);
|
||||
if let Some(group) = resolved_group(grouping, known, &cwd) {
|
||||
*tab.sidebar_group.borrow_mut() = group;
|
||||
}
|
||||
}
|
||||
tab.sidebar_group.borrow().clone()
|
||||
})
|
||||
@@ -1220,8 +1221,10 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
/// Which group a tab about to be spawned in `cwd` belongs to, when the
|
||||
/// repo probe for that directory has already landed. `Some(None)` means
|
||||
/// the cache knows it is not a repo; a bare `None` means it never looked.
|
||||
/// repo probe for that directory has already landed. A bare `None` means
|
||||
/// the cache never looked; `Some` is the group [`resolved_group`] reached
|
||||
/// — the repo home, the cwd itself under repo-or-directory grouping, or
|
||||
/// `Some(None)` for Scratch.
|
||||
///
|
||||
/// A tab's group otherwise starts empty and only fills in once its shell
|
||||
/// has started and reported a cwd, which parks every new tab in the
|
||||
@@ -1233,15 +1236,34 @@ impl Tty7App {
|
||||
cwd: Option<&Path>,
|
||||
cx: &gpui::App,
|
||||
) -> Option<Option<PathBuf>> {
|
||||
let cwd = cwd?;
|
||||
let host = self
|
||||
.window_workspace(cx)
|
||||
.as_ref()
|
||||
.map_or(crate::ui::host_ops::HostId::LOCAL, |ws| ws.target.host_id());
|
||||
cx.try_global::<GitStatusCache>()?
|
||||
.known_repo_for(host, cwd?)
|
||||
let known = cx.try_global::<GitStatusCache>()?.known_repo_for(host, cwd);
|
||||
resolved_group(cx.global::<Config>().sidebar_grouping, known, cwd)
|
||||
}
|
||||
}
|
||||
|
||||
/// The group a probed cwd resolves to under `grouping`: the repo home when
|
||||
/// the cache found one, otherwise Scratch — or the cwd itself under
|
||||
/// repo-or-directory grouping, so a shell in a plain folder still gets a
|
||||
/// header. `known` is the cache's three-valued answer; a probe that never
|
||||
/// ran resolves to `None`, no decision, and the tab keeps whatever group it
|
||||
/// already has rather than bouncing through Scratch mid-probe.
|
||||
fn resolved_group(
|
||||
grouping: SidebarGrouping,
|
||||
known: Option<Option<PathBuf>>,
|
||||
cwd: &Path,
|
||||
) -> Option<Option<PathBuf>> {
|
||||
Some(match known? {
|
||||
Some(root) => Some(root),
|
||||
None if grouping == SidebarGrouping::RepoOrDirectory => Some(cwd.to_path_buf()),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Section {
|
||||
key: Option<PathBuf>,
|
||||
@@ -1422,6 +1444,37 @@ mod tests {
|
||||
assert_eq!(diff_click_cwd::<PathBuf>(&cfg, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_probed_non_repo_groups_by_folder_only_in_the_fallback_mode() {
|
||||
let cwd = p("/w/plain");
|
||||
assert_eq!(
|
||||
resolved_group(SidebarGrouping::RepoOrDirectory, Some(None), &cwd),
|
||||
Some(Some(p("/w/plain")))
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_group(SidebarGrouping::Repo, Some(None), &cwd),
|
||||
Some(None),
|
||||
"under Repo a probed non-repo still falls to Scratch"
|
||||
);
|
||||
// Never probed: no decision in either mode, so the tab keeps the
|
||||
// group it already has instead of bouncing through Scratch.
|
||||
assert_eq!(resolved_group(SidebarGrouping::Repo, None, &cwd), None);
|
||||
assert_eq!(
|
||||
resolved_group(SidebarGrouping::RepoOrDirectory, None, &cwd),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_known_repo_home_wins_over_the_folder_in_both_modes() {
|
||||
for mode in [SidebarGrouping::Repo, SidebarGrouping::RepoOrDirectory] {
|
||||
assert_eq!(
|
||||
resolved_group(mode, Some(Some(p("/w/repo"))), &p("/w/repo/sub")),
|
||||
Some(Some(p("/w/repo")))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sections_order_groups_by_first_appearance_scratch_last() {
|
||||
let keys = vec![
|
||||
|
||||
Reference in New Issue
Block a user