use tokio::sync::mpsc;
use crate::github::{ChangedFile, PullRequest};
use crate::syntax::ParserPool;
use super::types::*;
use super::{App, DataState};
impl App {
pub(crate) fn calc_diff_line_count(files: &[ChangedFile], selected: usize) -> usize {
files
.get(selected)
.and_then(|f| f.patch.as_ref())
.map(|p| p.lines().count())
.unwrap_or(0)
}
pub fn files(&self) -> &[ChangedFile] {
match &self.data_state {
DataState::Loaded { files, .. } => files,
_ => &[],
}
}
pub fn pr(&self) -> Option<&PullRequest> {
match &self.data_state {
DataState::Loaded { pr, .. } => Some(pr.as_ref()),
_ => None,
}
}
pub(crate) fn update_diff_line_count(&mut self) {
let count = Self::calc_diff_line_count(self.files(), self.selected_file);
self.diff_scroll.set_line_count(count);
}
pub(crate) fn sync_diff_to_selected_file(&mut self) {
self.diff_scroll.reset();
self.multiline_selection = None;
self.cmt.comment_panel_open = false;
self.cmt.comment_panel_scroll = 0;
self.clear_pending_keys();
self.symbol_popup = None;
self.update_diff_line_count();
if self.cmt.review_comments.is_none() {
self.load_review_comments();
}
self.update_file_comment_positions();
self.request_lazy_diff();
self.ensure_diff_cache();
}
pub fn ensure_diff_cache(&mut self) {
let file_index = self.selected_file;
let markdown_rich = self.markdown_rich;
let is_md = self
.files()
.get(file_index)
.map(|f| crate::language::is_markdown_ext_from_filename(&f.filename))
.unwrap_or(false);
if self.diff_store.current_key() == Some(&file_index) {
if let Some(ref cache) = self.diff_store.current {
let md_ok = !is_md || cache.markdown_rich == markdown_rich;
if md_ok {
let Some(file) = self.files().get(file_index) else {
self.diff_store.clear_current();
return;
};
let Some(ref patch) = file.patch else {
self.diff_store.clear_current();
return;
};
let current_hash = hash_string(patch);
if cache.patch_hash == current_hash {
return; }
}
}
}
let Some(file) = self.files().get(file_index) else {
self.diff_store.clear_current();
return;
};
let Some(patch) = file.patch.clone() else {
self.diff_store.clear_current();
return;
};
let filename = file.filename.clone();
let current_hash = hash_string(&patch);
if self.diff_store.try_restore(&file_index, Some(current_hash)) {
let md_ok = !is_md
|| self
.diff_store
.current
.as_ref()
.map(|c| c.markdown_rich == markdown_rich)
.unwrap_or(false);
if md_ok {
return; }
self.diff_store.clear_current();
}
let tab_width = self.config.diff.tab_width;
let mut plain_cache = crate::ui::diff_view::build_plain_diff_cache(&patch, tab_width);
plain_cache.file_index = file_index;
self.diff_store.set_current(file_index, plain_cache);
let (tx, rx) = mpsc::channel(1);
self.diff_store.set_highlight_rx(rx);
let theme = self.config.diff.theme.clone();
tokio::task::spawn_blocking(move || {
let mut parser_pool = ParserPool::new();
let mut cache = crate::ui::diff_view::build_diff_cache(
&patch,
&filename,
&theme,
&mut parser_pool,
markdown_rich,
tab_width,
);
cache.file_index = file_index;
let _ = tx.try_send((file_index, cache));
});
}
pub(crate) fn open_pr_description(&mut self) {
self.previous_state = self.state;
self.state = AppState::PrDescription;
self.pr_description_scroll_offset = 0;
self.rebuild_pr_description_cache();
}
pub(crate) fn rebuild_pr_description_cache(&mut self) {
let body = self
.pr()
.and_then(|pr| pr.body.as_deref())
.unwrap_or("")
.to_string();
let body_hash = hash_string(&body);
let markdown_rich = self.markdown_rich;
if let Some(ref cache) = self.pr_description_cache {
if cache.patch_hash == body_hash && cache.markdown_rich == markdown_rich {
return; }
}
if body.is_empty() {
self.pr_description_cache = None;
return;
}
let patch = build_pr_description_patch(&body);
let tab_width = self.config.diff.tab_width;
let theme = self.config.diff.theme.clone();
let mut parser_pool = ParserPool::new();
let mut cache = crate::ui::diff_view::build_diff_cache(
&patch,
"description.md",
&theme,
&mut parser_pool,
markdown_rich,
tab_width,
);
cache.file_index = usize::MAX; cache.patch_hash = body_hash;
self.pr_description_cache = Some(cache);
}
}
pub fn build_pr_description_patch(body: &str) -> String {
let body = body.replace("\r\n", "\n");
let line_count = body.lines().count().max(1);
let mut patch = format!("@@ -1,{} +1,{} @@\n", line_count, line_count);
for line in body.lines() {
patch.push(' ');
patch.push_str(line);
patch.push('\n');
}
patch
}
#[cfg(test)]
mod patch_tests {
use super::*;
#[test]
fn test_basic_body() {
let patch = build_pr_description_patch("Hello\nWorld");
assert_eq!(patch, "@@ -1,2 +1,2 @@\n Hello\n World\n");
}
#[test]
fn test_single_line() {
let patch = build_pr_description_patch("Single line");
assert_eq!(patch, "@@ -1,1 +1,1 @@\n Single line\n");
}
#[test]
fn test_empty_body() {
let patch = build_pr_description_patch("");
assert_eq!(patch, "@@ -1,1 +1,1 @@\n");
}
#[test]
fn test_crlf_conversion() {
let patch = build_pr_description_patch("Line1\r\nLine2\r\nLine3");
assert_eq!(patch, "@@ -1,3 +1,3 @@\n Line1\n Line2\n Line3\n");
}
#[test]
fn test_lines_starting_with_plus_minus() {
let patch = build_pr_description_patch("+added\n-removed\n normal");
assert_eq!(patch, "@@ -1,3 +1,3 @@\n +added\n -removed\n normal\n");
}
#[test]
fn test_empty_lines_in_body() {
let patch = build_pr_description_patch("Hello\n\nWorld");
assert_eq!(patch, "@@ -1,3 +1,3 @@\n Hello\n \n World\n");
}
}