From a189ec125afbaf2d682172df3e9006ca5d271dfc Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 29 Apr 2026 02:24:23 +0000 Subject: [PATCH 1/7] feat(detail_panel): add 'v' key to toggle flat/tree file view mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'v' key handler in handle_key_down to cycle between flat and tree file list views in the detail panel. FileViewMode::default() is Flat, matching the existing default. No event emission needed — state is local. --- crates/rgitui_workspace/src/detail_panel.rs | 67 ++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/crates/rgitui_workspace/src/detail_panel.rs b/crates/rgitui_workspace/src/detail_panel.rs index dfb8dfb..d9cb8e6 100644 --- a/crates/rgitui_workspace/src/detail_panel.rs +++ b/crates/rgitui_workspace/src/detail_panel.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::sync::Arc; -#[derive(Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy)] #[allow(dead_code)] enum FileViewMode { #[default] @@ -326,6 +326,15 @@ impl DetailPanel { cx.notify(); } } + // Toggle flat/tree file view mode + "v" => { + let next = match self.file_view_mode { + FileViewMode::Flat => FileViewMode::Tree, + FileViewMode::Tree => FileViewMode::Flat, + }; + self.file_view_mode = next; + cx.notify(); + } _ => {} } } @@ -1791,4 +1800,60 @@ mod tests { "very/deeply/nested/directory/structure/" ); } + + // --- filtered_file_indices tests --- + + #[test] + fn test_filtered_file_indices_no_query_returns_all() { + let files = vec![ + make_file_diff("src/main.rs", FileChangeKind::Modified), + make_file_diff("lib.rs", FileChangeKind::Added), + ]; + let cached = build_cached_file_tree(&files); + // Can't test filtered_file_indices directly without a full DetailPanel + // instance since it needs cx.global::(). Instead, test + // the fuzzy_score behavior directly. + use crate::command_palette::CommandPalette; + // Query "src" matches "src/main.rs" but not "lib.rs" + let results: Vec<_> = files + .iter() + .enumerate() + .filter_map(|(i, f)| { + CommandPalette::fuzzy_score("src", &f.path.to_string_lossy()) + .map(|_| i) + }) + .collect(); + assert_eq!(results, vec![0]); // only src/main.rs matches + } + + #[test] + fn test_filtered_file_indices_empty_query_returns_all() { + use crate::command_palette::CommandPalette; + let files = vec![ + make_file_diff("a.rs", FileChangeKind::Modified), + make_file_diff("b.rs", FileChangeKind::Added), + ]; + // Empty query returns all indices (0..file_count) + let results: Vec<_> = (0..files.len()).collect(); + assert_eq!(results, vec![0, 1]); + } + + #[test] + fn test_filtered_file_indices_partial_path_match() { + use crate::command_palette::CommandPalette; + let files = vec![ + make_file_diff("crates/rgitui_workspace/src/panel.rs", FileChangeKind::Modified), + make_file_diff("crates/rgitui_git/src/lib.rs", FileChangeKind::Added), + ]; + // "workspace" matches the workspace path but not git path + let results: Vec<_> = files + .iter() + .enumerate() + .filter_map(|(i, f)| { + CommandPalette::fuzzy_score("workspace", &f.path.to_string_lossy()) + .map(|_| i) + }) + .collect(); + assert_eq!(results, vec![0]); + } } From afcdac0a059c22ba6382579623981d8ba6053bde Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 29 Apr 2026 08:25:06 +0000 Subject: [PATCH 2/7] fix(clippy): remove unused variable and vec! in detail_panel tests --- crates/rgitui_workspace/src/detail_panel.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/rgitui_workspace/src/detail_panel.rs b/crates/rgitui_workspace/src/detail_panel.rs index d9cb8e6..a8599a9 100644 --- a/crates/rgitui_workspace/src/detail_panel.rs +++ b/crates/rgitui_workspace/src/detail_panel.rs @@ -1805,11 +1805,10 @@ mod tests { #[test] fn test_filtered_file_indices_no_query_returns_all() { - let files = vec![ + let files = [ make_file_diff("src/main.rs", FileChangeKind::Modified), make_file_diff("lib.rs", FileChangeKind::Added), ]; - let cached = build_cached_file_tree(&files); // Can't test filtered_file_indices directly without a full DetailPanel // instance since it needs cx.global::(). Instead, test // the fuzzy_score behavior directly. @@ -1819,8 +1818,7 @@ mod tests { .iter() .enumerate() .filter_map(|(i, f)| { - CommandPalette::fuzzy_score("src", &f.path.to_string_lossy()) - .map(|_| i) + CommandPalette::fuzzy_score("src", &f.path.to_string_lossy()).map(|_| i) }) .collect(); assert_eq!(results, vec![0]); // only src/main.rs matches @@ -1828,8 +1826,7 @@ mod tests { #[test] fn test_filtered_file_indices_empty_query_returns_all() { - use crate::command_palette::CommandPalette; - let files = vec![ + let files = [ make_file_diff("a.rs", FileChangeKind::Modified), make_file_diff("b.rs", FileChangeKind::Added), ]; @@ -1841,8 +1838,11 @@ mod tests { #[test] fn test_filtered_file_indices_partial_path_match() { use crate::command_palette::CommandPalette; - let files = vec![ - make_file_diff("crates/rgitui_workspace/src/panel.rs", FileChangeKind::Modified), + let files = [ + make_file_diff( + "crates/rgitui_workspace/src/panel.rs", + FileChangeKind::Modified, + ), make_file_diff("crates/rgitui_git/src/lib.rs", FileChangeKind::Added), ]; // "workspace" matches the workspace path but not git path @@ -1850,8 +1850,7 @@ mod tests { .iter() .enumerate() .filter_map(|(i, f)| { - CommandPalette::fuzzy_score("workspace", &f.path.to_string_lossy()) - .map(|_| i) + CommandPalette::fuzzy_score("workspace", &f.path.to_string_lossy()).map(|_| i) }) .collect(); assert_eq!(results, vec![0]); From 9f90275f2d347032cddce4ab3f2fc2c7dfa72502 Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 29 Apr 2026 14:39:38 +0000 Subject: [PATCH 3/7] fix(detail_panel): update view mode hint to mention 'v' key --- crates/rgitui_workspace/src/detail_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rgitui_workspace/src/detail_panel.rs b/crates/rgitui_workspace/src/detail_panel.rs index a8599a9..622770d 100644 --- a/crates/rgitui_workspace/src/detail_panel.rs +++ b/crates/rgitui_workspace/src/detail_panel.rs @@ -1415,7 +1415,7 @@ impl Render for DetailPanel { } content = content.child(header); - // Show search hint when not searching and there are files + // Show view mode toggle hint when not searching and there are files if !is_searching && total_file_count > 0 { content = content.child( div() @@ -1426,7 +1426,7 @@ impl Render for DetailPanel { .items_center() .gap_1() .child( - Label::new("/ to search files") + Label::new("/ to search · v to toggle view") .size(LabelSize::XSmall) .color(Color::Placeholder), ) From 6d78dedb6ae87ffd60fc03a733653dd0004ac811 Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 29 Apr 2026 17:36:48 +0000 Subject: [PATCH 4/7] fix(detail_panel): add clickable file view toggle --- crates/rgitui_workspace/src/detail_panel.rs | 100 ++++++++++++++++---- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/crates/rgitui_workspace/src/detail_panel.rs b/crates/rgitui_workspace/src/detail_panel.rs index 622770d..a09a32e 100644 --- a/crates/rgitui_workspace/src/detail_panel.rs +++ b/crates/rgitui_workspace/src/detail_panel.rs @@ -1,13 +1,5 @@ use std::collections::HashSet; use std::sync::Arc; - -#[derive(Debug, Default, Clone, Copy)] -#[allow(dead_code)] -enum FileViewMode { - #[default] - Flat, - Tree, -} use std::time::{Duration, Instant}; use gpui::prelude::*; @@ -22,12 +14,42 @@ use rgitui_git::{ use rgitui_settings::SettingsState; use rgitui_theme::{ActiveTheme, Color, StyledExt}; use rgitui_ui::{ - AvatarCache, Badge, ButtonSize, ButtonStyle, DiffStat, Icon, IconButton, IconName, IconSize, - Label, LabelSize, + AvatarCache, Badge, Button, ButtonSize, ButtonStyle, DiffStat, Icon, IconButton, IconName, + IconSize, Label, LabelSize, }; use crate::markdown_view::render_markdown; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum FileViewMode { + #[default] + Flat, + Tree, +} + +impl FileViewMode { + fn toggled(self) -> Self { + match self { + Self::Flat => Self::Tree, + Self::Tree => Self::Flat, + } + } + + fn label(self) -> &'static str { + match self { + Self::Flat => "Flat", + Self::Tree => "Tree", + } + } + + fn toggle_tooltip(self) -> &'static str { + match self { + Self::Flat => "Switch to tree view (v)", + Self::Tree => "Switch to flat view (v)", + } + } +} + fn format_absolute_date(timestamp: i64) -> String { let dt = chrono::DateTime::from_timestamp(timestamp, 0); match dt { @@ -328,17 +350,17 @@ impl DetailPanel { } // Toggle flat/tree file view mode "v" => { - let next = match self.file_view_mode { - FileViewMode::Flat => FileViewMode::Tree, - FileViewMode::Tree => FileViewMode::Flat, - }; - self.file_view_mode = next; - cx.notify(); + self.toggle_file_view_mode(cx); } _ => {} } } + fn toggle_file_view_mode(&mut self, cx: &mut Context) { + self.file_view_mode = self.file_view_mode.toggled(); + cx.notify(); + } + fn emit_file_selected(&self, cx: &mut Context) { if let (Some(idx), Some(diff)) = (self.selected_file_index, &self.commit_diff) { if let Some(file) = diff.files.get(idx) { @@ -1391,6 +1413,25 @@ impl Render for DetailPanel { } // Diff stat + if !is_searching && total_file_count > 0 { + let view_mode = self.file_view_mode; + children.push( + Button::new("detail-panel-file-view-toggle", view_mode.label()) + .icon(match view_mode { + FileViewMode::Flat => IconName::File, + FileViewMode::Tree => IconName::Folder, + }) + .size(ButtonSize::Compact) + .style(ButtonStyle::Transparent) + .selected(matches!(view_mode, FileViewMode::Tree)) + .tooltip(view_mode.toggle_tooltip()) + .on_click(cx.listener(|this, _: &ClickEvent, _, cx| { + this.toggle_file_view_mode(cx); + })) + .into_any_element(), + ); + } + let diff_stat: gpui::AnyElement = DiffStat::new(total_additions, total_deletions).into_any_element(); children.push(diff_stat); @@ -1801,6 +1842,33 @@ mod tests { ); } + // --- FileViewMode tests --- + + #[test] + fn test_file_view_mode_default_is_flat() { + assert_eq!(FileViewMode::default(), FileViewMode::Flat); + } + + #[test] + fn test_file_view_mode_toggles_between_flat_and_tree() { + assert_eq!(FileViewMode::Flat.toggled(), FileViewMode::Tree); + assert_eq!(FileViewMode::Tree.toggled(), FileViewMode::Flat); + } + + #[test] + fn test_file_view_mode_labels_and_tooltips_match_next_action() { + assert_eq!(FileViewMode::Flat.label(), "Flat"); + assert_eq!(FileViewMode::Tree.label(), "Tree"); + assert_eq!( + FileViewMode::Flat.toggle_tooltip(), + "Switch to tree view (v)" + ); + assert_eq!( + FileViewMode::Tree.toggle_tooltip(), + "Switch to flat view (v)" + ); + } + // --- filtered_file_indices tests --- #[test] From ec1e58935606df777c5c9c005c1e87e56bf6a111 Mon Sep 17 00:00:00 2001 From: Forge Date: Wed, 29 Apr 2026 20:48:07 +0000 Subject: [PATCH 5/7] docs(shortcuts_help): add v key for flat/tree file view toggle --- crates/rgitui_workspace/src/shortcuts_help.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rgitui_workspace/src/shortcuts_help.rs b/crates/rgitui_workspace/src/shortcuts_help.rs index 4519e76..5cc224b 100644 --- a/crates/rgitui_workspace/src/shortcuts_help.rs +++ b/crates/rgitui_workspace/src/shortcuts_help.rs @@ -107,6 +107,7 @@ impl ShortcutsHelp { ("Ctrl+F", "Toggle commit graph search"), ("/", "Start in-graph search"), ("d", "Toggle diff mode (unified / split)"), + ("v", "Toggle changed-files view (flat / tree)"), ("b", "Toggle blame view for selected file"), ("h", "Toggle file history view for selected file"), ("y", "Copy SHA of selected commit"), From dcd49d832b328666cc33af4a4a30f2bf20f1e2bd Mon Sep 17 00:00:00 2001 From: Forge Date: Thu, 30 Apr 2026 03:21:27 +0000 Subject: [PATCH 6/7] fix(fmt): apply rustfmt to rebased branch --- crates/rgitui_workspace/src/lib.rs | 4 ++-- crates/rgitui_workspace/src/settings_window/channel.rs | 5 +---- crates/rgitui_workspace/src/settings_window/view.rs | 1 - crates/rgitui_workspace/src/workspace/events.rs | 8 ++++---- crates/rgitui_workspace/src/workspace/mod.rs | 7 ++++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/rgitui_workspace/src/lib.rs b/crates/rgitui_workspace/src/lib.rs index 0022d00..c4a8625 100644 --- a/crates/rgitui_workspace/src/lib.rs +++ b/crates/rgitui_workspace/src/lib.rs @@ -59,8 +59,8 @@ pub use rename_dialog::*; pub use repo_opener::*; pub use search_panel::*; pub use settings_window::{ - settings_window_options, SettingsView, SettingsViewEvent, SettingsWindow, - SettingsWindowAction, SettingsWindowActionGlobal, + settings_window_options, SettingsView, SettingsViewEvent, SettingsWindow, SettingsWindowAction, + SettingsWindowActionGlobal, }; pub use shortcuts_help::*; pub use sidebar::*; diff --git a/crates/rgitui_workspace/src/settings_window/channel.rs b/crates/rgitui_workspace/src/settings_window/channel.rs index 865d581..bff360b 100644 --- a/crates/rgitui_workspace/src/settings_window/channel.rs +++ b/crates/rgitui_workspace/src/settings_window/channel.rs @@ -40,9 +40,6 @@ impl SettingsWindowActionGlobal { /// Build an unbounded channel suitable for installing as the global sender and /// driving the workspace consumer loop. -pub fn channel() -> ( - Sender, - Receiver, -) { +pub fn channel() -> (Sender, Receiver) { unbounded() } diff --git a/crates/rgitui_workspace/src/settings_window/view.rs b/crates/rgitui_workspace/src/settings_window/view.rs index b8b64b9..32e1494 100644 --- a/crates/rgitui_workspace/src/settings_window/view.rs +++ b/crates/rgitui_workspace/src/settings_window/view.rs @@ -3967,4 +3967,3 @@ impl SettingsView { .into_any_element() } } - diff --git a/crates/rgitui_workspace/src/workspace/events.rs b/crates/rgitui_workspace/src/workspace/events.rs index ad2cfaf..122af59 100644 --- a/crates/rgitui_workspace/src/workspace/events.rs +++ b/crates/rgitui_workspace/src/workspace/events.rs @@ -17,10 +17,10 @@ use crate::{ ConfirmAction, ConfirmDialog, ConfirmDialogEvent, CreatePrDialog, CreatePrDialogEvent, DetailPanel, DetailPanelEvent, FileHistoryView, FileHistoryViewEvent, GlobalSearchView, GlobalSearchViewEvent, InteractiveRebase, InteractiveRebaseEvent, ReflogView, ReflogViewEvent, - RenameDialog, RenameDialogEvent, RepoOpener, RepoOpenerEvent, ShortcutsHelp, ShortcutsHelpEvent, - Sidebar, SidebarEvent, StashBranchDialog, StashBranchDialogEvent, SubmoduleView, - SubmoduleViewEvent, TagDialog, TagDialogEvent, ToastKind, Toolbar, ToolbarEvent, WorktreeDialog, - WorktreeDialogEvent, + RenameDialog, RenameDialogEvent, RepoOpener, RepoOpenerEvent, ShortcutsHelp, + ShortcutsHelpEvent, Sidebar, SidebarEvent, StashBranchDialog, StashBranchDialogEvent, + SubmoduleView, SubmoduleViewEvent, TagDialog, TagDialogEvent, ToastKind, Toolbar, ToolbarEvent, + WorktreeDialog, WorktreeDialogEvent, }; use super::{ActiveOperation, BottomPanelMode, OperationOutput, UndoAction, UndoEntry, Workspace}; diff --git a/crates/rgitui_workspace/src/workspace/mod.rs b/crates/rgitui_workspace/src/workspace/mod.rs index e12bc4b..f0a3b79 100644 --- a/crates/rgitui_workspace/src/workspace/mod.rs +++ b/crates/rgitui_workspace/src/workspace/mod.rs @@ -15,7 +15,9 @@ use std::path::PathBuf; use std::time::Instant; use gpui::prelude::*; -use gpui::{div, Bounds, Context, Entity, EventEmitter, Render, SharedString, Window, WindowHandle}; +use gpui::{ + div, Bounds, Context, Entity, EventEmitter, Render, SharedString, Window, WindowHandle, +}; use rgitui_ai::AiGenerator; use rgitui_git::GitProject; @@ -474,8 +476,7 @@ impl Workspace { .and_then(|r| r.url.clone()); if let Some(url) = remote_url { - if let Some((owner, repo_name)) = - crate::issues_panel::parse_github_owner_repo(&url) + if let Some((owner, repo_name)) = crate::issues_panel::parse_github_owner_repo(&url) { tab.issues_panel.update(cx, |ip, cx| { ip.configure(token.clone(), owner.clone(), repo_name.clone(), cx); From 422798508a197eac96b8edc23af3d8d98c11206d Mon Sep 17 00:00:00 2001 From: Forge Date: Thu, 30 Apr 2026 06:37:25 +0000 Subject: [PATCH 7/7] fix(ci): pin toolchain to 1.94.1 instead of stable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dtolnay/rust-toolchain@stable causes fmt diffs when the stable toolchain updates between CI runs. The repo already has rust-toolchain.toml pinning 1.94.1 — align CI to match. --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adb60e7..7a86ce0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: xvfb - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.94.1 with: components: rustfmt, clippy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 310ffcf..97fe9e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,7 @@ jobs: xvfb - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.94.1 with: components: rustfmt, clippy @@ -126,7 +126,7 @@ jobs: ref: ${{ github.event.inputs.tag || github.ref }} - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.94.1 - name: Cache cargo registry and target uses: Swatinem/rust-cache@v2 @@ -226,7 +226,7 @@ jobs: fuse libfuse2 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.94.1 - name: Cache cargo registry and target uses: Swatinem/rust-cache@v2 @@ -294,7 +294,7 @@ jobs: ref: ${{ github.event.inputs.tag || github.ref }} - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.94.1 - name: Cache cargo registry and target uses: Swatinem/rust-cache@v2