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 diff --git a/crates/rgitui_workspace/src/detail_panel.rs b/crates/rgitui_workspace/src/detail_panel.rs index dfb8dfb..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(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 { @@ -326,10 +348,19 @@ impl DetailPanel { cx.notify(); } } + // Toggle flat/tree file view mode + "v" => { + 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) { @@ -1382,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); @@ -1406,7 +1456,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() @@ -1417,7 +1467,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), ) @@ -1791,4 +1841,86 @@ mod tests { "very/deeply/nested/directory/structure/" ); } + + // --- 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] + fn test_filtered_file_indices_no_query_returns_all() { + let files = [ + make_file_diff("src/main.rs", FileChangeKind::Modified), + make_file_diff("lib.rs", FileChangeKind::Added), + ]; + // 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() { + let files = [ + 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 = [ + 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]); + } } 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/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"), 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);