use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
use notify::Watcher;
use crate::cache::PrCacheKey;
use super::types::*;
use super::App;
impl App {
pub(crate) fn toggle_markdown_rich(&mut self) {
self.markdown_rich = !self.markdown_rich;
let current_is_md = self
.files()
.get(self.selected_file)
.map(|f| crate::language::is_markdown_ext_from_filename(&f.filename))
.unwrap_or(false);
if current_is_md {
self.diff_cache = None;
self.diff_cache_receiver = None;
}
let files = self.files();
let md_indices: Vec<usize> = self
.highlighted_cache_store
.keys()
.copied()
.filter(|idx| {
files
.get(*idx)
.map(|f| crate::language::is_markdown_ext_from_filename(&f.filename))
.unwrap_or(false)
})
.collect();
for idx in md_indices {
self.highlighted_cache_store.remove(&idx);
}
self.pr_description_cache = None;
self.prefetch_receiver = None;
}
pub(crate) fn toggle_local_mode(&mut self) {
if matches!(self.state, AppState::AiRally) {
self.submission_result =
Some((false, "Cannot toggle mode during AI Rally".to_string()));
self.submission_result_time = Some(Instant::now());
return;
}
self.mark_viewed_receiver = None;
self.batch_diff_receiver = None;
self.lazy_diff_receiver = None;
self.lazy_diff_pending_file = None;
if self.local_mode {
self.deactivate_watcher();
self.saved_local_snapshot = Some(self.save_view_snapshot());
self.local_mode = false;
self.file_list_filter = None;
if let Some(snapshot) = self.saved_pr_snapshot.take() {
let pr_number = snapshot.pr_number;
self.restore_view_snapshot(snapshot);
if let Some(pr) = pr_number {
self.update_data_receiver_origin(pr);
}
self.restore_data_from_cache();
} else if let Some(pr) = self.original_pr_number {
self.pr_number = Some(pr);
self.update_data_receiver_origin(pr);
self.restore_data_from_cache();
} else if self.started_from_pr_list {
self.back_to_pr_list();
} else {
self.local_mode = true;
self.saved_local_snapshot = None; if let Some(handle) = &self.watcher_handle {
handle.active.store(true, Ordering::Release);
}
self.submission_result = Some((false, "No PR to return to".to_string()));
self.submission_result_time = Some(Instant::now());
return;
}
self.submission_result = Some((true, "Switched to PR mode".to_string()));
} else {
let from_pr_list = matches!(self.state, AppState::PullRequestList);
self.saved_pr_snapshot = Some(self.save_view_snapshot());
self.local_mode = true;
self.file_list_filter = None;
if from_pr_list {
self.state = AppState::FileList;
}
if let Some(snapshot) = self.saved_local_snapshot.take() {
self.restore_view_snapshot(snapshot);
} else {
self.selected_file = 0;
self.file_list_scroll_offset = 0;
self.selected_line = 0;
self.scroll_offset = 0;
self.diff_cache = None;
self.highlighted_cache_store.clear();
self.review_comments = None;
self.discussion_comments = None;
}
self.pr_number = Some(0);
self.update_data_receiver_origin(0);
self.diff_cache_receiver = None;
self.prefetch_receiver = None;
let cache_key = PrCacheKey {
repo: self.repo.clone(),
pr_number: 0,
};
if let Some(cached) = self.session_cache.get_pr_data(&cache_key) {
self.data_state = DataState::Loaded {
pr: cached.pr.clone(),
files: cached.files.clone(),
};
self.diff_line_count =
Self::calc_diff_line_count(&cached.files, self.selected_file);
self.start_prefetch_all_files();
} else {
self.data_state = DataState::Loading;
}
self.activate_watcher();
self.retry_load();
self.submission_result = Some((true, "Switched to Local mode".to_string()));
}
self.submission_result_time = Some(Instant::now());
}
pub(crate) fn update_data_receiver_origin(&mut self, pr_number: u32) {
if let Some((ref mut origin, _)) = self.data_receiver {
*origin = pr_number;
}
}
pub(crate) fn restore_data_from_cache(&mut self) {
let pr_number = self.pr_number.unwrap_or(0);
let cache_key = PrCacheKey {
repo: self.repo.clone(),
pr_number,
};
if let Some(cached) = self.session_cache.get_pr_data(&cache_key) {
self.data_state = DataState::Loaded {
pr: cached.pr.clone(),
files: cached.files.clone(),
};
self.diff_line_count = Self::calc_diff_line_count(&cached.files, self.selected_file);
self.start_prefetch_all_files();
} else {
self.data_state = DataState::Loading;
}
self.retry_load();
}
pub(crate) fn detect_local_base_branch(working_dir: Option<&str>) -> Option<String> {
let mut cmd = std::process::Command::new("git");
cmd.args(["rev-parse", "--abbrev-ref", "@{upstream}"]);
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
if let Ok(output) = cmd.output() {
if output.status.success() {
let upstream = String::from_utf8_lossy(&output.stdout).trim().to_string();
if let Some(branch) = upstream.strip_prefix("origin/") {
return Some(branch.to_string());
}
return Some(upstream);
}
}
for candidate in &["main", "master"] {
let mut cmd = std::process::Command::new("git");
cmd.args(["rev-parse", "--verify", &format!("origin/{}", candidate)]);
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
if let Ok(output) = cmd.output() {
if output.status.success() {
return Some(candidate.to_string());
}
}
}
None
}
pub(crate) fn save_view_snapshot(&mut self) -> ViewSnapshot {
ViewSnapshot {
pr_number: self.pr_number,
selected_file: self.selected_file,
file_list_scroll_offset: self.file_list_scroll_offset,
selected_line: self.selected_line,
scroll_offset: self.scroll_offset,
diff_cache: self.diff_cache.take(),
highlighted_cache_store: std::mem::take(&mut self.highlighted_cache_store),
review_comments: self.review_comments.take(),
discussion_comments: self.discussion_comments.take(),
local_file_signatures: std::mem::take(&mut self.local_file_signatures),
local_file_patch_signatures: std::mem::take(&mut self.local_file_patch_signatures),
}
}
pub(crate) fn restore_view_snapshot(&mut self, snapshot: ViewSnapshot) {
self.pr_number = snapshot.pr_number;
self.selected_file = snapshot.selected_file;
self.file_list_scroll_offset = snapshot.file_list_scroll_offset;
self.selected_line = snapshot.selected_line;
self.scroll_offset = snapshot.scroll_offset;
self.diff_cache = snapshot.diff_cache;
self.highlighted_cache_store = snapshot.highlighted_cache_store;
self.review_comments = snapshot.review_comments;
self.discussion_comments = snapshot.discussion_comments;
self.local_file_signatures = snapshot.local_file_signatures;
self.local_file_patch_signatures = snapshot.local_file_patch_signatures;
self.diff_cache_receiver = None;
self.prefetch_receiver = None;
self.comment_receiver = None;
self.discussion_comment_receiver = None;
self.comment_submit_receiver = None;
self.comment_submitting = false;
self.comments_loading = false;
self.discussion_comments_loading = false;
}
pub(crate) fn activate_watcher(&mut self) {
if let Some(ref handle) = self.watcher_handle {
handle.active.store(true, Ordering::Release);
return;
}
let Some(ref retry_sender) = self.retry_sender else {
return;
};
let refresh_pending = self
.refresh_pending
.get_or_insert_with(|| Arc::new(AtomicBool::new(false)))
.clone();
let watch_dir = self.working_dir.clone().unwrap_or_else(|| {
std::env::current_dir()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string())
});
let active = Arc::new(AtomicBool::new(true));
let active_clone = active.clone();
let refresh_tx = retry_sender.clone();
let thread = std::thread::spawn(move || {
let callback = move |result: notify::Result<notify::Event>| {
if !active_clone.load(Ordering::Acquire) {
return;
}
let Ok(event) = result else {
return;
};
let dominated_by_git = event
.paths
.iter()
.all(|p| p.components().any(|c| c.as_os_str() == ".git"));
let is_access = matches!(event.kind, notify::EventKind::Access(_));
if !is_access && !dominated_by_git && !refresh_pending.swap(true, Ordering::AcqRel)
{
let _ = refresh_tx.try_send(RefreshRequest::LocalRefresh);
}
};
let Ok(mut watcher) =
notify::RecommendedWatcher::new(callback, notify::Config::default())
else {
return;
};
let _ = watcher.watch(
std::path::Path::new(&watch_dir),
notify::RecursiveMode::Recursive,
);
loop {
std::thread::sleep(std::time::Duration::from_secs(60));
}
});
self.watcher_handle = Some(WatcherHandle {
active,
_thread: thread,
});
}
pub(crate) fn deactivate_watcher(&mut self) {
if let Some(ref handle) = self.watcher_handle {
handle.active.store(false, Ordering::Release);
}
}
pub(crate) fn toggle_auto_focus(&mut self) {
self.local_auto_focus = !self.local_auto_focus;
let msg = if self.local_auto_focus {
"Auto-focus: ON"
} else {
"Auto-focus: OFF"
};
self.submission_result = Some((true, msg.to_string()));
self.submission_result_time = Some(Instant::now());
}
}