quinjet 0.0.47

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

impl App {
    #[doc = " `refresh` separates a live poll from merely arriving in the section: the"]
    #[doc = " latter reuses what is already loaded rather than spending a request."]
    pub(super) fn request_pull_request_checks(
        &mut self,
        refresh: bool,
        effects: &mut Vec<AppEffect>,
    ) {
        if self.pull_request_checks_loading || (!refresh && !self.pull_request_checks.is_empty()) {
            return;
        }
        let Some(pull_request) = self.pull_request.clone() else {
            return;
        };
        self.pull_request_checks_generation = self.pull_request_checks_generation.wrapping_add(1);
        self.pull_request_checks_loading = true;
        if self.pull_request_checks.is_empty() {
            self.invalidate_pull_request_content_rows();
        }
        effects.push(AppEffect::Git(Box::new(
            WorkerCommand::LoadPullRequestChecks {
                generation: self.pull_request_checks_generation,
                pull_request: Box::new(pull_request),
                refresh,
            },
        )));
    }

    #[doc = " The overview sidebar is the pull request itself, then status sections and"]
    #[doc = " their checks, so the cursor walks that composed list."]
    pub(super) fn move_check_cursor(&mut self, amount: isize) {
        let targets = self.check_list_targets();
        if targets.is_empty() {
            return;
        }
        let current = self.selected_check_list_target();
        let index = targets
            .iter()
            .position(|target| *target == current)
            .unwrap_or(0);
        let next = if amount < 0 {
            index.saturating_sub(amount.unsigned_abs())
        } else {
            index.saturating_add(count(amount)).min(targets.len() - 1)
        };
        if let Some(target) = targets.get(next).copied() {
            self.select_check_list_target(target);
        }
    }

    #[doc = " Every row in the overview sidebar shows a different document on the right,"]
    #[doc = " so a new selection always starts at the top of it."]
    pub(super) fn set_check_cursor(&mut self, cursor: Option<usize>) -> bool {
        let next = cursor.filter(|index| *index < self.pull_request_checks.len());
        if next.is_some() {
            self.selected_check_section = None;
        }
        if self.pull_request_check_cursor == next {
            return false;
        }
        self.pull_request_check_cursor = next;
        self.reset_view_content_position(View::PullRequests);
        self.invalidate_check_run_log();
        self.invalidate_pull_request_content_rows();
        true
    }

    #[doc = " The sidebar viewport for this frame. Wheel scrolling detaches the"]
    #[doc = " window from the selection so the list can be browsed without changing"]
    #[doc = " the preview; any selection movement reattaches it."]
    pub(crate) fn sidebar_viewport(&mut self, cursor: usize, height: usize, length: usize) {
        if self.sidebar_last_cursor != Some(cursor) {
            self.sidebar_last_cursor = Some(cursor);
            self.sidebar_free_scroll = false;
        }
        if height == 0 || length == 0 {
            self.sidebar_offset = 0;
            return;
        }
        if !self.sidebar_free_scroll {
            if cursor < self.sidebar_offset {
                self.sidebar_offset = cursor;
            } else if cursor >= self.sidebar_offset.saturating_add(height) {
                self.sidebar_offset = cursor.saturating_add(1).saturating_sub(height);
            }
        }
        self.sidebar_offset = self.sidebar_offset.min(length.saturating_sub(height));
    }

    pub(crate) const fn reset_sidebar_scroll(&mut self) {
        self.sidebar_offset = 0;
        self.sidebar_free_scroll = false;
        self.sidebar_last_cursor = None;
    }

    pub(crate) fn set_document(&mut self, document: DiffDocument) {
        self.document = document;
        self.invalidate_diff_rows();
    }

    pub(crate) const fn invalidate_diff_rows(&mut self) {
        self.document_layout_generation = self.document_layout_generation.wrapping_add(1);
        self.diff_rows_key = None;
    }

    pub(super) const fn invalidate_pull_request_content_rows(&mut self) {
        self.pull_request_content_generation = self.pull_request_content_generation.wrapping_add(1);
        self.pull_request_content_rows_key = None;
    }

    pub(super) fn invalidate_check_run_log(&mut self) {
        self.pull_request_check_log = None;
        self.pull_request_check_log_error = None;
        self.pull_request_check_log_target = None;
        self.pull_request_check_log_loading = false;
        self.pull_request_log_read_at = None;
        self.expanded_check_steps.clear();
        self.pull_request_step_cursor = 0;
        self.pull_request_check_log_generation =
            self.pull_request_check_log_generation.wrapping_add(1);
    }

    pub(crate) fn check_log_visible(&self) -> bool {
        self.view == View::PullRequests
            && self.pull_request_section == PullRequestSection::Overview
            && self.pull_request_check_cursor.is_some()
    }

    pub(super) fn check_step_numbers(&self) -> Vec<usize> {
        self.pull_request_check_log
            .as_ref()
            .map(|log| log.steps.iter().map(|step| step.number).collect())
            .unwrap_or_default()
    }

    pub(crate) fn check_step_expanded(&self, step: usize) -> bool {
        self.expanded_check_steps.contains(&step)
    }

    pub(super) fn toggle_check_step(&mut self, step: usize) {
        toggle_membership(&mut self.expanded_check_steps, step);
        self.reveal_check_step(step);
        self.invalidate_pull_request_content_rows();
    }

