use super::*; use crate::review::{Anchor, Comment, Review, Side, Verdict}; #[derive(Clone, Copy, Eq, PartialEq)] enum ReviewText { Comment(u64), Summary, Overview, Reviews, } enum ReviewSource<'a> { Stored(i64), Json(&'a Path), New, } pub(super) struct ReviewBuffer { owner: usize, kind: ReviewText, title: String, pending_comment: Option, } impl Editor { pub fn set_review_directory(&mut self, directory: PathBuf) { self.review_directory = Some(directory); } pub(super) fn initialize_local_review(&mut self) -> Result<(), String> { let Some(directory) = self.review_directory.as_ref() else { return Ok(()); }; let Some(diff) = self .workspace_diff() .filter(|diff| diff.phase == Phase::View) else { return Ok(()); }; if diff.local_review.initialized { return Ok(()); } let root = diff .comparison .as_ref() .ok_or("Comparison is loading")? .root .clone(); let directory = directory.clone(); self.review_diff_mut().unwrap().local_review.initialized = true; self.review_diff_mut().unwrap().local_review.document = Review::new(root.clone()); let store = crate::review::Store::open(&directory)?; let mut reviews = store.list(&root)?; if reviews.len() == 1 { self.review_diff_mut().unwrap().local_review.document = reviews.pop().unwrap(); } else if reviews.len() > 1 { self.show_review_selector(reviews)?; } Ok(()) } fn open_review_selector(&mut self) -> Result<(), String> { let diff = self.workspace_diff().ok_or("No comparison is open")?; let directory = self .review_directory .as_deref() .ok_or("Review storage is not configured")?; let root = &diff .comparison .as_ref() .ok_or("Comparison is loading")? .root; let reviews = crate::review::Store::open(directory)?.list(root)?; self.show_review_selector(reviews) } fn show_review_selector(&mut self, reviews: Vec) -> Result<(), String> { let mut text = String::from( "# Local reviews\n\nEnter opens the review under the caret.\n:diff.review.new starts a separate review.\n", ); for review in reviews { let id = review.storage_id.unwrap(); let label = review .summary .lines() .find(|line| !line.trim().is_empty()) .map_or_else(|| format!("Review {id}"), |line| line.trim().to_owned()); use std::fmt::Write as _; writeln!(text, "\n## Review #{id} · {label}").unwrap(); } let row = text .lines() .position(|line| line.starts_with("## Review #")) .unwrap_or(0); self.open_review_buffer( ReviewText::Reviews, &text, "Local reviews · Enter opens · :diff.review.new".into(), )?; self.place_navigation(TextPosition { row, column: 0 }); Ok(()) } fn mark_review_saved(&mut self) { self.review_diff_mut().unwrap().local_review.dirty = false; for buffer in std::iter::once(&mut self.active).chain(self.parked.values_mut()) { if buffer .review_buffer .as_ref() .is_some_and(|binding| binding.owner == self.tab_id) { buffer.review_buffer.as_mut().unwrap().pending_comment = None; buffer.saved_lines.clone_from(&buffer.lines); buffer.dirty_cache.set(Some(false)); } } } pub(super) fn autosave_local_review(&mut self) -> Result<(), String> { let Some(directory) = self.review_directory.as_deref() else { return Ok(()); }; if !self .workspace_diff() .is_some_and(|diff| diff.local_review.dirty) { return Ok(()); } let mut store = crate::review::Store::open(directory)?; store.save( &mut self .review_diff_mut() .ok_or("No comparison is open")? .local_review .document, )?; self.review_diff_mut().unwrap().local_review.dirty = false; Ok(()) } pub fn review_panel_title(&self) -> Option<&str> { self.active .review_buffer .as_ref() .map(|buffer| buffer.title.as_str()) } pub fn review_panel(&self) -> Option { self.review_pane .filter(|pane| *pane == self.pane_id && self.workspace_diff().is_some()) } pub(super) fn dismiss_review_panel(&mut self) -> bool { let Some(panel) = self.review_panel() else { return false; }; let source = self.panes[&panel] .origin .expect("review panel has a source pane"); self.finish_pane_interaction(); if let Some(mut binding) = self.active.review_buffer.take() { let text = self.active.saved_lines.join("\n"); let wrap = self.active.view.word_wrap; let mut buffer = Buffer::from_text(self.active.id, None, None, &text, None) .expect("saved review text is valid"); buffer.view.word_wrap = wrap; buffer.resolver_readonly = self.active.resolver_readonly; if binding.pending_comment.take().is_some() { self.review_diff_mut().unwrap().local_review.selected = None; } buffer.review_buffer = Some(binding); self.active = buffer; } self.select_pane(source); self.bind_diff_buffer(); true } fn review_diff_mut(&mut self) -> Option<&mut DiffState> { self.diff.as_mut().or(self.parked_diff.as_mut()) } pub(super) fn local_reviews_dirty(&self) -> bool { self.workspace_diff() .is_some_and(|diff| diff.local_review.dirty) || self .tabs .values() .any(|tab| tab.diff().is_some_and(|diff| diff.local_review.dirty)) || std::iter::once(&self.active) .chain(self.parked.values()) .any(|buffer| buffer.review_buffer.is_some() && buffer.dirty()) } pub(super) fn current_review_dirty(&self) -> bool { self.workspace_diff() .is_some_and(|diff| diff.local_review.dirty) || std::iter::once(&self.active) .chain(self.parked.values()) .any(|buffer| { buffer .review_buffer .as_ref() .is_some_and(|binding| binding.owner == self.tab_id) && buffer.dirty() }) } pub(super) fn local_review_close_blocked(&mut self) -> bool { if self.review_directory.is_some() && let Err(error) = self.autosave_local_review() { self.set_message(format!("Review not saved: {error}")); return true; } if self.current_review_dirty() { self.set_message("Unsaved local review; use :diff.review.save before closing"); true } else { false } } pub(super) fn detach_review_buffers(&mut self) { for buffer in std::iter::once(&mut self.active).chain(self.parked.values_mut()) { if buffer .review_buffer .as_ref() .is_some_and(|binding| binding.owner == self.tab_id) { buffer.review_buffer = None; } } } fn replace_local_review( &mut self, root: PathBuf, source: ReviewSource<'_>, ) -> Result<(), String> { let (document, path, dirty) = match source { ReviewSource::Stored(id) => { let directory = self .review_directory .as_deref() .ok_or("Review storage is not configured")?; ( crate::review::Store::open(directory)?.load(&root, id)?, None, false, ) } ReviewSource::Json(path) => ( Review::load(path, &root)?, Some(path.to_path_buf()), self.review_directory.is_some(), ), ReviewSource::New => (Review::new(root), None, true), }; // Retire old drafts before comment IDs can be reused. self.detach_review_buffers(); let state = &mut self.review_diff_mut().unwrap().local_review; state.document = document; state.path = path; state.selected = None; state.pending_navigation = None; state.dirty = dirty; self.review_diff_mut().unwrap().restore_reviewed_files(); // Persistence failure leaves the identity change applied and unsaved. self.autosave_local_review()?; self.open_review_overview() } fn delete_local_review_comment(&mut self, args: &str) -> Result<(), String> { let id = self.review_comment_id(args)?; let state = &mut self.review_diff_mut().unwrap().local_review; state.document.comments.retain(|comment| comment.id != id); state.selected = None; state.pending_navigation = None; state.dirty = true; // The deleted draft cannot later overwrite another comment with the same ID. for buffer in std::iter::once(&mut self.active).chain(self.parked.values_mut()) { if buffer.review_buffer.as_ref().is_some_and(|binding| { binding.owner == self.tab_id && binding.kind == ReviewText::Comment(id) }) { buffer.review_buffer = None; buffer.saved_lines.clone_from(&buffer.lines); buffer.dirty_cache.set(Some(false)); } } self.autosave_local_review()?; self.open_review_overview() } fn sync_review_text(&mut self) -> Vec<(ReviewText, Option)> { let owner = self.tab_id; let updates: Vec<_> = std::iter::once(&self.active) .chain(self.parked.values()) .filter_map(|buffer| { let binding = buffer.review_buffer.as_ref()?; (binding.owner == owner && matches!(binding.kind, ReviewText::Comment(_) | ReviewText::Summary)) .then(|| { ( binding.kind, buffer.lines.join("\n"), binding.pending_comment.clone(), ) }) }) .collect(); let Some(diff) = self.review_diff_mut() else { return Vec::new(); }; let mut previous = Vec::new(); for (kind, text, pending) in updates { if let Some(mut comment) = pending && !diff .local_review .document .comments .iter() .any(|entry| entry.id == comment.id) { previous.push((kind, None)); comment.body = text; diff.local_review.document.comments.push(comment); diff.local_review.dirty = true; continue; } let destination = match kind { ReviewText::Comment(id) => diff .local_review .document .comments .iter_mut() .find(|comment| comment.id == id) .map(|comment| &mut comment.body), ReviewText::Summary => Some(&mut diff.local_review.document.summary), ReviewText::Overview => None, ReviewText::Reviews => None, }; if let Some(destination) = destination && *destination != text { let old = std::mem::replace(destination, text); previous.push((kind, Some(old))); diff.local_review.dirty = true; } } previous } pub(super) fn save_local_review(&mut self, path: Option<&Path>) -> Result<(), String> { let dirty = self .review_diff_mut() .ok_or("No comparison is open")? .local_review .dirty; let previous = self.sync_review_text(); let result = self.write_local_review(path); if result.is_err() { let state = &mut self.review_diff_mut().unwrap().local_review; for (kind, text) in previous { match (kind, text) { (ReviewText::Comment(id), None) => { state.document.comments.retain(|comment| comment.id != id) } (ReviewText::Comment(id), Some(text)) => { state .document .comments .iter_mut() .find(|comment| comment.id == id) .unwrap() .body = text; } (ReviewText::Summary, Some(text)) => state.document.summary = text, _ => unreachable!("only editable review text is synchronized"), } } state.dirty = dirty; } result } fn write_local_review(&mut self, path: Option<&Path>) -> Result<(), String> { if self.review_directory.is_some() { if let Some(path) = path { self.review_diff_mut() .ok_or("No comparison is open")? .local_review .document .save(path)?; self.review_diff_mut().unwrap().local_review.path = Some(path.to_path_buf()); } self.autosave_local_review()?; self.mark_review_saved(); self.set_message(if path.is_some() { "Review exported as JSON; autosave remains in SQLite" } else { "Local review saved to SQLite" }); return Ok(()); } let state = &mut self .review_diff_mut() .ok_or("No comparison is open")? .local_review; let path = path .map(Path::to_path_buf) .or_else(|| state.path.clone()) .ok_or("Use :diff.review.save to choose a local JSON review file")?; state.document.save(&path)?; state.path = Some(path); self.mark_review_saved(); self.set_message("Local review saved"); Ok(()) } pub(super) fn local_review_command( &mut self, action: Action, args: &str, ) -> Result<(), String> { if self.resolver().is_some() { return Err("Local reviews require an ordinary diff comparison".into()); } self.initialize_local_review()?; let diff = self .workspace_diff() .filter(|diff| diff.phase == Phase::View) .ok_or("No comparison is open")?; let root = diff .comparison .as_ref() .ok_or("Comparison is still loading")? .root .clone(); if !matches!( action, Action::ReviewEdit | Action::ReviewDelete | Action::ReviewResolve | Action::ReviewUnresolve | Action::ReviewVerdict | Action::ReviewSave | Action::ReviewOpen ) && !args.is_empty() { return Err("This review command takes no arguments".into()); } if self .workspace_diff() .unwrap() .local_review .document .root .as_os_str() .is_empty() { self.review_diff_mut().unwrap().local_review.document = Review::new(root.clone()); } if !matches!(action, Action::ReviewSave | Action::ReviewCopy) { self.autosave_local_review()?; } if action == Action::ReviewOpen { if self.current_review_dirty() { return Err("Save the current local review before opening another".into()); } if args.is_empty() { return self.open_review_selector(); } let source = if self.review_directory.is_some() { args.parse::() .map(ReviewSource::Stored) .unwrap_or_else(|_| ReviewSource::Json(Path::new(args))) } else { ReviewSource::Json(Path::new(args)) }; return self.replace_local_review(root, source); } match action { Action::DiffComment => self.add_review_comment()?, Action::ReviewNew => self.replace_local_review(root, ReviewSource::New)?, Action::ReviewEdit => { let id = self.review_comment_id(args)?; self.show_review_comment(id)?; } Action::ReviewResolve | Action::ReviewUnresolve => { let id = self.review_comment_id(args)?; let resolved = action == Action::ReviewResolve; let state = &mut self.review_diff_mut().unwrap().local_review; let comment = state .document .comments .iter_mut() .find(|comment| comment.id == id) .unwrap(); if comment.resolved != resolved { comment.resolved = resolved; state.dirty = true; } let status = if resolved { "resolved" } else { "unresolved" }; for buffer in std::iter::once(&mut self.active).chain(self.parked.values_mut()) { if let Some(binding) = buffer.review_buffer.as_mut() && binding.owner == self.tab_id && binding.kind == ReviewText::Comment(id) { binding.title = binding.title.replacen( if resolved { " · unresolved · " } else { " · resolved · " }, &format!(" · {status} · "), 1, ); } } self.autosave_local_review()?; self.open_review_overview()?; } Action::ReviewDelete => self.delete_local_review_comment(args)?, Action::ReviewNext | Action::ReviewPrevious => { let state = &self.workspace_diff().unwrap().local_review; let comments = &state.document.comments; if comments.is_empty() { return Err("No local review comments".into()); } let current = state .selected .and_then(|id| comments.iter().position(|comment| comment.id == id)); let index = match (current, action == Action::ReviewNext) { (Some(index), true) => (index + 1) % comments.len(), (Some(index), false) => (index + comments.len() - 1) % comments.len(), (None, true) => 0, (None, false) => comments.len() - 1, }; self.show_review_comment(comments[index].id)?; } Action::ReviewOverview => self.open_review_overview()?, Action::ReviewCopy => { let review = &self.workspace_diff().unwrap().local_review.document; let count = review.comments.len(); if count == 0 { return Err("No local review comments".into()); } let text = review.format_text(true); self.store_clipboard( vec![text], self.clipboard_mode == ClipboardMode::System, false, )?; self.set_message(format!("Copied {count} review comments")); } Action::ReviewSummary => { let text = self .workspace_diff() .unwrap() .local_review .document .summary .clone(); self.open_review_buffer( ReviewText::Summary, &text, "Review summary · :w saves review".into(), )?; } Action::ReviewVerdict => { let verdict = match args { "looks_good" => Some(Verdict::LooksGood), "changes_requested" => Some(Verdict::ChangesRequested), "none" => None, _ => { return Err( "Usage: diff.review.verdict ".into(), ); } }; let state = &mut self.review_diff_mut().unwrap().local_review; if state.document.verdict != verdict { state.document.verdict = verdict; state.dirty = true; } self.autosave_local_review()?; self.open_review_overview()?; } Action::ReviewSave => { self.save_local_review((!args.is_empty()).then(|| Path::new(args)))? } _ => unreachable!("local review action"), } Ok(()) } fn review_comment_id(&self, args: &str) -> Result { let state = &self .workspace_diff() .ok_or("No comparison is open")? .local_review; let under_cursor = self .active .review_buffer .as_ref() .filter(|binding| binding.kind == ReviewText::Overview) .and_then(|_| { self.active.lines[..=self.cursor().row] .iter() .rev() .find_map(|line| { line.strip_prefix("## Comment #")? .split_whitespace() .next()? .parse() .ok() }) }); let id = if args.is_empty() { under_cursor .or(state.selected) .ok_or("Select a comment or supply its ID")? } else { args.parse() .map_err(|_| "Comment ID must be a positive integer")? }; state .document .comments .iter() .any(|comment| comment.id == id) .then_some(id) .ok_or_else(|| format!("No local comment #{id}")) } fn add_review_comment(&mut self) -> Result<(), String> { let owner = self.diff_owner.ok_or("No comparison is open")?; if self.pane_id != owner { self.select_pane(owner); } self.bind_diff_buffer(); let focus = self .diff_buffer_focus() .filter(|_| self.diff_interacting()) .ok_or("Focus a diff text pane to add a comment")?; if self.selection_count() != 1 { return Err("Use one caret or one contiguous selection for a review comment".into()); } let range = self.selections().into_iter().next().unwrap_or_else(|| { let row = self.cursor().row; Selection { start: Cursor { row, column: 0 }, end: if row + 1 < self.active.lines.len() { Cursor { row: row + 1, column: 0, } } else { Cursor { row, column: grapheme_count(&self.active.lines[row]), } }, } }); let diff = self.diff.as_ref().unwrap(); let comparison = diff.comparison.as_ref().unwrap(); let file = &comparison.files[diff.file]; let side = if focus == Focus::Before { Side::Before } else { Side::After }; let anchor = Anchor { revision: comparison.revisions[side.index()].clone(), path: if side == Side::Before { file.old_path.as_ref().unwrap_or(&file.path) } else { &file.path } .clone(), side, start: TextPosition { row: range.start.row, column: range.start.column, }, end: TextPosition { row: range.end.row, column: range.end.column, }, content: self.active.lines.join("\n"), }; let id = diff .local_review .document .comments .iter() .map(|comment| comment.id) .chain( std::iter::once(&self.active) .chain(self.parked.values()) .filter_map(|buffer| { let binding = buffer.review_buffer.as_ref()?; (binding.owner == self.tab_id) .then_some(binding.pending_comment.as_ref()?.id) }), ) .max() .unwrap_or(0) .checked_add(1) .ok_or("Review comment IDs exhausted")?; self.prepare_command(); self.finish_command_mode(); let comment = Comment { id, anchor, body: String::new(), resolved: false, }; self.open_review_comment(id, Some(&comment))?; self.active.review_buffer.as_mut().unwrap().pending_comment = Some(comment); Ok(()) } fn show_review_comment(&mut self, id: u64) -> Result<(), String> { self.open_review_comment(id, None) } fn open_review_comment(&mut self, id: u64, draft: Option<&Comment>) -> Result<(), String> { let diff = self.workspace_diff().unwrap(); let comment = draft .or_else(|| { diff.local_review .document .comments .iter() .find(|comment| comment.id == id) }) .ok_or("Comment no longer exists")?; let text = comment.body.clone(); let anchor = &comment.anchor; let status = if comment.resolved { "resolved" } else { "unresolved" }; let mapped = diff.review_anchor_range(anchor); let current = mapped.is_some(); let location = if current { "current" } else { "original snapshot" }; let range = mapped.unwrap_or(anchor.start..anchor.end); let title = format!( "Comment #{id} · {status} · {location} · {}:{}:{}–{}:{} · {:?} · :w saves review", anchor.path.display(), range.start.row + 1, range.start.column + 1, range.end.row + 1, range.end.column + 1, anchor.side ); let target = diff.comparison.as_ref().and_then(|comparison| { comparison.files.iter().position(|file| { let path = if anchor.side == Side::Before { file.old_path.as_ref().unwrap_or(&file.path) } else { &file.path }; path == &anchor.path }) }); let stale = !current && target.is_none_or(|index| diff.comparison.as_ref().unwrap().files[index].loaded); let start = anchor.start; let owner = self.diff_owner.unwrap(); self.select_pane(owner); let diff = self.diff.as_mut().unwrap(); diff.local_review.selected = Some(id); if let Some(index) = target { diff.select_file_index(index); diff.local_review.pending_navigation = Some(id); } self.finish_review_navigation(); if draft.is_some() { self.place_navigation(start); } self.open_review_buffer(ReviewText::Comment(id), &text, title)?; if stale { self.set_message("Comment is stale; original snapshot anchor preserved"); } Ok(()) } pub(super) fn finish_review_navigation(&mut self) { let Some(diff) = self.diff.as_mut() else { return; }; let Some(id) = diff.local_review.pending_navigation else { return; }; if !diff.selected_file_loaded() { return; } diff.local_review.pending_navigation = None; let Some(comment) = diff .local_review .document .comments .iter() .find(|comment| comment.id == id) else { return; }; let selected_path = diff .comparison .as_ref() .and_then(|comparison| comparison.files.get(diff.file)) .map(|file| { if comment.anchor.side == Side::Before { file.old_path.as_ref().unwrap_or(&file.path) } else { &file.path } }); if selected_path != Some(&comment.anchor.path) { return; } let Some(range) = diff.review_anchor_range(&comment.anchor) else { self.set_message("Comment is stale; original snapshot anchor preserved"); return; }; let position = range.start; let focus = if comment.anchor.side == Side::Before { Focus::Before } else { Focus::After }; diff.focus = focus; diff.reveal_source_row(focus, position.row); self.bind_diff_buffer(); self.place_navigation(position); } fn open_review_overview(&mut self) -> Result<(), String> { let diff = self.workspace_diff().unwrap(); let state = &diff.local_review; let verdict = match state.document.verdict { Some(Verdict::LooksGood) => "Looks good", Some(Verdict::ChangesRequested) => "Changes requested", None => "Not set", }; let storage = if self.review_directory.is_some() { state.document.storage_id.map_or_else( || "SQLite · uncommitted draft".into(), |id| { format!( "SQLite · review #{id} · revision {}", state.document.storage_revision ) }, ) } else { state .path .as_ref() .map_or_else(|| "not saved".into(), |path| path.display().to_string()) }; let mut text = format!( "# Local review\n\nVerdict: {verdict}\nStorage: {storage}\n\n## Summary\n{}\n\nEnter edits the comment under the caret.\n:diff.review.summary · :diff.review.verdict · :diff.review.save\n", state.document.summary ); for comment in &state.document.comments { let anchor = &comment.anchor; let mapped = diff.review_anchor_range(anchor); let range = mapped.clone().unwrap_or(anchor.start..anchor.end); let status = if mapped.is_some() { "current" } else if diff.comparison.as_ref().is_some_and(|comparison| { comparison.files.iter().any(|file| { !file.loaded && if anchor.side == Side::Before { file.old_path.as_ref().unwrap_or(&file.path) == &anchor.path } else { file.path == anchor.path } }) }) { "not checked (file not loaded)" } else { "stale" }; let resolution = if comment.resolved { "resolved" } else { "unresolved" }; use std::fmt::Write as _; write!( text, "\n## Comment #{} · {resolution} · {status}\n{}:{}:{}–{}:{} · {:?}\nRevision: {}\n{}\n", comment.id, anchor.path.display(), range.start.row + 1, range.start.column + 1, range.end.row + 1, range.end.column + 1, anchor.side, anchor.revision, comment.body ) .unwrap(); } self.open_review_buffer( ReviewText::Overview, &text, "Local review overview · Enter edits · :diff.review.next".into(), ) } fn open_review_buffer( &mut self, kind: ReviewText, text: &str, title: String, ) -> Result<(), String> { self.prepare_command(); self.finish_command_mode(); let existing = std::iter::once(&self.active) .chain(self.parked.values()) .find(|buffer| { buffer .review_buffer .as_ref() .is_some_and(|binding| binding.owner == self.tab_id && binding.kind == kind) }) .map(|buffer| buffer.id); let panel = if let Some(panel) = self.review_pane { panel } else { let id = self.next_pane_id; self.next_pane_id += 1; self.panes.insert( id, Pane { buffer_id: self.active.id, origin: self.diff_owner, views: BTreeMap::from([(self.active.id, self.active.view.inherit())]), }, ); self.review_pane = Some(id); id }; self.panes.get_mut(&panel).unwrap().origin = self.diff_owner; self.select_pane(panel); if let Some(id) = existing { self.switch_buffer(id); self.active.review_buffer.as_mut().unwrap().title = title; if matches!(kind, ReviewText::Overview | ReviewText::Reviews) { self.active.lines = split_lines(text); self.active.saved_lines.clone_from(&self.active.lines); self.active.text_revision = self.active.text_revision.wrapping_add(1); self.active.dirty_cache.set(Some(false)); self.normalize_targets(); } } else { let mut buffer = Buffer::from_text(self.next_buffer_id, None, None, text, None) .map_err(|error| error.to_string())?; buffer.review_buffer = Some(ReviewBuffer { owner: self.tab_id, kind, title, pending_comment: None, }); buffer.resolver_readonly = matches!(kind, ReviewText::Overview | ReviewText::Reviews); buffer.view.word_wrap = true; self.add_buffer(buffer); } self.set_message(if self.review_directory.is_some() { "Local review · :w saves · Escape discards unsaved text · :diff.review.save path exports JSON" } else { "Local review · :w saves JSON; first choose :diff.review.save " }); Ok(()) } pub(super) fn handle_review_overview(&mut self, input: &Input) -> bool { if *input == Input::Escape && self.active.view.mode == Mode::Normal && self.palette.is_none() && self.sequence_pending().is_none() && self.count.is_none() && self.dismiss_review_panel() { return true; } if *input == Input::Enter && self.active.view.mode == Mode::Normal && self.palette.is_none() && self.active.review_buffer.as_ref().is_some_and(|binding| { matches!(binding.kind, ReviewText::Overview | ReviewText::Reviews) }) { let choice = if self.active.review_buffer.as_ref().unwrap().kind == ReviewText::Reviews { self.active.lines[..=self.cursor().row] .iter() .rev() .find_map(|line| line.strip_prefix("## Review #")?.split_whitespace().next()) .map(str::to_owned) .ok_or_else(|| "Place the caret on a review".to_string()) .and_then(|id| self.local_review_command(Action::ReviewOpen, &id)) } else { self.local_review_command(Action::ReviewEdit, "") }; if let Err(error) = choice { self.set_message(error); } true } else { false } } }