wisp/screens/git_diff/
mod.rs1use std::{path::PathBuf, sync::Arc};
2
3use clankerdiff_git::RepositorySnapshot;
4use clankerdiff_ratatui::diff::{DiffReviewEvent, DiffScope, InteractionPhase, ReviewCapabilities};
5use clankerdiff_ratatui::theme::ThemeChoice;
6use clankerdiff_ratatui::{DiffReviewState, DiffReviewWidget, InputOutcome, default_diff_keybindings};
7use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
8use ratatui::{
9 buffer::Buffer,
10 layout::{Position, Rect},
11 widgets::{Clear, StatefulWidget, Widget},
12};
13
14use crate::git_review::{GitDiffEvent, GitWatchEvent};
15use crate::renderer::DrawContext;
16use crate::request::RequestId;
17use crate::screens::reviewer::{crossterm_event, theme_selection};
18use crate::surfaces::input::{GitReviewOutput, ReviewOutcome, UiEvent};
19use crate::view::generation::Generation;
20use crate::{
21 command::{GitCommand, GitWatchCommand},
22 settings::builtin_review_theme_choices,
23};
24
25pub struct GitDiffScreen {
26 review_id: RequestId,
27 installed: Option<Arc<RepositorySnapshot>>,
28 scope: DiffScope,
29 state: DiffReviewState,
30 theme_generation: Option<Generation>,
31}
32
33impl GitDiffScreen {
34 pub fn new(working_dir: PathBuf) -> (Self, GitWatchCommand) {
35 let mut state = DiffReviewState::loading();
36 let mut bindings = default_diff_keybindings();
37 bindings.retain(|binding| binding.key != KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL).into());
38 state.set_keybindings(bindings);
39 state.set_theme_choices(builtin_review_theme_choices());
40 state.set_capabilities(ReviewCapabilities { clipboard: false, ..ReviewCapabilities::default() });
41 state.set_repository_pending();
42
43 let review_id = RequestId::next();
44 let scope = DiffScope::default();
45 let screen = Self { review_id, installed: None, scope, state, theme_generation: None };
46 (screen, GitWatchCommand::Open { review_id, working_dir, scope })
47 }
48
49 pub fn close(&self) -> GitWatchCommand {
50 GitWatchCommand::Close { review_id: self.review_id }
51 }
52
53 pub fn set_theme_choices(&mut self, choices: Vec<ThemeChoice>) {
54 self.state.set_theme_choices(choices);
55 }
56
57 pub(crate) fn is_browsing(&self) -> bool {
58 self.state.interaction_phase() == InteractionPhase::Browse
59 }
60
61 pub fn on_ui_event(&mut self, event: UiEvent) -> Vec<GitReviewOutput> {
62 self.handle_event(crossterm_event(event))
63 }
64
65 pub fn on_watch_event(&mut self, event: GitWatchEvent) {
66 if event.review_id != self.review_id {
67 return;
68 }
69 match event.result {
70 Ok(retained) => {
71 if retained.snapshot.scope != self.scope {
72 return;
73 }
74 if self.installed.as_ref() != Some(&retained.snapshot) {
75 self.state.set_scope(retained.snapshot.scope);
76 self.state.set_document(retained.snapshot.document.clone());
77 self.installed = Some(retained.snapshot.clone());
78 }
79 self.state.set_background_error(retained.error_message());
80 }
81 Err(error) => {
82 self.state.clear_repository_pending();
83 if self.installed.is_some() {
84 self.state.set_background_error(Some(error.to_string()));
85 } else {
86 self.state.set_error(error.to_string());
87 }
88 }
89 }
90 }
91
92 pub fn on_event(&mut self, event: GitDiffEvent) {
93 if event.review_id == self.review_id {
94 match event.result {
95 Ok(()) => self.state.clear_repository_pending(),
96 Err(error) => self.state.set_repository_error(error.to_string()),
97 }
98 }
99 }
100
101 pub fn render(&mut self, area: Rect, buf: &mut Buffer, cx: &mut DrawContext<'_>) -> Option<Position> {
102 if self.theme_generation != Some(cx.theme_generation) {
103 self.state.set_theme(cx.theme.review().clone());
104 self.theme_generation = Some(cx.theme_generation);
105 }
106 Clear.render(area, buf);
107 DiffReviewWidget::new().title(format!("Git Diff ยท {:?}", self.scope)).render(area, buf, &mut self.state);
108 self.state.cursor_position()
109 }
110
111 fn refresh(&mut self) -> GitWatchCommand {
112 self.state.set_repository_pending();
113 GitWatchCommand::Refresh { review_id: self.review_id, scope: self.scope }
114 }
115
116 fn handle_event(&mut self, event: Event) -> Vec<GitReviewOutput> {
117 let outcome = clankerdiff_ratatui::handle_crossterm_event(&mut self.state, event);
118 match outcome {
119 InputOutcome::Ignored | InputOutcome::Consumed => Vec::new(),
120 InputOutcome::ThemeSelected(id) => vec![GitReviewOutput::SetTheme(theme_selection(id))],
121 InputOutcome::Emitted(event) => match event {
122 DiffReviewEvent::Cancel => vec![GitReviewOutput::Outcome(ReviewOutcome::Cancelled)],
123 DiffReviewEvent::SubmitReview(submission) => {
124 vec![GitReviewOutput::Outcome(ReviewOutcome::Submitted(submission.formatted))]
125 }
126 DiffReviewEvent::RepositoryAction(action) => {
127 self.state.set_repository_pending();
128 vec![GitReviewOutput::Task(GitCommand::Apply { review_id: self.review_id, action })]
129 }
130 DiffReviewEvent::SetScope(scope) => {
131 self.scope = scope;
132 self.state.set_scope(scope);
133 vec![GitReviewOutput::Watch(self.refresh())]
134 }
135 DiffReviewEvent::Refresh => vec![GitReviewOutput::Watch(self.refresh())],
136 DiffReviewEvent::CopyFormattedReview(_) => Vec::new(),
137 },
138 }
139 }
140}