    pub(super) fn toggle_all_check_steps(&mut self) {
        let steps = self.check_step_numbers();
        if self.expanded_check_steps.is_empty() {
            self.expanded_check_steps.extend(steps);
        } else {
            self.expanded_check_steps.clear();
        }
        self.content_scroll = 0;
        self.invalidate_pull_request_content_rows();
    }

    #[doc = " Move between steps the way `[` and `]` move between diff hunks, so a long"]
    #[doc = " log can be walked without scrolling through it."]
    #[doc = " Move the step selection and ask the next draw to bring it into view."]
    pub(super) const fn reveal_check_step(&mut self, step: usize) {
        self.pull_request_step_cursor = step;
        self.pull_request_step_reveal = true;
    }

    pub(super) fn move_check_step_cursor(&mut self, amount: isize) -> bool {
        let steps = self.check_step_numbers();
        if steps.is_empty() {
            return false;
        }
        let current = steps
            .iter()
            .position(|step| *step == self.pull_request_step_cursor)
            .unwrap_or_default();
        let next = if amount < 0 {
            current.saturating_sub(amount.unsigned_abs())
        } else {
            current.saturating_add(count(amount)).min(steps.len() - 1)
        };
        let Some(step) = steps.get(next).copied() else {
            return false;
        };
        self.reveal_check_step(step);
        true
    }

    pub(super) fn select_pull_request_check(
        &mut self,
        cursor: Option<usize>,
        effects: &mut Vec<AppEffect>,
    ) {
        if self.set_check_cursor(cursor) {
            self.request_check_run_log(false, effects);
        }
    }

    #[doc = " Fetch the selected check's steps and log. A selection change starts from a"]
    #[doc = " clean slate; a live refresh of the same run updates in place so the reader"]
    #[doc = " keeps their scroll position while a job is still writing output. A log"]
    #[doc = " already held for the selected run is only re-read when `refresh` asks for"]
    #[doc = " it, so redrawing or re-entering the section costs nothing."]
    pub(super) fn request_check_run_log(&mut self, refresh: bool, effects: &mut Vec<AppEffect>) {
        let (Some(pull_request), Some(check)) = (
            self.pull_request.clone(),
            self.selected_pull_request_check().cloned(),
        ) else {
            self.pull_request_check_log = None;
            self.pull_request_check_log_error = None;
            self.pull_request_check_log_loading = false;
            self.pull_request_check_log_target = None;
            self.pull_request_check_log_generation =
                self.pull_request_check_log_generation.wrapping_add(1);
            return;
        };
        let target = check.identity();
        if self.pull_request_check_log_target.as_ref() == Some(&target) {
            if self.pull_request_check_log_loading {
                return;
            }
            let held = self.pull_request_check_log.is_some()
                || self.pull_request_check_log_error.is_some();
            if held && !refresh {
                return;
            }
        } else {
            self.pull_request_check_log = None;
            self.pull_request_check_log_error = None;
            self.expanded_check_steps.clear();
            self.pull_request_check_log_target = Some(target);
            self.pull_request_log_read_at = None;
        }
        self.pull_request_check_log_generation =
            self.pull_request_check_log_generation.wrapping_add(1);
        self.pull_request_check_log_loading = true;
        if self.pull_request_check_log.is_none() {
            self.invalidate_pull_request_content_rows();
        }
        effects.push(AppEffect::Git(Box::new(WorkerCommand::LoadCheckRunLog {
            generation: self.pull_request_check_log_generation,
            pull_request: Box::new(pull_request),
            check: Box::new(check),
        })));
    }

    #[doc = " Warm every finished run's log once per pull request. Selecting a check"]
    #[doc = " then costs a disk read rather than a round trip, which is the difference"]
    #[doc = " between the list being browsable and being a series of waits."]
    pub(super) fn request_check_log_prefetch(&mut self, effects: &mut Vec<AppEffect>) {
        let Some(pull_request) = self.pull_request.clone() else {
            return;
        };
        let settled: Vec<PullRequestCheck> = self
            .pull_request_checks
            .iter()
            .filter(|check| !check.status.is_running() && check.job_id().is_some())
            .filter(|check| {
                !self
                    .pull_request_prefetched_logs
                    .contains(&check.identity())
            })
            .take(32_usize.saturating_sub(self.pull_request_prefetched_logs.len()))
            .cloned()
            .collect();
        if settled.is_empty() {
            return;
        }
        self.pull_request_prefetched_logs
            .extend(settled.iter().map(PullRequestCheck::identity));
        effects.push(AppEffect::Git(Box::new(
            WorkerCommand::PrefetchCheckRunLogs {
                generation: 0,
                pull_request: Box::new(pull_request),
                checks: settled,
            },
        )));
    }

    pub(super) fn request_pull_request_conversation(
        &mut self,
        refresh: bool,
        effects: &mut Vec<AppEffect>,
    ) {
        if self.pull_request_conversation_loading {
            self.pull_request_conversation_refresh_again |= refresh;
            return;
        }
        if !refresh && !self.pull_request_conversation.entries.is_empty() {
            return;
        }
        let Some(pull_request) = self.pull_request.clone() else {
            return;
        };
        self.pull_request_conversation_generation =
            self.pull_request_conversation_generation.wrapping_add(1);
        self.pull_request_conversation_loading = true;
        if self.pull_request_conversation.entries.is_empty() {
            self.invalidate_pull_request_content_rows();
        }
        effects.push(AppEffect::Git(Box::new(
            WorkerCommand::LoadPullRequestConversation {
                generation: self.pull_request_conversation_generation,
                pull_request: Box::new(pull_request),
            },
        )));
    }
}