aboutsummaryrefslogtreecommitdiff
path: root/src/app/state.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/state.rs')
-rw-r--r--src/app/state.rs117
1 files changed, 117 insertions, 0 deletions
diff --git a/src/app/state.rs b/src/app/state.rs
index 0321a6ee..ee9c69be 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -869,6 +869,48 @@ pub(crate) struct NavigatorRow {
pub is_tab: bool,
pub expanded: bool,
pub search_text: String,
+ /// Whether this row itself matched the active query/state filter, as
+ /// opposed to being included as ancestor context or cascaded subtree of a
+ /// matching workspace or tab. Always true when no filter is active.
+ pub matched: bool,
+}
+
+/// One rendered line in the navigator body. Spacer lines separate workspace
+/// groups visually and are not selectable.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum NavigatorDisplayLine {
+ Spacer,
+ Row(usize),
+}
+
+pub(crate) fn navigator_display_lines(rows: &[NavigatorRow]) -> Vec<NavigatorDisplayLine> {
+ let mut lines = Vec::with_capacity(rows.len().saturating_mul(2));
+ for (idx, row) in rows.iter().enumerate() {
+ if row.is_workspace && !lines.is_empty() {
+ lines.push(NavigatorDisplayLine::Spacer);
+ }
+ lines.push(NavigatorDisplayLine::Row(idx));
+ }
+ lines
+}
+
+pub(crate) fn navigator_display_index_of_row(
+ lines: &[NavigatorDisplayLine],
+ row_idx: usize,
+) -> Option<usize> {
+ lines
+ .iter()
+ .position(|line| *line == NavigatorDisplayLine::Row(row_idx))
+}
+
+pub(crate) fn navigator_first_row_at_or_after(
+ lines: &[NavigatorDisplayLine],
+ line_idx: usize,
+) -> Option<usize> {
+ lines.get(line_idx..)?.iter().find_map(|line| match line {
+ NavigatorDisplayLine::Row(idx) => Some(*idx),
+ NavigatorDisplayLine::Spacer => None,
+ })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2289,6 +2331,81 @@ mod tests {
state.assert_invariants_for_test();
}
+ fn navigator_row_for_display(is_workspace: bool) -> NavigatorRow {
+ NavigatorRow {
+ target: NavigatorTarget::Workspace { ws_idx: 0 },
+ depth: if is_workspace { 0 } else { 1 },
+ label: String::new(),
+ meta: String::new(),
+ status: crate::detect::AgentState::Idle,
+ seen: true,
+ is_current: false,
+ is_workspace,
+ is_tab: false,
+ expanded: true,
+ search_text: String::new(),
+ matched: true,
+ }
+ }
+
+ #[test]
+ fn navigator_display_lines_separate_workspace_groups() {
+ let rows = vec![
+ navigator_row_for_display(true),
+ navigator_row_for_display(false),
+ navigator_row_for_display(true),
+ navigator_row_for_display(false),
+ ];
+ assert_eq!(
+ navigator_display_lines(&rows),
+ vec![
+ NavigatorDisplayLine::Row(0),
+ NavigatorDisplayLine::Row(1),
+ NavigatorDisplayLine::Spacer,
+ NavigatorDisplayLine::Row(2),
+ NavigatorDisplayLine::Row(3),
+ ]
+ );
+ }
+
+ #[test]
+ fn navigator_display_lines_have_no_leading_spacer() {
+ let rows = vec![
+ navigator_row_for_display(true),
+ navigator_row_for_display(false),
+ ];
+ assert_eq!(
+ navigator_display_lines(&rows),
+ vec![NavigatorDisplayLine::Row(0), NavigatorDisplayLine::Row(1)]
+ );
+ assert!(navigator_display_lines(&[]).is_empty());
+ }
+
+ #[test]
+ fn navigator_display_index_maps_row_to_line() {
+ let rows = vec![
+ navigator_row_for_display(true),
+ navigator_row_for_display(false),
+ navigator_row_for_display(true),
+ ];
+ let lines = navigator_display_lines(&rows);
+ assert_eq!(navigator_display_index_of_row(&lines, 2), Some(3));
+ assert_eq!(navigator_display_index_of_row(&lines, 9), None);
+ }
+
+ #[test]
+ fn navigator_first_row_skips_spacer_lines() {
+ let rows = vec![
+ navigator_row_for_display(true),
+ navigator_row_for_display(false),
+ navigator_row_for_display(true),
+ ];
+ let lines = navigator_display_lines(&rows);
+ // Line 2 is the spacer before the second workspace.
+ assert_eq!(navigator_first_row_at_or_after(&lines, 2), Some(2));
+ assert_eq!(navigator_first_row_at_or_after(&lines, 4), None);
+ }
+
#[test]
fn built_in_theme_names_resolve() {
for name in THEME_NAMES {