Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/plannotator-tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ cargo build --release

| Where | Keys |
|---|---|
| anywhere | `Tab` cycle focus (tree · document · rail) · `E` send feedback (clipboard) · `t` show/hide tree · `r` reload · `q` quit |
| document | drag with the mouse, or `v` then `hjkl` / `w` `b` / `0` `$` to select; `Enter` confirms · `j`/`k` or click selects a block · `c` comments on the block · `x` clears the block's annotations |
| anywhere | `Tab` cycle focus (tree · document · rail) · `E` send feedback (clipboard) · `C` clear all annotations on the current document (confirm) · `t` show/hide tree · `r` reload · `q` quit |
| document | drag with the mouse, or `v` then `hjkl` / `w` `b` / `0` `$` to select; `Enter` confirms · `j`/`k` or click selects a block · `c` comments on the block · `x` clears the block's annotations · wide Markdown tables wrap by cell |
| selection toolbar | `a` 👍 looks good · `c` 💬 comment (opens a box at the selection) · `d` ✗ delete · `Esc` clears |
| rail | `j`/`k` move · `e` / `Enter` edit body · `x` remove · click a bubble to focus it |
| file/folder review | `E` send new · `m` review menu (`R` resend all · `F` finish review · `U` undo · `H` archive) |
Expand Down
10 changes: 9 additions & 1 deletion crates/plannotator-tui/src/app/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl App {
Mode::Pick => self.draw_pick(frame),
Mode::Archive => self.draw_archive(frame),
Mode::ReviewMenu => self.draw_review_menu(frame),
Mode::Browse | Mode::ConfirmQuit => {}
Mode::Browse | Mode::ConfirmQuit | Mode::ConfirmClearDocument => {}
}
}

Expand Down Expand Up @@ -372,6 +372,14 @@ impl App {
}

