use super::App;
use crate::error::Result;
impl App {
pub(super) fn navigate_up(&mut self) {
if self.ui.tree_index == 0 {
return;
}
let mut target = self.ui.tree_index - 1;
while !self.is_navigable(target) && target > 0 {
target -= 1;
}
if !self.is_navigable(target) {
return; }
self.ui.tree_index = target;
self.update_selected_detail();
}
pub(super) fn navigate_down(&mut self) {
let max = self.cache.tree_items.len().saturating_sub(1);
if self.ui.tree_index >= max {
return;
}
let mut target = self.ui.tree_index + 1;
while !self.is_navigable(target) && target < max {
target += 1;
}
if !self.is_navigable(target) {
return; }
self.ui.tree_index = target;
self.update_selected_detail();
}
fn is_navigable(&self, index: usize) -> bool {
self.cache
.tree_items
.get(index)
.map(|i| i.node.is_navigable())
.unwrap_or(false)
}
pub(super) fn jump_to_next_action(&mut self, reverse: bool) {
if self.cache.tree_items.is_empty() {
return;
}
let action_indices: Vec<usize> = self
.cache
.tree_items
.iter()
.enumerate()
.filter(|(_, item)| item.action_symbol.is_some())
.map(|(i, _)| i)
.collect();
if action_indices.is_empty() {
return;
}
let current = self.ui.tree_index;
let next_index = if reverse {
action_indices
.iter()
.rev()
.find(|&&i| i < current)
.or_else(|| action_indices.last())
.copied()
} else {
action_indices
.iter()
.find(|&&i| i > current)
.or_else(|| action_indices.first())
.copied()
};
if let Some(idx) = next_index {
let target_id = self.cache.tree_items[idx].node.id().to_string();
self.expand_to_reveal(&target_id);
self.rebuild_tree();
for (i, item) in self.cache.tree_items.iter().enumerate() {
if item.node.id() == target_id {
self.ui.tree_index = i;
break;
}
}
self.update_selected_detail();
}
}
fn expand_to_reveal(&mut self, target_id: &str) {
if let Some(solution) = self.data.solutions.iter().find(|s| s.id == target_id) {
self.ui.expanded_nodes.insert(solution.problem_id.clone());
if let Some(problem) = self
.data
.problems
.iter()
.find(|p| p.id == solution.problem_id)
{
if let Some(milestone_id) = &problem.milestone_id {
self.ui.expanded_nodes.insert(milestone_id.clone());
} else {
self.ui.expanded_nodes.insert("backlog".to_string());
}
}
}
if let Some(problem) = self.data.problems.iter().find(|p| p.id == target_id) {
if let Some(milestone_id) = &problem.milestone_id {
self.ui.expanded_nodes.insert(milestone_id.clone());
} else {
self.ui.expanded_nodes.insert("backlog".to_string());
}
}
if let Some(critique) = self.data.critiques.iter().find(|c| c.id == target_id) {
self.ui.expanded_nodes.insert(critique.solution_id.clone());
if let Some(solution) = self
.data
.solutions
.iter()
.find(|s| s.id == critique.solution_id)
{
self.ui.expanded_nodes.insert(solution.problem_id.clone());
if let Some(problem) = self
.data
.problems
.iter()
.find(|p| p.id == solution.problem_id)
{
if let Some(milestone_id) = &problem.milestone_id {
self.ui.expanded_nodes.insert(milestone_id.clone());
} else {
self.ui.expanded_nodes.insert("backlog".to_string());
}
}
}
}
}
pub(super) fn collapse_or_parent(&mut self) {
if let Some(item) = self.cache.tree_items.get(self.ui.tree_index) {
let node_id = item.node.id().to_string();
if item.node.is_expanded() {
self.ui.expanded_nodes.remove(&node_id);
self.rebuild_tree();
} else if item.depth > 0 {
for i in (0..self.ui.tree_index).rev() {
if self.cache.tree_items[i].depth < item.depth {
self.ui.tree_index = i;
break;
}
}
self.update_selected_detail();
}
}
}
pub(super) fn expand_or_child(&mut self) {
if let Some(item) = self.cache.tree_items.get(self.ui.tree_index) {
if !item.has_children {
return;
}
let node_id = item.node.id().to_string();
if item.node.is_expanded() {
if self.ui.tree_index + 1 < self.cache.tree_items.len() {
self.ui.tree_index += 1;
}
} else {
self.ui.expanded_nodes.insert(node_id);
self.rebuild_tree();
}
}
}
pub(super) fn scroll_detail_down(&mut self) {
self.ui.detail_scroll = self.ui.detail_scroll.saturating_add(1);
}
pub(super) fn scroll_detail_up(&mut self) {
self.ui.detail_scroll = self.ui.detail_scroll.saturating_sub(1);
}
pub(super) fn page_detail_up(&mut self) {
self.ui.detail_scroll = self.ui.detail_scroll.saturating_sub(10);
}
pub(super) fn page_detail_down(&mut self) {
self.ui.detail_scroll = self.ui.detail_scroll.saturating_add(10);
}
pub(super) fn detail_scroll_to_top(&mut self) {
self.ui.detail_scroll = 0;
}
pub(super) fn detail_scroll_to_bottom(&mut self) {
self.ui.detail_scroll = u16::MAX;
}
pub(super) fn toggle_selection(&mut self) {
if let Some(item) = self.cache.tree_items.get(self.ui.tree_index) {
if item.node.is_selectable() {
let id = item.node.id().to_string();
if !self.ui.selected_ids.remove(&id) {
self.ui.selected_ids.insert(id);
}
}
}
}
pub(super) fn select_all_visible(&mut self) {
let visible_ids: Vec<String> = self
.cache
.tree_items
.iter()
.filter(|item| item.node.is_selectable())
.map(|item| item.node.id().to_string())
.collect();
let all_selected = visible_ids
.iter()
.all(|id| self.ui.selected_ids.contains(id));
if all_selected {
self.ui.selected_ids.clear();
} else {
self.ui.selected_ids.extend(visible_ids);
}
}
pub(super) fn clear_selection(&mut self) {
self.ui.selected_ids.clear();
}
pub(super) fn toggle_related_panel(&mut self) {
self.ui.show_related = !self.ui.show_related;
}
pub(super) fn toggle_filter(&mut self) {
self.ui.filter_actions_only = !self.ui.filter_actions_only;
let mode = if self.ui.filter_actions_only {
"Actions only"
} else {
"Full tree"
};
self.show_flash(mode);
}
pub(super) fn start_search(&mut self) {
use super::InputAction;
use super::InputMode;
let buffer = self.ui.search_filter.clone().unwrap_or_default();
let cursor_pos = buffer.chars().count();
self.ui.input_mode = InputMode::Input {
prompt: "/".to_string(),
buffer,
action: InputAction::Search,
cursor_pos,
};
}
pub(super) fn toggle_help(&mut self) {
use super::InputMode;
self.ui.input_mode = match &self.ui.input_mode {
InputMode::Help => InputMode::Normal,
_ => InputMode::Help,
};
}
pub(super) fn goto_change(&mut self) -> Result<()> {
use super::super::tree::TreeNode;
let solution_id = if let Some(item) = self.cache.tree_items.get(self.ui.tree_index) {
match &item.node {
TreeNode::Solution { id, .. } => id.clone(),
_ => return Ok(()),
}
} else {
return Ok(());
};
let solution = match self.data.solutions.iter().find(|s| s.id == solution_id) {
Some(s) => s,
None => {
self.show_flash("Solution not found");
return Ok(());
}
};
if let Some(change_id) = solution.change_ids.last() {
match self.store.jj_client.edit(change_id) {
Ok(_) => self.show_flash(&format!("Switched to {}", change_id)),
Err(e) => self.show_flash(&format!("Error: {}", e)),
}
} else {
self.show_flash("No changes attached");
}
Ok(())
}
pub fn rebuild_tree(&mut self) {
let tree_ctx = super::super::tree::TreeBuildContext {
solutions: &self.data.solutions,
critiques: &self.data.critiques,
expanded_nodes: &self.ui.expanded_nodes,
personal_orderings: &self.ui.personal_orderings,
};
self.cache.tree_items = super::super::tree::build_flat_tree_ranked(
&self.data.milestones,
&self.data.problems,
&tree_ctx,
self.ui.show_personal_ordering,
&self.ui.tier_drill,
);
super::super::annotate_tree_with_actions(
&mut self.cache.tree_items,
&self.cache.next_actions,
);
if self.ui.search_filter.is_some() {
self.apply_search_filter_to_tree();
}
}
pub(super) fn apply_search_filter(&mut self) {
self.rebuild_tree();
let max_index = self.cache.tree_items.len().saturating_sub(1);
if self.ui.tree_index > max_index {
self.ui.tree_index = max_index;
}
self.update_selected_detail();
}
fn apply_search_filter_to_tree(&mut self) {
if let Some(ref query) = self.ui.search_filter {
let query_lower = query.to_lowercase();
self.cache.tree_items.retain(|item| {
let title = item.node.title().to_lowercase();
let id = item.node.id().to_lowercase();
title.contains(&query_lower) || id.contains(&query_lower)
});
}
}
pub fn context_hints(&self) -> String {
use super::super::tree::TreeNode;
if !self.ui.selected_ids.is_empty() {
return format!(
"{} selected: [s]olve [d]ecline [A]ssign [m]ove [x]delete [Esc]clear",
self.ui.selected_ids.len()
);
}
if let Some(item) = self.cache.tree_items.get(self.ui.tree_index) {
match &item.node {
TreeNode::ProjectRoot { .. } => "[n]ew milestone".to_string(),
TreeNode::Milestone { id, .. } => {
format!(
"{}: [n]ew problem [e]dit [E]ditor [s] complete [o] activate [D] cancel [A]ssign [x] delete",
id
)
}
TreeNode::Backlog { .. } => "[n]ew problem".to_string(),
TreeNode::Problem { id, .. } => {
format!(
"{}: [n]ew solution [s]olve [d]issolve [o] reopen [A]ssign [m]ove [e]dit [t]ags [E]ditor [x] delete",
id
)
}
TreeNode::Solution { id, .. } => {
format!(
"{}: [n]ew critique [u] submit [a]pprove [d] withdraw [A]ssign [g]o to change [e]dit [t]ags [E]ditor [x] delete",
id
)
}
TreeNode::Critique { id, .. } => {
format!(
"{}: [a]ddress [d]ismiss [v]alidate [e]dit [E]ditor [x] delete",
id
)
}
TreeNode::TierSeparator { .. } => String::new(),
}
} else {
"No selection".to_string()
}
}
pub fn update_selected_detail(&mut self) {
use super::super::tree::TreeNode;
if let Some(item) = self.cache.tree_items.get(self.ui.tree_index) {
self.cache.selected_detail = match &item.node {
TreeNode::Milestone { id, .. } => self
.data
.milestones
.iter()
.find(|m| m.id == *id)
.cloned()
.map(super::super::DetailContent::Milestone)
.unwrap_or(super::super::DetailContent::None),
TreeNode::ProjectRoot { .. }
| TreeNode::Backlog { .. }
| TreeNode::TierSeparator { .. } => super::super::DetailContent::None,
TreeNode::Problem { id, .. } => {
if let Some(problem) = self.data.problems.iter().find(|p| p.id == *id).cloned()
{
let rank_info = self.build_problem_rank_info(&problem);
super::super::DetailContent::Problem(problem, rank_info)
} else {
super::super::DetailContent::None
}
}
TreeNode::Solution { id, .. } => self
.data
.solutions
.iter()
.find(|s| s.id == *id)
.cloned()
.map(super::super::DetailContent::Solution)
.unwrap_or(super::super::DetailContent::None),
TreeNode::Critique { id, .. } => self
.data
.critiques
.iter()
.find(|c| c.id == *id)
.cloned()
.map(super::super::DetailContent::Critique)
.unwrap_or(super::super::DetailContent::None),
};
}
self.ui.detail_scroll = 0; self.load_related_for_selected(); }
fn build_problem_rank_info(
&self,
problem: &crate::models::Problem,
) -> Option<super::super::ProblemRankInfo> {
let milestone_id = problem.milestone_id.as_ref()?;
let (rank_pos, _voter_count) = if self.ui.show_personal_ordering {
let ordering = self.ui.personal_orderings.get(milestone_id)?;
let pos = ordering.order.iter().position(|id| id == &problem.id)?;
(pos + 1, 1usize)
} else {
let milestone_rankings = self.data.rankings.get(milestone_id)?;
let (pos, voter_str) = milestone_rankings.get(&problem.id)?;
(*pos, voter_str.parse().unwrap_or(0))
};
let (my_votes, budget_used, budget_total) =
if let Some(ordering) = self.ui.personal_orderings.get(milestone_id) {
let v = ordering.votes.get(&problem.id).copied().unwrap_or(0);
let used = crate::ranking::borda::vote_cost(v);
let problem_count = ordering.order.len();
let total = crate::ranking::borda::qv_budget(problem_count);
(v, used, total)
} else {
(0, 0, 0)
};
Some(super::super::ProblemRankInfo {
rank: Some(rank_pos),
votes: my_votes,
budget_used,
budget_total,
})
}
pub(super) fn get_selected_entity(
&self,
) -> Option<(String, super::super::next_actions::EntityType)> {
use super::super::tree::TreeNode;
self.cache
.tree_items
.get(self.ui.tree_index)
.and_then(|item| match &item.node {
TreeNode::Problem { id, .. } => {
Some((id.clone(), super::super::next_actions::EntityType::Problem))
}
TreeNode::Solution { id, .. } => {
Some((id.clone(), super::super::next_actions::EntityType::Solution))
}
TreeNode::Critique { id, .. } => {
Some((id.clone(), super::super::next_actions::EntityType::Critique))
}
TreeNode::Milestone { id, .. } => Some((
id.clone(),
super::super::next_actions::EntityType::Milestone,
)),
_ => None,
})
}
pub(super) fn action_targets(&self) -> Vec<(String, super::super::next_actions::EntityType)> {
use super::super::tree::TreeNode;
if !self.ui.selected_ids.is_empty() {
self.cache
.tree_items
.iter()
.filter(|item| self.ui.selected_ids.contains(item.node.id()))
.filter_map(|item| match &item.node {
TreeNode::Problem { id, .. } => {
Some((id.clone(), super::super::next_actions::EntityType::Problem))
}
TreeNode::Solution { id, .. } => {
Some((id.clone(), super::super::next_actions::EntityType::Solution))
}
TreeNode::Critique { id, .. } => {
Some((id.clone(), super::super::next_actions::EntityType::Critique))
}
TreeNode::Milestone { id, .. } => Some((
id.clone(),
super::super::next_actions::EntityType::Milestone,
)),
_ => None,
})
.collect()
} else {
self.get_selected_entity().into_iter().collect()
}
}
pub(super) fn get_selected_entity_info(&self) -> Option<(String, String)> {
use super::super::tree::TreeNode;
self.cache
.tree_items
.get(self.ui.tree_index)
.and_then(|item| match &item.node {
TreeNode::Problem { id, .. } => Some(("problem".to_string(), id.clone())),
TreeNode::Solution { id, .. } => Some(("solution".to_string(), id.clone())),
TreeNode::Critique { id, .. } => Some(("critique".to_string(), id.clone())),
TreeNode::Milestone { id, .. } => Some(("milestone".to_string(), id.clone())),
TreeNode::ProjectRoot { .. }
| TreeNode::Backlog { .. }
| TreeNode::TierSeparator { .. } => None,
})
}
}