quinjet 0.0.52

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 = " Drop only the prepared diff. The section, cursors, checks and"]
    #[doc = " conversation stay exactly where the reader left them."]
    pub(super) fn reset_pull_request_diff_runtime(&mut self) {
        self.pull_request_workspace_generation = None;
        self.pull_request_documents.clear();
        self.pull_request_document_order.clear();
        self.pull_request_document_bytes = 0;
        self.pull_request_prefetched_paths.clear();
        self.pull_request_loading_path = None;
        self.pull_request_single_file = None;
        self.pull_request_prefetching = false;
        self.pull_request_prefetch_retrying = false;
    }

    pub(super) fn reset_pull_request_runtime(&mut self) {
        self.reset_pull_request_diff_runtime();
        self.reset_pull_request_review();
        self.pull_request_section = PullRequestSection::Overview;
        self.pull_request_file_view = PullRequestFileView::AllFiles;
        self.pull_request_files.clear();
        self.pull_request_tree.clear();
        self.pull_request_total_files = 0;
        self.pull_request_files_truncated = false;
        self.pull_request_file_cursor = 0;
        self.pull_request_tree_cursor = 0;
        self.collapsed_pull_request_directories.clear();
        self.pull_request_checks.clear();
        self.pull_request_check_cursor = None;
        self.selected_check_section = None;
        self.collapsed_check_sections.clear();
        self.pull_request_checks_loading = false;
        self.pull_request_checks_error = None;
        self.pull_request_checks_generation = self.pull_request_checks_generation.wrapping_add(1);
        self.pull_request_prefetched_logs.clear();
        self.pull_request_conversation = PullRequestConversation::default();
        self.pull_request_conversation_loading = false;
        self.pull_request_conversation_refresh_again = false;
        self.pull_request_conversation_error = None;
        self.pull_request_conversation_generation =
            self.pull_request_conversation_generation.wrapping_add(1);
        self.pull_request_check_log = None;
        self.pull_request_check_log_loading = false;
        self.pull_request_check_log_error = None;
        self.pull_request_check_log_target = None;
        self.pull_request_check_log_generation =
            self.pull_request_check_log_generation.wrapping_add(1);
        self.expanded_check_steps.clear();
        self.pull_request_step_cursor = 0;
        self.pull_request_content_rows.clear();
        self.pull_request_content_rows_key = None;
        self.pull_request_content_width = 0;
        self.pull_request_content_links.clear();
        self.pull_request_content_generation = self.pull_request_content_generation.wrapping_add(1);
        self.pull_request_checks_read_at = None;
        self.pull_request_detail_read_at = None;
        self.pull_request_log_read_at = None;
        self.reset_view_sidebar_scroll(View::PullRequests);
    }

    pub(super) fn apply_pull_request_index(&mut self, index: PullRequestDiffIndex) {
        self.pull_request_file_view = PullRequestFileView::AllFiles;
        self.pull_request_documents.clear();
        self.pull_request_loading_path = None;
        self.pull_request_single_file = None;
        self.pull_request_prefetching = false;
        self.pull_request_prefetch_retrying = false;
        self.pull_request_files = index.files;
        self.pull_request_total_files = index.total_files;
        self.pull_request_files_truncated = index.truncated;
        self.pull_request_file_cursor = self
            .pull_request_file_cursor
            .min(self.pull_request_files.len().saturating_sub(1));
        self.sync_pull_request_tree_cursor_to_file();
        let paths = self
            .pull_request_files
            .iter()
            .map(|file| file.path.clone())
            .collect::<Vec<_>>();
        self.reset_preview_file_folds(&paths);
        self.selected_preview_file = self
            .selected_pull_request_file()
            .map(|file| file.path.clone());
        self.preview_file_cursor = self.pull_request_file_cursor;
        self.rebuild_pull_request_all_files_document();
    }

    pub(super) fn rebuild_pull_request_all_files_document(&mut self) {
        let Some(pull_request) = self.pull_request.as_ref() else {
            return;
        };
        let title = format!(
            "PR #{} — All Files · {} changed",
            pull_request.number, self.pull_request_total_files
        );
        if self.pull_request_files.is_empty() {
            let mut document = DiffDocument::empty(
                title,
                if self.pull_request_files_truncated {
                    "The changed-file index was truncated before any paths were read"
                } else {
                    "This pull request has no changed files"
                },
            );
            document.pull_request_details = Some(pull_request_details(pull_request));
            self.set_document(document);
            return;
        }
        let index = DiffIndex {
            title,
            files: self
                .pull_request_files
                .iter()
                .map(|file| crate::git::diff::DiffFileIndexEntry {
                    path: file.path.clone(),
                    old_path: file.old_path.clone(),
                    status: pull_request_file_status_label(file.status).to_owned(),
                    counts: file.counts,
                })
                .collect(),
            truncated: self.pull_request_files_truncated,
            commit_details: None,
        };
        let paths = index
            .files
            .iter()
            .map(|file| file.path.clone())
            .collect::<Vec<_>>();
        let visible = self.visible_preview_paths(&paths);
        let mut document = index
            .document_with_visibility(&self.pull_request_documents, |path| visible.contains(path));
        document.pull_request_details = Some(pull_request_details(pull_request));
        self.set_document(document);
    }

    pub(super) fn cache_current_pull_request_single_document(&mut self) {
        let Some(path) = self.pull_request_single_file.take() else {
            return;
        };
        if self.pull_request_documents.contains_key(&path) {
            return;
        }
        let mut document = std::mem::take(&mut self.document);
        document
            .lines
            .retain(|line| line.kind != DiffLineKind::Review);
        self.invalidate_diff_rows();
        self.cache_pull_request_document(path, document);
    }

    pub(super) fn cache_pull_request_document(&mut self, path: PathBuf, document: DiffDocument) {
        if let Some(previous) = self.pull_request_documents.remove(&path) {
            self.pull_request_document_bytes = self
                .pull_request_document_bytes
                .saturating_sub(diff_document_size(&previous));
            self.pull_request_document_order
                .retain(|candidate| candidate != &path);
        }
        self.pull_request_document_bytes = self
            .pull_request_document_bytes
            .saturating_add(diff_document_size(&document));
        let _ = self.pull_request_prefetched_paths.insert(path.clone());
        self.pull_request_document_order.push_back(path.clone());
        drop(self.pull_request_documents.insert(path, document));
        self.prune_pull_request_documents(MAX_PULL_REQUEST_DOCUMENT_BYTES);
    }

    pub(super) fn prune_pull_request_documents(&mut self, maximum_bytes: usize) {
        while self.pull_request_document_bytes > maximum_bytes
            && self.pull_request_documents.len() > 1
        {
            let Some(expired) = self.pull_request_document_order.pop_front() else {
                break;
            };
            if let Some(document) = self.pull_request_documents.remove(&expired) {
                self.pull_request_document_bytes = self
                    .pull_request_document_bytes
                    .saturating_sub(diff_document_size(&document));
            }
        }
    }

    pub(super) fn take_pull_request_document(&mut self, path: &Path) -> Option<DiffDocument> {
        let document = self.pull_request_documents.remove(path)?;
        self.pull_request_document_bytes = self
            .pull_request_document_bytes
            .saturating_sub(diff_document_size(&document));
        self.pull_request_document_order
            .retain(|candidate| candidate != path);
        Some(document)
    }

    pub(super) fn show_pull_request_all_files(&mut self) {
        self.cache_current_pull_request_single_document();
        self.pull_request_file_view = PullRequestFileView::AllFiles;
        self.pull_request_loading_path = None;
        self.document_loading = false;
        self.selected_preview_file = self
            .selected_pull_request_file()
            .map(|file| file.path.clone());
        self.preview_file_cursor = self.pull_request_file_cursor;
        self.rebuild_pull_request_all_files_document();
    }

    pub(super) fn select_pull_request_section(
        &mut self,
        section: PullRequestSection,
        effects: &mut Vec<AppEffect>,
    ) {
        if section == PullRequestSection::Files {
            if self.pull_request_section == PullRequestSection::Files
                && self.pull_request_file_view == PullRequestFileView::AllFiles
            {
                return;
            }
            self.invalidate_preview();
            self.pull_request_section = PullRequestSection::Files;
            self.reset_sidebar_scroll();
            self.content_scroll = 0;
            self.horizontal_scroll = 0;
            self.show_pull_request_all_files();
            self.request_pull_request_review(false, effects);
            self.request_preview(effects);
            return;
        }
        if self.pull_request_section == section {
            return;
        }
        self.invalidate_preview();
        self.pull_request_section = section;
        self.reset_sidebar_scroll();
        self.content_scroll = 0;
        self.horizontal_scroll = 0;
        self.request_pull_request_checks(false, effects);
        self.request_pull_request_conversation(false, effects);
        self.request_pull_request_review(false, effects);
        self.request_check_run_log(false, effects);
    }

    pub(super) fn request_pull_request_diff_file(
        &mut self,
        path: PathBuf,
        show_loading: bool,
        effects: &mut Vec<AppEffect>,
    ) {
        if !self.pull_request_files.iter().any(|file| file.path == path) {
            return;
        }
        if self.pull_request_file_view == PullRequestFileView::SingleFile {
            if self.pull_request_documents.contains_key(&path) {
                self.cache_current_pull_request_single_document();
            }
            if let Some(document) = self.take_pull_request_document(&path) {
                self.document_loading = false;
                self.set_document(document);
                self.pull_request_single_file = Some(path);
                self.selected_preview_file = None;
                self.preview_file_cursor = 0;
                self.decorate_pull_request_review();
                return;
            }
        } else if self.pull_request_documents.contains_key(&path) {
            self.document_loading = false;
            self.rebuild_pull_request_all_files_document();
            return;
        }
        let Some(workspace_generation) = self.pull_request_workspace_generation else {
            return;
        };
        self.diff_generation = self.diff_generation.wrapping_add(1);
        self.pull_request_loading_path = Some(path.clone());
        self.document_loading = show_loading;
        self.pull_request_progress = None;
        effects.push(AppEffect::Git(Box::new(
            WorkerCommand::LoadPullRequestFile {
                generation: self.diff_generation,
                workspace_generation,
                path,
            },
        )));
    }

    #[doc = " A path still needs its patch unless it is already cached, already in"]
    #[doc = " flight, or currently occupying the single-file document."]
    pub(super) fn pull_request_file_needs_patch(&self, path: &Path) -> bool {
        !self.pull_request_documents.contains_key(path)
            && self.pull_request_loading_path.as_deref() != Some(path)
            && self.pull_request_single_file.as_deref() != Some(path)
    }

    #[doc = " A finished patch knows its real totals, so a file whose counts GitHub"]
    #[doc = " could not report fills its header in as soon as its document arrives."]
    pub(super) fn backfill_pull_request_counts(
        &mut self,
        path: &Path,
        document: &DiffDocument,
    ) -> bool {
        if document.truncated {
            return false;
        }
        let Some(file) = self
            .pull_request_files
            .iter_mut()
            .find(|file| file.path == path && file.counts.is_none())
        else {
            return false;
        };
        let mut additions = 0_usize;
        let mut deletions = 0_usize;
        for line in &document.lines {
            match line.kind {
                DiffLineKind::Added => additions = additions.saturating_add(1),
                DiffLineKind::Removed => deletions = deletions.saturating_add(1),
                _ => {}
            }
        }
        file.counts = Some(DiffLineCounts {
            additions,
            deletions,
            binary: false,
        });
        true
    }

    #[doc = " Where background fill should start: the first file visible in the"]
    #[doc = " Files tree, so patches land where the reader is looking and then wrap"]
    #[doc = " around the rest of the index in order."]
    pub(super) fn prefetch_anchor_index(&self) -> usize {
        if self.view != View::PullRequests || self.pull_request_section != PullRequestSection::Files
        {
            return 0;
        }
        self.pull_request_tree
            .iter()
            .skip(self.sidebar_offset)
            .find_map(|entry| match entry {
                PullRequestTreeEntry::File { index, .. } => Some(*index),
                PullRequestTreeEntry::Directory { .. } => None,
            })
            .unwrap_or(0)
    }

    #[doc = " Walk the index in batches until every file has a patch. Each batch is one"]
    #[doc = " Git invocation and lands as soon as it is parsed, so the diff fills in"]
    #[doc = " progressively instead of a file at a time on demand."]
    pub(super) fn request_pull_request_prefetch(&mut self, effects: &mut Vec<AppEffect>) {
        if self.pull_request_prefetching {
            return;
        }
        let Some(workspace_generation) = self.pull_request_workspace_generation else {
            return;
        };
        if self.pull_request_prefetched_paths.len() >= MAX_PREFETCHED_PULL_REQUEST_FILES {
            return;
        }
        let remaining = MAX_PREFETCHED_PULL_REQUEST_FILES
            .saturating_sub(self.pull_request_prefetched_paths.len());
        let limit = PULL_REQUEST_PREFETCH_BATCH.min(remaining);
        let anchor = self
            .prefetch_anchor_index()
            .min(self.pull_request_files.len());
        let (before, from_anchor) = self.pull_request_files.split_at(anchor);
        let mut batch_bytes = 0_usize;
        let mut paths: Vec<PathBuf> = Vec::new();
        for file in from_anchor.iter().chain(before.iter()) {
            if paths.len() >= limit {
                break;
            }
            if !self.pull_request_file_needs_patch(&file.path)
                || self.pull_request_prefetched_paths.contains(&file.path)
            {
                continue;
            }
            let estimate = estimated_patch_bytes(file.counts);
            if !paths.is_empty()
                && batch_bytes.saturating_add(estimate) > PULL_REQUEST_PREFETCH_BYTE_BUDGET
            {
                break;
            }
            batch_bytes = batch_bytes.saturating_add(estimate);
            paths.push(file.path.clone());
        }
        if paths.is_empty() {
            return;
        }
        self.pull_request_prefetching = true;
        effects.push(AppEffect::Git(Box::new(
            WorkerCommand::LoadPullRequestFileBatch {
                workspace_generation,
                paths,
            },
        )));
    }
}