fn draw_footer(&mut self, frame: &mut Frame, mut area: Rect) {
if self.mode == Mode::ConfirmClearDocument {
let question = format!(
" clear all {} annotation(s) on current document? y clear · n cancel",
self.open.store.len()
);
frame.render_widget(Paragraph::new(Line::from(Span::raw(question).bold())), area);
return;
}
if self.mode == Mode::ConfirmQuit {
// The question owns the footer: the browse help would name keys that are not
// live while it is up.
Expand Down
36 changes: 36 additions & 0 deletions crates/plannotator-tui/src/app/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ impl App {
Event::Key(key) if key.kind != KeyEventKind::Release => match &self.mode {
Mode::Browse => self.browse_key(*key),
Mode::ConfirmQuit => self.confirm_quit_key(*key),
Mode::ConfirmClearDocument => self.confirm_clear_document_key(*key),
Mode::Pick => self.pick_key(*key),
Mode::Archive => {
self.archive_key(*key);
Expand Down Expand Up @@ -56,6 +57,10 @@ impl App {
return Ok(());
}
(KeyCode::Char('E'), _) => return self.send_feedback(),
(KeyCode::Char('C'), _) => {
self.request_clear_document();
return Ok(());
}
(KeyCode::Char('m'), _) if self.is_file_review() => {
self.open_review_menu();
return Ok(());
Expand Down Expand Up @@ -109,6 +114,36 @@ impl App {
Ok(())
}

/// Ask before deleting every annotation on the current document.
fn request_clear_document(&mut self) {
if self.open.store.len() == 0 {
self.status = Some("no annotations on current document".into());
} else {
self.mode = Mode::ConfirmClearDocument;
}
}

/// Confirm or cancel the current-document clear operation.
fn confirm_clear_document_key(&mut self, key: KeyEvent) -> Result<()> {
match key.code {
KeyCode::Char('y' | 'Y') | KeyCode::Enter => {
let removed = self.open.store.clear_all()?;
self.mark_unsent();
self.clear_selection();
self.rail_cursor = 0;
self.sync_tree_counts();
self.mode = Mode::Browse;
self.status = Some(format!("cleared {removed} annotation(s) on current document"));
}
KeyCode::Char('n' | 'N') | KeyCode::Esc => {
self.mode = Mode::Browse;
self.status = Some("clear cancelled".into());
}
_ => {}
}
Ok(())
}

fn cycle_focus(&mut self) {
let has_tree = self.tree.is_some();
let has_rail = !self.open.store.placed().is_empty();
Expand Down Expand Up @@ -304,6 +339,7 @@ impl App {
Mode::Compose
| Mode::Browse
| Mode::ConfirmQuit
| Mode::ConfirmClearDocument
| Mode::Pick
| Mode::Archive
| Mode::ReviewMenu => {
Expand Down
2 changes: 2 additions & 0 deletions crates/plannotator-tui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ enum Mode {
Edit(String),
/// Quit was asked for while feedback is unsent; the footer asks first.
ConfirmQuit,
/// Clearing every annotation on the current document was requested; the footer asks first.
ConfirmClearDocument,
/// Choosing which of the agent's recent messages to review.
Pick,
/// Restoring annotations from finished file reviews.
Expand Down
19 changes: 19 additions & 0 deletions crates/plannotator-tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,25 @@ fn quitting_with_unsent_feedback_asks_before_it_quits() {
assert_eq!(app.send_state, SendState::Ready, "nothing was sent");
}

#[test]
fn uppercase_c_clears_all_annotations_on_the_current_document_after_confirmation() {
let mut app = app(Box::new(Discard));
app.add_block_annotation(0, Kind::Comment, "first".to_owned()).expect("annotation");
app.add_block_annotation(1, Kind::LooksGood, String::new()).expect("annotation");
assert_eq!(app.open.store.len(), 2);

app.handle_event(&key(KeyCode::Char('C'), KeyModifiers::NONE)).expect("clear request");
assert_eq!(app.mode, Mode::ConfirmClearDocument);
app.handle_event(&key(KeyCode::Char('n'), KeyModifiers::NONE)).expect("cancel clear");
assert_eq!(app.open.store.len(), 2);

app.handle_event(&key(KeyCode::Char('C'), KeyModifiers::NONE)).expect("clear request");
app.handle_event(&key(KeyCode::Char('y'), KeyModifiers::NONE)).expect("confirm clear");
assert_eq!(app.mode, Mode::Browse);
assert_eq!(app.open.store.len(), 0);
assert!(app.status.as_deref().is_some_and(|status| status.starts_with("cleared 2")));
}

fn candidates() -> Vec<plannotator_tui_hosts::Message> {
use plannotator_tui_hosts::{Message, Role};
let message = |id: &str, text: &str, at: &str| Message {
Expand Down
6 changes: 4 additions & 2 deletions crates/plannotator-tui/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tui_markdown::{Options, StyleSheet};

use crate::doc::{BlockKind, Document};
use crate::srcmap::{LineOffsets, align};
use crate::wrap::{Row, clip_line, wrap_line};
use crate::wrap::{Row, clip_line, wrap_line, wrap_table};

/// Rows of vertical space between blocks.
const BLOCK_GAP: usize = 1;
Expand Down Expand Up @@ -111,7 +111,9 @@ impl DocLayout {
for block in &mut self.blocks {
block.first_row = row;
let lines = block.text.lines.iter().zip(&block.offsets);
block.rows = if block.kind.preserves_columns() {
block.rows = if block.kind == BlockKind::Table {
wrap_table(&block.text.lines, &block.offsets, width)
} else if block.kind.preserves_columns() {
lines.map(|(l, o)| clip_line(l, o, width)).collect()
} else {
lines.flat_map(|(l, o)| wrap_line(l, o, width)).collect()
Expand Down
9 changes: 9 additions & 0 deletions crates/plannotator-tui/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ impl Store {
Ok(removed)
}

/// Remove every annotation belonging to the open document. Returns how many were removed.
pub(crate) fn clear_all(&mut self) -> Result<usize> {
let removed = self.annotations.len();
self.annotations.clear();
self.resolved.clear();
self.save()?;
Ok(removed)
}

fn remove_unsaved(&mut self, id: &str) -> bool {
let Some(index) = self.annotations.iter().position(|a| a.id == id) else { return false };
self.annotations.remove(index);
Expand Down
Loading