diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 21:18:40 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 21:18:40 +0300 |
| commit | 8cf0e9b35b00f9dd91270fcd3931015d61ffd8a0 (patch) | |
| tree | 2b76b158344fdbe0de17c6abe98564a262266d2f /src/app/state.rs | |
| parent | 859185b491c5a80e888ba8f9dd213d2c35baa034 (diff) | |
fix: confirm implicit worktree group close
refs #369
Diffstat (limited to 'src/app/state.rs')
| -rw-r--r-- | src/app/state.rs | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/src/app/state.rs b/src/app/state.rs index a9c1b8a3..9466fcce 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -593,6 +593,48 @@ pub struct WorktreeOpenEntry { pub already_open_ws_idx: Option<usize>, } +impl WorktreeOpenEntry { + pub(crate) fn display_name(&self) -> String { + self.branch.clone().unwrap_or_else(|| { + self.path + .file_name() + .and_then(|name| name.to_str()) + .map(str::to_owned) + .unwrap_or_else(|| self.path.display().to_string()) + }) + } + + pub(crate) fn status_label(&self) -> &'static str { + if self.already_open_ws_idx.is_some() { + "open" + } else if self.branch.is_some() { + "" + } else if self.is_linked_worktree { + "detached" + } else { + "root" + } + } + + fn search_text(&self) -> String { + format!( + "{} {} {} {}", + self.display_name(), + self.path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(), + self.path.display(), + self.status_label() + ) + .to_lowercase() + } + + fn matches_query(&self, query: &str) -> bool { + text_matches_query(query, &self.search_text()) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorktreeOpenState { pub source_workspace_id: String, @@ -603,9 +645,65 @@ pub struct WorktreeOpenState { pub repo_name: String, pub entries: Vec<WorktreeOpenEntry>, pub selected: usize, + pub query: String, + pub search_focused: bool, pub error: Option<String>, } +impl WorktreeOpenState { + pub(crate) fn filtered_indices(&self) -> Vec<usize> { + let query = self.query.trim(); + self.entries + .iter() + .enumerate() + .filter_map(|(idx, entry)| { + (query.is_empty() || entry.matches_query(query)).then_some(idx) + }) + .collect() + } + + pub(crate) fn selected_entry_index(&self) -> Option<usize> { + let indices = self.filtered_indices(); + if indices.contains(&self.selected) { + Some(self.selected) + } else { + indices.first().copied() + } + } + + pub(crate) fn normalize_selection(&mut self) { + if let Some(selected) = self.selected_entry_index() { + self.selected = selected; + } + } + + pub(crate) fn select_previous_filtered(&mut self) { + let indices = self.filtered_indices(); + let Some(current) = self.selected_entry_index() else { + return; + }; + let pos = indices.iter().position(|idx| *idx == current).unwrap_or(0); + self.selected = indices[pos.saturating_sub(1)]; + } + + pub(crate) fn select_next_filtered(&mut self) { + let indices = self.filtered_indices(); + let Some(current) = self.selected_entry_index() else { + return; + }; + let pos = indices.iter().position(|idx| *idx == current).unwrap_or(0); + self.selected = indices[(pos + 1).min(indices.len().saturating_sub(1))]; + } +} + +pub(crate) fn text_matches_query(query: &str, text: &str) -> bool { + let haystack = text.to_lowercase(); + query + .to_lowercase() + .split_whitespace() + .all(|needle| haystack.contains(needle)) +} + /// Computed view geometry — derived from AppState + terminal size. /// Updated before each render, consumed by render and mouse handling. #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
