Skip to content

Commit ceea25f

Browse files
committed
Tables: Add select all rows functionality
* Add action to select all logs for both tables context menu * Add shortcut to select all logs from the active table `Ctrl/Cmd + A` * Add Select all logs and search results to command palette. * Rename selection scope fields to main and search
1 parent 1cc84e8 commit ceea25f

11 files changed

Lines changed: 315 additions & 37 deletions

File tree

‎crates/app/src/host/ui/command_palette/commands.rs‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ const COMMANDS: &[CommandDefinition] = &[
131131
title: "Find in Search Results",
132132
action: CommandAction::NestedSearch,
133133
},
134+
CommandDefinition {
135+
title: "Select All Logs",
136+
action: CommandAction::SelectAllLogs,
137+
},
138+
CommandDefinition {
139+
title: "Select All Search Results",
140+
action: CommandAction::SelectAllSearchResults,
141+
},
134142
CommandDefinition {
135143
title: "Terminal with Plain Text",
136144
action: CommandAction::ConnectionSetup {
@@ -241,6 +249,8 @@ pub enum CommandAction {
241249
ToggleSdeBar,
242250
JumpToRow,
243251
NestedSearch,
252+
SelectAllLogs,
253+
SelectAllSearchResults,
244254
ConnectionSetup {
245255
stream: StreamNames,
246256
parser: ParserNames,
@@ -257,7 +267,10 @@ pub enum CommandScope {
257267
impl CommandAction {
258268
fn scope(self) -> CommandScope {
259269
match self {
260-
Self::JumpToRow | Self::NestedSearch => CommandScope::Session,
270+
Self::JumpToRow
271+
| Self::NestedSearch
272+
| Self::SelectAllLogs
273+
| Self::SelectAllSearchResults => CommandScope::Session,
261274
Self::GoHome
262275
| Self::OpenFiles
263276
| Self::OpenFilesWithPlugin
@@ -437,6 +450,18 @@ pub fn execute_action(
437450
}
438451
true
439452
}
453+
CommandAction::SelectAllLogs => {
454+
if let HostTab::Session(session) = tabs.active_mut() {
455+
session.select_all_logs(ui.ctx());
456+
}
457+
true
458+
}
459+
CommandAction::SelectAllSearchResults => {
460+
if let HostTab::Session(session) = tabs.active_mut() {
461+
session.select_all_search_results(&mut state.preferences, ui.ctx());
462+
}
463+
true
464+
}
440465
CommandAction::ConnectionSetup { stream, parser } => actions.try_send_command(
441466
cmd_tx,
442467
HostCommand::ConnectionSessionSetup { stream, parser },
@@ -494,10 +519,14 @@ mod tests {
494519
let global_actions = result_actions(&global_results);
495520
assert!(!global_actions.contains(&CommandAction::JumpToRow));
496521
assert!(!global_actions.contains(&CommandAction::NestedSearch));
522+
assert!(!global_actions.contains(&CommandAction::SelectAllLogs));
523+
assert!(!global_actions.contains(&CommandAction::SelectAllSearchResults));
497524

498525
let session_results = recompute_results(&mut matcher, CommandScope::Session);
499526
let session_actions = result_actions(&session_results);
500527
assert!(session_actions.contains(&CommandAction::JumpToRow));
501528
assert!(session_actions.contains(&CommandAction::NestedSearch));
529+
assert!(session_actions.contains(&CommandAction::SelectAllLogs));
530+
assert!(session_actions.contains(&CommandAction::SelectAllSearchResults));
502531
}
503532
}

‎crates/app/src/host/ui/command_palette/row.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ fn action_icon(action: CommandAction) -> &'static str {
134134
CommandAction::ToggleSdeBar => icons::regular::TERMINAL_WINDOW,
135135
CommandAction::JumpToRow => icons::regular::LIST_NUMBERS,
136136
CommandAction::NestedSearch => icons::regular::MAGNIFYING_GLASS,
137+
CommandAction::SelectAllLogs | CommandAction::SelectAllSearchResults => {
138+
icons::regular::SELECTION_ALL
139+
}
137140
CommandAction::ConnectionSetup { .. } => icons::regular::TERMINAL,
138141
}
139142
}

‎crates/app/src/session/ui/bottom_panel/details/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ impl DetailsUI {
8989
{
9090
copy_selected_rows(
9191
shared,
92-
SelectionScope::AllSelected,
92+
SelectionScope::MainTable,
9393
actions,
9494
&self.cmd_tx,
9595
);

‎crates/app/src/session/ui/bottom_panel/search/search_table.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,10 @@ impl SearchTable {
167167
ui: &mut Ui,
168168
) {
169169
// Selection is global, so the search table acts only on rows it actually shows.
170-
let scope = SelectionScope::SearchRows;
170+
let scope = SelectionScope::SearchResults;
171171
let selected_count = scope.count(shared);
172172
copy::render_copy_action(shared, scope, selected_count, actions, &self.cmd_tx, ui);
173+
common::log_table::table::render_select_all_action(shared, scope, ui);
173174
common::log_table::table::render_unselect_action(shared, ui);
174175

175176
let can_start_export = shared.exports.can_start();

‎crates/app/src/session/ui/common/log_table/mod.rs‎

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,70 @@ pub enum LogTableKind {
1515
Search,
1616
}
1717

18-
/// Narrows the global row selection to the rows one table's actions operate on.
18+
/// Identifies the log table whose logical contents an action operates on.
19+
///
20+
/// The row selection is global, so actions started from one table must narrow it to the rows
21+
/// that table actually shows.
1922
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2023
pub enum SelectionScope {
21-
/// Every selected stream row.
22-
AllSelected,
23-
/// Selected rows that belong to the search table's logical contents.
24-
SearchRows,
24+
/// The main logs table, which contains every stream row.
25+
MainTable,
26+
/// The search results table, which contains search matches and bookmarked rows.
27+
SearchResults,
2528
}
2629

2730
impl SelectionScope {
2831
/// Iterates over the selected stream rows inside this scope.
2932
pub fn rows(self, shared: &SessionShared) -> impl Iterator<Item = u64> + '_ {
3033
shared.logs.selected_rows().filter(move |&row| match self {
31-
Self::AllSelected => true,
32-
Self::SearchRows => shared.search.has_match(row) || shared.logs.is_bookmarked(row),
34+
Self::MainTable => true,
35+
Self::SearchResults => shared.search.has_match(row) || shared.logs.is_bookmarked(row),
3336
})
3437
}
3538

3639
/// Returns how many selected rows fall inside this scope.
3740
pub fn count(self, shared: &SessionShared) -> usize {
3841
match self {
39-
Self::AllSelected => shared.logs.selected_count(),
40-
Self::SearchRows => self.rows(shared).count(),
42+
Self::MainTable => shared.logs.selected_count(),
43+
Self::SearchResults => self.rows(shared).count(),
44+
}
45+
}
46+
47+
/// Returns how many rows this table contains in total.
48+
pub fn total_count(self, shared: &SessionShared) -> usize {
49+
match self {
50+
Self::MainTable => shared.logs.logs_count() as usize,
51+
// Bookmarks are user-created and few, so correcting the overlap per frame is cheap.
52+
Self::SearchResults => {
53+
let extra_bookmarks = shared
54+
.logs
55+
.bookmarked_rows
56+
.iter()
57+
.filter(|row| !shared.search.has_match(**row))
58+
.count();
59+
60+
shared.search.matches_count() + extra_bookmarks
61+
}
62+
}
63+
}
64+
65+
/// Replaces the selection with every row this table contains.
66+
///
67+
/// This reports no [`SelectionChange`](crate::session::ui::shared::SelectionChange): selecting
68+
/// a whole table must not load details or realign the peer table.
69+
pub fn select_all(self, shared: &mut SessionShared) {
70+
match self {
71+
Self::MainTable => {
72+
let logs_count = shared.logs.logs_count();
73+
shared.logs.select_rows(0..logs_count);
74+
}
75+
Self::SearchResults => {
76+
// Bookmarks live in `LogsState`, which is borrowed mutably below.
77+
let bookmarks: Vec<u64> = shared.logs.bookmarked_rows.iter().copied().collect();
78+
shared
79+
.logs
80+
.select_rows(shared.search.match_positions().chain(bookmarks));
81+
}
4182
}
4283
}
4384
}
@@ -95,27 +136,59 @@ mod tests {
95136
shared
96137
}
97138

139+
fn sorted_selection(shared: &SessionShared) -> Vec<u64> {
140+
let mut rows = shared.logs.selected_rows().collect::<Vec<_>>();
141+
rows.sort_unstable();
142+
143+
rows
144+
}
145+
98146
#[test]
99147
fn search_scope_includes_matches_and_bookmarks_only() {
100148
let shared = shared_with_selection();
101149

102-
let mut rows = SelectionScope::SearchRows.rows(&shared).collect::<Vec<_>>();
150+
let mut rows = SelectionScope::SearchResults
151+
.rows(&shared)
152+
.collect::<Vec<_>>();
103153
rows.sort_unstable();
104154

105155
assert_eq!(rows, vec![10, 20, 30]);
106-
assert_eq!(SelectionScope::SearchRows.count(&shared), 3);
156+
assert_eq!(SelectionScope::SearchResults.count(&shared), 3);
107157
}
108158

109159
#[test]
110-
fn all_selected_scope_keeps_whole_selection() {
160+
fn main_table_scope_keeps_whole_selection() {
111161
let shared = shared_with_selection();
112162

113-
let mut rows = SelectionScope::AllSelected
114-
.rows(&shared)
115-
.collect::<Vec<_>>();
163+
let mut rows = SelectionScope::MainTable.rows(&shared).collect::<Vec<_>>();
116164
rows.sort_unstable();
117165

118166
assert_eq!(rows, vec![10, 20, 30, 40]);
119-
assert_eq!(SelectionScope::AllSelected.count(&shared), 4);
167+
assert_eq!(SelectionScope::MainTable.count(&shared), 4);
168+
}
169+
170+
#[test]
171+
fn main_table_select_all_replaces_selection_with_every_stream_row() {
172+
let mut shared = shared_with_selection();
173+
shared.logs.set_logs_count(5);
174+
175+
assert_eq!(SelectionScope::MainTable.total_count(&shared), 5);
176+
177+
SelectionScope::MainTable.select_all(&mut shared);
178+
179+
assert_eq!(sorted_selection(&shared), vec![0, 1, 2, 3, 4]);
180+
}
181+
182+
#[test]
183+
fn search_results_select_all_takes_matches_and_bookmarks_only() {
184+
let mut shared = shared_with_selection();
185+
shared.logs.set_logs_count(50);
186+
187+
// Row 30 is both a match and a bookmark, so the overlap must not be counted twice.
188+
assert_eq!(SelectionScope::SearchResults.total_count(&shared), 3);
189+
190+
SelectionScope::SearchResults.select_all(&mut shared);
191+
192+
assert_eq!(sorted_selection(&shared), vec![10, 20, 30]);
120193
}
121194
}

‎crates/app/src/session/ui/common/log_table/table.rs‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use std::ops::{Range, RangeInclusive};
88

99
use egui::{
10-
Align, Color32, CursorIcon, Frame, Label, Layout, Margin, Rect, Response, RichText, Sense,
11-
Shape, Stroke, TextStyle, Ui, Widget as _,
10+
Align, Button, Color32, CursorIcon, Frame, Label, Layout, Margin, Rect, Response, RichText,
11+
Sense, Shape, Stroke, TextStyle, Ui, Widget as _,
1212
text::{LayoutJob, TextFormat},
1313
vec2,
1414
};
@@ -31,11 +31,12 @@ use crate::{
3131
definitions::schema::LogSchema,
3232
logs_table::LogAttachmentInfo,
3333
shared::{ObserveState, SelectionChange, SelectionIntent, SessionShared, UiViewState},
34+
shortcuts::SELECT_ALL_ROWS_SHORTCUT,
3435
},
3536
},
3637
};
3738

38-
use super::LogTableKind;
39+
use super::{LogTableKind, SelectionScope};
3940

4041
/// Constants needed when sending grab logs commands.
4142
pub mod grab_cmd_consts {
@@ -65,6 +66,23 @@ pub enum TableScroll {
6566
Bottom,
6667
}
6768

69+
/// Renders the shared context-menu command for selecting every row of one log table.
70+
pub fn render_select_all_action(shared: &mut SessionShared, scope: SelectionScope, ui: &mut Ui) {
71+
let total_count = scope.total_count(shared);
72+
let label = if total_count == 0 {
73+
String::from("Select All")
74+
} else {
75+
format!("Select All {total_count} row(s)")
76+
};
77+
78+
let shortcut_text = ui.ctx().format_shortcut(&SELECT_ALL_ROWS_SHORTCUT);
79+
let button = Button::new(label).shortcut_text(shortcut_text);
80+
if ui.add_enabled(total_count > 0, button).clicked() {
81+
scope.select_all(shared);
82+
ui.close();
83+
}
84+
}
85+
6886
/// Renders the shared context-menu command for clearing log selection.
6987
pub fn render_unselect_action(shared: &mut SessionShared, ui: &mut Ui) {
7088
let selected_count = shared.logs.selected_count();

‎crates/app/src/session/ui/logs_table/mod.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,10 @@ impl LogsTable {
152152
actions: &mut UiActions,
153153
ui: &mut Ui,
154154
) {
155-
let scope = SelectionScope::AllSelected;
155+
let scope = SelectionScope::MainTable;
156156
let selected_count = scope.count(shared);
157157
copy::render_copy_action(shared, scope, selected_count, actions, &self.cmd_tx, ui);
158+
table::render_select_all_action(shared, scope, ui);
158159
table::render_unselect_action(shared, ui);
159160

160161
let can_start_export = shared.exports.can_start();

0 commit comments

Comments
 (0)