use review_protocol::{
CommentSide, NewComment, PullRequest, ReviewProvider, ReviewVerdict,
};
use tui_textarea::TextArea;
pub struct ReviewSession {
pub pr: PullRequest,
pub provider: Box<dyn ReviewProvider>,
pub drafts: Vec<DraftComment>,
pub modal: Option<Modal>,
pub last_status: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DraftComment {
pub path: String,
pub line: u32,
pub side: CommentSide,
pub body: String,
}
impl DraftComment {
pub fn into_protocol(self) -> NewComment {
NewComment {
path: self.path,
line: self.line,
side: self.side,
body: self.body,
}
}
}
pub enum Modal {
Comment(CommentModal),
Submit(SubmitModal),
Drafts(DraftsModal),
}
pub struct CommentModal {
pub path: String,
pub line: u32,
pub side: CommentSide,
pub textarea: TextArea<'static>,
pub editing_index: Option<usize>,
}
impl CommentModal {
pub fn new(path: String, line: u32, side: CommentSide) -> Self {
let mut textarea = TextArea::default();
textarea.set_cursor_line_style(ratatui::style::Style::default());
Self {
path,
line,
side,
textarea,
editing_index: None,
}
}
pub fn editing(
index: usize,
path: String,
line: u32,
side: CommentSide,
body: &str,
) -> Self {
let mut m = Self::new(path, line, side);
m.editing_index = Some(index);
for (i, l) in body.split('\n').enumerate() {
if i > 0 {
m.textarea.insert_newline();
}
m.textarea.insert_str(l);
}
m
}
pub fn body(&self) -> String {
self.textarea.lines().join("\n")
}
}
pub struct DraftsModal {
pub selected: usize,
}
impl DraftsModal {
pub fn new() -> Self {
Self { selected: 0 }
}
}
pub struct SubmitModal {
pub verdict: ReviewVerdict,
pub textarea: TextArea<'static>,
pub focus: SubmitFocus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubmitFocus {
Verdict,
Body,
}
impl SubmitModal {
pub fn new(default: ReviewVerdict) -> Self {
let mut textarea = TextArea::default();
textarea.set_cursor_line_style(ratatui::style::Style::default());
textarea.set_block(
ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.title(" Overall body "),
);
Self {
verdict: default,
textarea,
focus: SubmitFocus::Verdict,
}
}
pub fn body(&self) -> String {
self.textarea.lines().join("\n")
}
}
impl ReviewSession {
pub fn new(pr: PullRequest, provider: Box<dyn ReviewProvider>) -> Self {
Self {
pr,
provider,
drafts: Vec::new(),
modal: None,
last_status: None,
}
}
pub fn has_draft_at(&self, path: &str, line: u32, side: CommentSide) -> bool {
self.drafts
.iter()
.any(|d| d.path == path && d.line == line && d.side == side)
}
pub fn open_drafts(&mut self) {
if self.drafts.is_empty() {
self.last_status = Some("no drafts to show".into());
return;
}
self.modal = Some(Modal::Drafts(DraftsModal::new()));
}
pub fn edit_draft(&mut self, index: usize) {
let Some(d) = self.drafts.get(index) else {
return;
};
self.modal = Some(Modal::Comment(CommentModal::editing(
index,
d.path.clone(),
d.line,
d.side,
&d.body,
)));
}
pub fn delete_draft(&mut self, index: usize) {
if index < self.drafts.len() {
self.drafts.remove(index);
}
if self.drafts.is_empty() {
self.modal = None;
} else if let Some(Modal::Drafts(m)) = &mut self.modal {
if m.selected >= self.drafts.len() {
m.selected = self.drafts.len() - 1;
}
}
}
pub fn open_comment(&mut self, path: String, line: u32, side: CommentSide) {
self.modal = Some(Modal::Comment(CommentModal::new(path, line, side)));
}
pub fn open_submit(&mut self) {
self.modal = Some(Modal::Submit(SubmitModal::new(ReviewVerdict::Comment)));
}
pub fn close_modal(&mut self) {
self.modal = None;
}
pub fn save_comment_draft(&mut self) {
let Some(Modal::Comment(m)) = self.modal.take() else {
return;
};
let body = m.body();
if body.trim().is_empty() {
return;
}
let entry = DraftComment {
path: m.path,
line: m.line,
side: m.side,
body,
};
match m.editing_index {
Some(i) if i < self.drafts.len() => {
self.drafts[i] = entry;
}
_ => self.drafts.push(entry),
}
}
pub fn submit(&mut self) {
let Some(Modal::Submit(m)) = self.modal.take() else {
return;
};
let body = m.body();
let verdict = m.verdict;
let comments: Vec<NewComment> = std::mem::take(&mut self.drafts)
.into_iter()
.map(DraftComment::into_protocol)
.collect();
match self.provider.submit_review(&self.pr, &body, verdict, comments) {
Ok(()) => self.last_status = Some(format!("review submitted ({:?})", verdict)),
Err(e) => {
self.last_status = Some(format!("submit failed: {e}"));
}
}
}
}