diff --git a/docs/configuration.md b/docs/configuration.md index 8d90ebe..928b901 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -259,7 +259,7 @@ Normal and Visual Ctrl+u/Ctrl+d invoke `config.core.scroll_step_up` / `config.co | Normal | `edit`, `buffers`, `buffer`, `buffer_next`, `buffer_previous`, `buffer_delete`, `buffer_force_delete` | | Normal | `follow_link` | | Normal | `diff`, `diff.mode.toggle`, `diff.view.toggle`, `diff.orientation.toggle`, `diff.review.toggle` | -| Normal, Visual | `comment`, `diff.comment` | +| Normal, Visual | `comment` | | Normal | `review.edit`, `review.delete`, `review.resolve`, `review.unresolve`, `review.next`, `review.previous`, `review.overview`, `review.copy`, `review.summary`, `review.verdict`, `review.save`, `review.open`, `review.new` | | Normal, Insert, Visual, Command, Search | `diff.close` | | Normal | `resolve`, `resolve.next`, `resolve.previous`, `resolve.use_source`, `resolve.source`, `resolve.base`, `resolve.next_file`, `resolve.previous_file`, `resolve.files`, `resolve.draft`, `resolve.history`, `resolve.view.toggle`, `resolve.close` | @@ -346,7 +346,7 @@ Ordinary `config.core.file_picker` and `config.core.file_explorer` bindings work `config.core.diff.review.toggle` is a non-mutating Normal-mode action, also available as `:diff.review.toggle` and in the palette. It marks/unmarks the highlighted changed-file leaf when the comparison's explorer is focused, or the current comparison file from a text pane. Read-only snapshots permit review marks. The default `c` shortcut applies only while browsing the diff's changed-file tree; explicit bindings and disabled keys override it. For example, `config.bind "ctrl+g", config.core.diff.review.toggle`; use the palette or a control-key binding while filtering, including List view, so printable input stays in the query. -Local comments use `config.core.comment` in Normal or Visual mode in ordinary repository source buffers and diff text panes; the remaining `config.core.review` actions in the table above use Normal mode. Update custom Ruby bindings from `config.core.diff.comment` and `config.core.diff.review.*` to these names; only the old Ex command spellings (`:diff.comment` and `:diff.review.*`) remain aliases. File checkmarks still use `config.core.diff.review.toggle`. These actions have no default bindings and are available through the palette and Ex commands. `edit`, `delete`, `resolve`, `unresolve`, `verdict`, `save`, and `open` open argument prompts when selected as actions. Comment IDs are optional and default to the overview entry under the caret or selected comment. For example: +Local comments use `config.core.comment` in Normal or Visual mode in ordinary repository source buffers and diff text panes; the remaining `config.core.review` actions in the table above use Normal mode. File checkmarks use `config.core.diff.review.toggle`. These actions have no default bindings and are available through the palette and Ex commands. `edit`, `delete`, `resolve`, `unresolve`, `verdict`, `save`, and `open` open argument prompts when selected as actions. Comment IDs are optional and default to the overview entry under the caret or selected comment. For example: ```ruby config.bind "leader r c", config.core.comment diff --git a/docs/development.md b/docs/development.md index 6504e3c..693b5f9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -67,7 +67,7 @@ The two skipped shell cleanup checks execute `/bin/ps`, which macOS refuses to l - Prompt-history checks cover separate session-wide command/search entries, Unicode draft restoration, editable recalls, clamped/empty/bounded history, duplicate/empty/cancelled submissions, remapped/disabled keys, and preserved Visual/read-only diff targets. Live PTY smoke exercises arrows and raw Ctrl+k/j, restores drafts, accepts a recalled search with Enter, and checks command/search isolation and unchanged source bytes. - Command-completion checks cover forward/backward alphabetical cycling/wrap across core names, aliases and live plugin registrations, reverse-first completion and direction changes retaining the prefix, explicit execution, edits/history/cancel resets, empty/unknown/Unicode prefixes, untouched arguments, remapped/disabled keys, and preserved Visual/read-only diff targets and literal search tabs. Live PTY smoke cycles `tab.*` in both directions through wraparound, submits a completed `tab.new`, and exits with unchanged source bytes. - Shell-command checks use file-gated subprocesses to prove editing remains live, stdout/stderr/status and cwd handling, null stdin, output bounds, launch/worker errors, and process-group cleanup on completion/drop. Spinner timing uses an injected `Instant`; render checks cover scrolling, dismissal, hidden source caret, and tiny/offset windows. Live PTY smoke gates completion, saves an edit while pending, then observes both output streams and exit status and dismisses the popup. -- Visual-command checks preserve directed characterwise/linewise ranges and multiple carets through command cancellation and plugin dispatch, exercise remapped command entry, and submit `:diff.comment` on either historical side. Live PTY smoke selects lines, cancels a prompt, then runs `:diff.comment` directly and verifies the saved original range without using the palette. +- Visual-command checks preserve directed characterwise/linewise ranges and multiple carets through command cancellation and plugin dispatch, exercise remapped command entry, and submit `:comment` on either historical side. Live PTY smoke selects lines, cancels a prompt, then runs `:comment` directly and verifies the saved original range without using the palette. - Presentation regressions render retained characterwise/linewise highlights during command entry in ordinary, split-diff, and unified views. Floating-review checks preserve source rectangles and the workspace layout, place the caret inside the overlay, retain explicitly saved text across tab switches, and restore source focus on Escape or pane close. - Review prose checks render long comments and summaries with wrapping enabled by default in split/unified views, preserve exact single-line text and source wrapping, and retain an explicit wrap toggle on reopening. Live PTY smoke displays a long comment across continuation rows, saves it, verifies exact text through the review CLI, and reopens it still wrapped. - Precise-range checks cover forward/reversed characterwise and partial-multiline selections on both sides, linewise and caret/empty-line fallbacks, source-column navigation, Unicode grapheme bounds, invalid schemas, and preserved save-conflict checks. Render checks cover exact text/newline cells, wrapping, horizontal clipping, shared unified context, stale suppression, and native Visual precedence. Live PTY smoke saves and reopens a selection containing a wide and combining grapheme. diff --git a/docs/editing.md b/docs/editing.md index 87504c5..07da630 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -196,7 +196,7 @@ Up/Ctrl+k recalls older entries; Down/Ctrl+j recalls newer entries and restores Tab replaces the command-name prefix with the first case-sensitive match in alphabetical order; repeated presses cycle forward through the same matches and wrap. Shift+Tab cycles backward, starting at the last match when no cycle is active. Switching direction retains the original prefix. Candidates include canonical core commands, shorthand aliases, and commands from currently enabled plugins. Empty input cycles all command names. Typing, deleting, or browsing history starts a fresh completion cycle. Completion only changes the prompt: Enter still executes, and Escape cancels. Arguments (including paths) are not completed; Tab and Shift+Tab leave them unchanged. Search-mode Tab remains literal text. Rebind or disable forward/backward completion with `config.core.command_complete` / `config.core.command_complete_previous` in Command mode. -Command input works in both Normal and Visual modes, including read-only diff panes. The original selection stays highlighted while typing. Enter dispatches with its ranges, direction, and linewise shape intact; individual commands decide whether to consume or retain them. Escape returns to the original Visual selection without executing. For example, select lines with `V`, then type `:diff.comment` and press Enter—no command palette is needed. `config.core.command` can also be bound in Visual mode. +Command input works in both Normal and Visual modes, including read-only diff panes. The original selection stays highlighted while typing. Enter dispatches with its ranges, direction, and linewise shape intact; individual commands decide whether to consume or retain them. Escape returns to the original Visual selection without executing. For example, select lines with `V`, then type `:comment` and press Enter—no command palette is needed. `config.core.command` can also be bound in Visual mode. Use `:!COMMAND` (for example `:!pwd` or `:!echo "$PATH"`) to run shell commands asynchronously while continuing to edit. Vamp runs `$SHELL -c COMMAND`, falling back to `/bin/sh`, in the editor's project directory with the inherited environment and closed stdin. Shell quoting, pipes, and redirection work normally; interactive terminal programs are not supported. Commands have the same permissions as Vamp, including file and network access. Submit additional `:!` commands while others are running to execute them concurrently. @@ -480,9 +480,9 @@ Mark or unmark a diff file with **`c` while browsing the changed-file tree**, ** Collapsed folders show a green **`✓`** when every changed file beneath them is reviewed, including nested files and descendants hidden by filtering. Expanded folders stay unmarked. Folder marks update automatically when a descendant's review mark or the changed-file catalog changes. -Review marks are saved with the selected local review and restored by immutable per-file Before/After identities and file paths. Changes to unrelated files or commit metadata do not clear them; changes to either side's bytes, final newline, file mode, or rename paths require re-review. They survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. Editing a writable After file clears only that file's mark and saves the removal; unchanged refreshes and saves preserve it. Marks on live text also retain its exact contents, including the final newline, so different unsaved text cannot inherit a checkmark. Marks do not change document text, undo history, or repository state. Without configured SQLite storage, use `:diff.review.save path` to save them as JSON. Older revision-pair-scoped marks remain stored but must be marked again to acquire per-file identities; comments, summaries, and verdicts are unaffected. +Review marks are saved with the selected local review and restored by immutable per-file Before/After identities and file paths. Changes to unrelated files or commit metadata do not clear them; changes to either side's bytes, final newline, file mode, or rename paths require re-review. They survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. Editing a writable After file clears only that file's mark and saves the removal; unchanged refreshes and saves preserve it. Marks on live text also retain its exact contents, including the final newline, so different unsaved text cannot inherit a checkmark. Marks do not change document text, undo history, or repository state. Without configured SQLite storage, use `:review.save path` to save them as JSON. Older revision-pair-scoped marks remain stored but must be marked again to acquire per-file identities; comments, summaries, and verdicts are unaffected. -**Local review comments** work in ordinary repository source buffers and diff text panes; they share storage with file-reviewed checkmarks and never publish to GitHub. Run **`:comment`** to comment on the current line. A characterwise Visual selection (`v`) anchors its exact text, including partial lines and multiline spans; Visual Line (`V`) anchors whole selected lines. Multiple independent carets/ranges must be reviewed separately. `:diff.comment` remains available for the same operation. +**Local review comments** work in ordinary repository source buffers and diff text panes; they share storage with file-reviewed checkmarks and never publish to GitHub. Run **`:comment`** to comment on the current line. A characterwise Visual selection (`v`) anchors its exact text, including partial lines and multiline spans; Visual Line (`V`) anchors whole selected lines. Multiple independent carets/ranges must be reviewed separately. Comments, summaries, and the review overview open in a focused floating editor over the source workspace, not a source-side split. The source layout, file explorer, and viewports stay unchanged. Normal editing, selections, and undo/redo work in editable review buffers. Escape in Normal mode or `:pane.close` dismisses the window and returns to the source, discarding that editor's unsaved text. An unsaved new comment is not attached to the review; an existing comment or summary reopens with its last saved text and no discarded undo history. Insert/Visual/Command Escape first retains its usual modal behavior. Source rows are never replaced with comment text. Magenta **`○`** gutter markers identify saved comment ranges; **`●`** identifies the selected comment. Its exact current text range has a magenta background, including wrapped continuations and selected newline cells; native Visual selections take precedence. Stale comments have neither markers nor text highlighting. Markers preserve addition/deletion signs, appear only on the first wrapped segment, and also work in unified view. @@ -504,15 +504,15 @@ Local review buffers (comments, summaries, overview, and stored-review selector) | `:review.open [id or path]` | Show stored reviews, open a stored review ID, or import a JSON review as a new review | | `:review.new` | Create a separate stored review for the current repository | -The `:diff.review.*` spellings remain available for these same operations. File checkmarks remain diff-specific (`:diff.review.toggle`). +File checkmarks remain diff-specific (`:diff.review.toggle`). -Comment and summary text is committed only by **`:w`** or **`:diff.review.save`**, which saves the entire review, including hidden drafts. Dismissing an editor discards its unsaved text; switching between review editors can retain hidden drafts but does not save them. File checkmarks, verdicts, deletions, and manual resolution changes commit immediately to local SQLite without committing draft text. `:diff.review.save path` or `:w path` additionally exports JSON; later metadata autosaves still go to SQLite and leave that export unchanged. Imports create a new stored review identity rather than overwriting another review. +Comment and summary text is committed only by **`:w`** or **`:review.save`**, which saves the entire review, including hidden drafts. Dismissing an editor discards its unsaved text; switching between review editors can retain hidden drafts but does not save them. File checkmarks, verdicts, deletions, and manual resolution changes commit immediately to local SQLite without committing draft text. `:review.save path` or `:w path` additionally exports JSON; later metadata autosaves still go to SQLite and leave that export unchanged. Imports create a new stored review identity rather than overwriting another review. -`:diff.review.copy` copies the same text as `vamp --review show ID --all`: repository, summary, verdict, counts, and every saved comment with its anchor and exact excerpt. Resolved and stale comments are included, across all files in the selected review. `clipboard_mode` chooses the system clipboard or Separate-mode internal register. Copying does not save or include unsaved comment/summary drafts. No comments or a clipboard write failure reports an error without replacing the register. +`:review.copy` copies the same text as `vamp --review show ID --all`: repository, summary, verdict, counts, and every saved comment with its anchor and exact excerpt. Resolved and stale comments are included, across all files in the selected review. `clipboard_mode` chooses the system clipboard or Separate-mode internal register. Copying does not save or include unsaved comment/summary drafts. No comments or a clipboard write failure reports an error without replacing the register. From the shell, use `vamp --review delete REVIEW_ID COMMENT_ID [--repo PATH] [--json]` to delete a stored comment immediately without opening the editor. Find IDs with `vamp --review list` and `vamp --review show REVIEW_ID --all`. The repository defaults to the current checkout. Both resolved and unresolved comments can be deleted; the review and any JSON exports remain intact. An editor holding the old review revision must reload before saving. -Storage defaults to `$XDG_DATA_HOME/vamp/reviews/reviews.sqlite3`, or `~/.local/share/vamp/reviews/reviews.sqlite3` when XDG_DATA_HOME is unset or relative. Configure `review_directory` to override its directory; relative configuration paths resolve against the config file. Repository identity is the canonical checkout root. Each review has a persistent database ID independent of comparison revisions, so reopening changed revisions retains stale comments. Opening a comparison automatically restores the repository's sole review. Multiple reviews open a floating selector: move to an entry and press Enter, or use `:diff.review.open id`. Use `:diff.review.new` for another independent review. +Storage defaults to `$XDG_DATA_HOME/vamp/reviews/reviews.sqlite3`, or `~/.local/share/vamp/reviews/reviews.sqlite3` when XDG_DATA_HOME is unset or relative. Configure `review_directory` to override its directory; relative configuration paths resolve against the config file. Repository identity is the canonical checkout root. Each review has a persistent database ID independent of comparison revisions, so reopening changed revisions retains stale comments. Opening a comparison automatically restores the repository's sole review. Multiple reviews open a floating selector: move to an entry and press Enter, or use `:review.open id`. Use `:review.new` for another independent review. Write errors, locked databases, incompatible schemas, and conflicting edits from another editor instance retain dirty drafts and report the failure. Failed text saves leave the committed review unchanged; unrelated metadata autosaves cannot commit those drafts. Normal destructive closes remain guarded until drafts are explicitly saved or discarded. `:w!` does not bypass database revision conflicts; export your draft before reopening the current stored review. JSON exports retain external-file-change protection. Only the current database schema is accepted; existing incompatible databases are preserved rather than reset or migrated. diff --git a/src/editor.rs b/src/editor.rs index 397f314..52b9c7b 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1188,20 +1188,6 @@ const COMMAND_ALIASES: &[(&str, Action)] = &[ ("bprev", Action::BufferPrevious), ("bdelete", Action::BufferDelete), ("bdelete!", Action::BufferForceDelete), - ("diff.comment", Action::Comment), - ("diff.review.edit", Action::ReviewEdit), - ("diff.review.delete", Action::ReviewDelete), - ("diff.review.resolve", Action::ReviewResolve), - ("diff.review.unresolve", Action::ReviewUnresolve), - ("diff.review.next", Action::ReviewNext), - ("diff.review.previous", Action::ReviewPrevious), - ("diff.review.overview", Action::ReviewOverview), - ("diff.review.copy", Action::ReviewCopy), - ("diff.review.summary", Action::ReviewSummary), - ("diff.review.verdict", Action::ReviewVerdict), - ("diff.review.save", Action::ReviewSave), - ("diff.review.open", Action::ReviewOpen), - ("diff.review.new", Action::ReviewNew), ]; fn core_command(name: &str) -> Option<&'static CoreAction> { diff --git a/src/main.rs b/src/main.rs index 20d8766..31b91f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,6 @@ Local reviews: :comment anchors exact Visual text, Visual Line rows, or the curr :review.copy copies all saved comments in CLI format using the configured clipboard :w saves comment/summary text, including hidden drafts; verdicts/deletions commit immediately :review.save PATH exports JSON; :review.open [ID|PATH] selects/imports; :review.new - Existing :diff.comment and :diff.review.* spellings remain available Normal Esc or :pane.close discards unsaved text and dismisses the floating editor Snapshot anchors stay immutable; stale comments lose gutter markers; no publishing Pull requests: :pull_requests [REMOTE]; GitHub/GitLab/Gitea Open/Closed/Drafts/All tabs, type to search diff --git a/tests/unit/editor.rs b/tests/unit/editor.rs index 402b415..e524588 100644 --- a/tests/unit/editor.rs +++ b/tests/unit/editor.rs @@ -1205,11 +1205,11 @@ fn command_completion_respects_bindings_and_preserves_visual_diff_and_search_inp editor.handle(Input::Char('V')); let selections = editor.selections(); let cursor = editor.cursor(); - type_text(&mut editor, ":diff.com"); + type_text(&mut editor, ":comm"); editor.handle(Input::Char('\t')); - assert_eq!(editor.command_line(), "diff.com"); + assert_eq!(editor.command_line(), "comm"); editor.handle(Input::Ctrl('n')); - assert_eq!(editor.command_line(), "diff.comment"); + assert_eq!(editor.command_line(), "comment"); assert_eq!(editor.selections(), selections); assert_eq!(editor.cursor(), cursor); editor.handle(Input::Escape); @@ -3301,7 +3301,7 @@ fn unsaved_review_comments_are_discarded_on_escape() { let mut editor = editor(); editor.set_review_directory(storage.clone()); historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]); - editor.execute("diff.comment"); + editor.execute("comment"); type_text(&mut editor, "iDiscard me"); editor.handle(Input::Escape); editor.handle(Input::Escape); @@ -3313,17 +3313,17 @@ fn unsaved_review_comments_are_discarded_on_escape() { .unwrap() .is_empty() ); - editor.execute("diff.comment"); + editor.execute("comment"); assert_eq!(editor.context().text, ""); type_text(&mut editor, "iKeep me"); editor.handle(Input::Escape); editor.execute("w"); editor.handle(Input::Escape); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); type_text(&mut editor, "A discarded"); editor.handle(Input::Escape); editor.handle(Input::Escape); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); assert_eq!(editor.context().text, "Keep me"); editor.handle(Input::Char('u')); assert_eq!(editor.context().text, "Keep me"); @@ -3342,13 +3342,13 @@ fn review_metadata_autosaves_do_not_commit_hidden_comment_or_summary_drafts() { let mut editor = editor(); editor.set_review_directory(storage.clone()); historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]); - editor.execute("diff.comment"); + editor.execute("comment"); type_text(&mut editor, "iHidden comment"); editor.handle(Input::Escape); - editor.execute("diff.review.summary"); + editor.execute("review.summary"); type_text(&mut editor, "iHidden summary"); editor.handle(Input::Escape); - editor.execute("diff.review.verdict looks_good"); + editor.execute("review.verdict looks_good"); let store = crate::review::Store::open(&storage).unwrap(); let saved = store.list(&root).unwrap().pop().unwrap(); assert!(saved.comments.is_empty()); @@ -3376,12 +3376,12 @@ fn reviewed_file_marks_autosave_restore_and_stay_with_the_selected_review() { let id = saved.storage_id.unwrap(); assert_eq!(saved.reviewed_files[0].path, Path::new("file.txt")); let export = root.join("review.json"); - first.execute(&format!("diff.review.save {}", export.display())); + first.execute(&format!("review.save {}", export.display())); assert_eq!( Review::load(&export, &root).unwrap().reviewed_files, saved.reviewed_files ); - first.execute("diff.review.summary"); + first.execute("review.summary"); type_text(&mut first, "iUnsaved summary"); first.handle(Input::Escape); first.execute("diff.review.toggle"); @@ -3396,9 +3396,9 @@ fn reviewed_file_marks_autosave_restore_and_stay_with_the_selected_review() { reopened.workspace_diff().unwrap().reviewed_files, BTreeSet::from([0]) ); - reopened.execute("diff.review.new"); + reopened.execute("review.new"); assert!(reopened.workspace_diff().unwrap().reviewed_files.is_empty()); - reopened.execute(&format!("diff.review.open {id}")); + reopened.execute(&format!("review.open {id}")); assert_eq!( reopened.workspace_diff().unwrap().reviewed_files, BTreeSet::from([0]) @@ -3474,7 +3474,7 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() { .as_mut() .unwrap() .revisions[1] = "working:new".into(); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); let owner = editor.diff_owner.unwrap(); editor.select_pane(owner); assert_eq!(editor.cursor(), Cursor { row: 2, column: 14 }); @@ -3487,7 +3487,7 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() { .unwrap(); editor.refresh_live_diff(); set_carets(&mut editor, &[(0, 0)], 0); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); assert!(editor.message().contains("stale")); editor.select_pane(owner); assert_eq!(editor.cursor(), Cursor::default()); @@ -3508,7 +3508,7 @@ fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() { vec!["p界e\u{301}z".into()], vec!["changed".into()], ); - type_text(&mut first, "lvl:diff.comment"); + type_text(&mut first, "lvl:comment"); first.handle(Input::Enter); type_text(&mut first, "iFirst feedback"); first.handle(Input::Escape); @@ -3533,17 +3533,17 @@ fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() { persisted.comments[0].anchor.end, TextPosition { row: 0, column: 3 } ); - first.execute("diff.review.summary"); + first.execute("review.summary"); type_text(&mut first, "iSummary"); first.handle(Input::Escape); first.execute("w"); - first.execute("diff.review.verdict changes_requested"); + first.execute("review.verdict changes_requested"); let persisted = store.load(&root, id).unwrap(); assert_eq!(persisted.summary, "Summary"); assert_eq!(persisted.verdict, Some(Verdict::ChangesRequested)); let exported = root.join("shared.json"); - first.execute(&format!("diff.review.save {}", exported.display())); - first.execute("diff.review.edit 1"); + first.execute(&format!("review.save {}", exported.display())); + first.execute("review.edit 1"); type_text(&mut first, "A revised"); first.handle(Input::Escape); first.execute("w"); @@ -3574,7 +3574,7 @@ fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() { .review_anchor_range(&restored.comments[0].anchor, &reopened.local_review) .is_none() ); - reopened.execute("diff.review.new"); + reopened.execute("review.new"); let second_id = reopened.local_review.document.storage_id.unwrap(); assert_ne!(id, second_id); let mut choosing = editor(); @@ -3582,11 +3582,11 @@ fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() { historical_diff(&mut choosing, &root, vec!["old".into()], vec!["new".into()]); choosing.handle(Input::Enter); assert_eq!(choosing.local_review.document.storage_id, Some(second_id)); - choosing.execute(&format!("diff.review.open {id}")); + choosing.execute(&format!("review.open {id}")); assert_eq!(choosing.local_review.document.storage_id, Some(id)); - choosing.execute("diff.review.delete 1"); + choosing.execute("review.delete 1"); assert!(store.load(&root, id).unwrap().comments.is_empty()); - choosing.execute(&format!("diff.review.open {}", exported.display())); + choosing.execute(&format!("review.open {}", exported.display())); let imported_id = choosing.local_review.document.storage_id.unwrap(); assert_ne!(imported_id, id); assert_eq!( @@ -3604,7 +3604,7 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors( let mut first = editor(); first.set_review_directory(storage.clone()); historical_diff(&mut first, &root, vec!["old".into()], vec!["new".into()]); - first.execute("diff.comment"); + first.execute("comment"); type_text(&mut first, "iFeedback"); first.handle(Input::Escape); first.execute("w"); @@ -3614,7 +3614,7 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors( .unwrap() .contains(" · unresolved · ") ); - first.execute("diff.review.resolve"); + first.execute("review.resolve"); assert!( first .context() @@ -3626,7 +3626,7 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors( let id = document.storage_id.unwrap(); let anchor = document.comments[0].anchor.clone(); assert!(store.load(&root, id).unwrap().comments[0].resolved); - first.execute("diff.review.next"); + first.execute("review.next"); assert_eq!(first.context().text, "Feedback"); assert!( first @@ -3635,7 +3635,7 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors( .contains(" · resolved · ") ); let export = root.join("resolved.json"); - first.execute(&format!("diff.review.save {}", export.display())); + first.execute(&format!("review.save {}", export.display())); assert!(Review::load(&export, &root).unwrap().comments[0].resolved); let mut stale = editor(); stale.set_review_directory(storage.clone()); @@ -3645,35 +3645,35 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors( vec!["changed".into()], vec!["new".into()], ); - stale.execute("diff.review.overview"); + stale.execute("review.overview"); assert!( stale .context() .text .contains("Comment #1 · resolved · stale") ); - first.execute("diff.review.unresolve 1"); + first.execute("review.unresolve 1"); assert!(!store.load(&root, id).unwrap().comments[0].resolved); - stale.execute("diff.review.unresolve 1"); + stale.execute("review.unresolve 1"); assert!(stale.current_review_dirty()); assert!(!stale.local_review.document.comments[0].resolved); assert_eq!(stale.local_review.document.comments[0].anchor, anchor); assert!(stale.message().contains("changed in another session")); - first.execute("diff.review.resolve"); + first.execute("review.resolve"); assert!( first .context() .text .contains("Comment #1 · resolved · current") ); - first.execute("diff.review.unresolve"); + first.execute("review.unresolve"); assert!( first .context() .text .contains("Comment #1 · unresolved · current") ); - first.execute(&format!("diff.review.open {}", export.display())); + first.execute(&format!("review.open {}", export.display())); let imported = &first.local_review.document; assert_ne!(imported.storage_id, Some(id)); assert!( @@ -3694,7 +3694,7 @@ fn sqlite_review_conflicts_and_write_failures_keep_dirty_drafts() { let mut first = editor(); first.set_review_directory(storage.clone()); historical_diff(&mut first, &root, vec!["old".into()], vec!["new".into()]); - first.execute("diff.comment"); + first.execute("comment"); type_text(&mut first, "iStored feedback"); first.handle(Input::Escape); first.execute("w"); @@ -3704,8 +3704,8 @@ fn sqlite_review_conflicts_and_write_failures_keep_dirty_drafts() { stale.set_review_directory(storage.clone()); historical_diff(&mut stale, &root, vec!["old".into()], vec!["new".into()]); let version = stale.local_review.document.storage_revision; - first.execute("diff.review.verdict looks_good"); - stale.execute("diff.review.verdict changes_requested"); + first.execute("review.verdict looks_good"); + stale.execute("review.verdict changes_requested"); assert!(stale.current_review_dirty()); assert_eq!(stale.local_review.document.storage_revision, version); assert_eq!( @@ -3719,7 +3719,7 @@ fn sqlite_review_conflicts_and_write_failures_keep_dirty_drafts() { let blocked = root.join("not-a-directory"); fs::write(&blocked, "preserve").unwrap(); first.set_review_directory(blocked.clone()); - first.execute("diff.review.edit 1"); + first.execute("review.edit 1"); type_text(&mut first, "A unsaved"); first.handle(Input::Escape); first.execute("w"); @@ -3758,7 +3758,7 @@ fn local_review_native_composition_roundtrip_and_draft_lifecycle() { ); let owner = editor.pane_id(); let source = editor.context().text; - editor.execute("diff.comment"); + editor.execute("comment"); assert_ne!(editor.pane_id(), owner); assert!( editor @@ -3768,12 +3768,12 @@ fn local_review_native_composition_roundtrip_and_draft_lifecycle() { ); type_text(&mut editor, "iExplain the removed branch"); editor.handle(Input::Escape); - editor.execute("diff.review.summary"); + editor.execute("review.summary"); type_text(&mut editor, "iPlease simplify the branch"); editor.handle(Input::Escape); - editor.execute("diff.review.verdict changes_requested"); + editor.execute("review.verdict changes_requested"); assert!(editor.context().text.contains("Changes requested")); - editor.execute(&format!("diff.review.save {}", path.display())); + editor.execute(&format!("review.save {}", path.display())); let review = Review::load(&path, &root).unwrap(); assert_eq!(review.comments[0].body, "Explain the removed branch"); assert_eq!(review.comments[0].anchor.side, Side::Before); @@ -3781,7 +3781,7 @@ fn local_review_native_composition_roundtrip_and_draft_lifecycle() { assert_eq!(review.summary, "Please simplify the branch"); assert_eq!(review.verdict, Some(Verdict::ChangesRequested)); assert!(!editor.local_reviews_dirty()); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); // Native undo applies to comment text, never to the reviewed source. type_text(&mut editor, "A!"); editor.handle(Input::Escape); @@ -3796,11 +3796,11 @@ fn local_review_native_composition_roundtrip_and_draft_lifecycle() { vec!["before".into(), "removed".into()], vec!["after".into(), "added".into()], ); - editor.execute(&format!("diff.review.open {}", path.display())); + editor.execute(&format!("review.open {}", path.display())); assert!(editor.context().text.contains("Explain the removed branch")); - editor.execute("diff.review.next"); + editor.execute("review.next"); assert_eq!(editor.context().text, "Explain the removed branch"); - editor.execute("diff.review.delete"); + editor.execute("review.delete"); assert!(editor.context().text.starts_with("# Local review")); editor.execute("w"); assert!(Review::load(&path, &root).unwrap().comments.is_empty()); @@ -3827,34 +3827,34 @@ fn review_identity_changes_retire_drafts_before_reusing_comment_ids() { editor.set_review_directory(root.join("data")); } historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]); - editor.execute("diff.comment"); + editor.execute("comment"); type_text(&mut editor, "iOriginal feedback"); editor.handle(Input::Escape); - editor.execute("diff.review.summary"); + editor.execute("review.summary"); type_text(&mut editor, "iKeep summary"); editor.handle(Input::Escape); let original = root.join("original.json"); - editor.execute(&format!("diff.review.save {}", original.display())); + editor.execute(&format!("review.save {}", original.display())); - editor.execute("diff.review.delete 1"); - editor.execute("diff.comment"); + editor.execute("review.delete 1"); + editor.execute("comment"); assert_eq!(editor.context().text, ""); type_text(&mut editor, "iFresh feedback"); editor.handle(Input::Escape); let fresh = root.join("fresh.json"); - editor.execute(&format!("diff.review.save {}", fresh.display())); + editor.execute(&format!("review.save {}", fresh.display())); let document = crate::review::Review::load(&fresh, &root).unwrap(); assert_eq!(document.comments[0].id, 1); assert_eq!(document.comments[0].body, "Fresh feedback"); assert_eq!(document.summary, "Keep summary"); - editor.execute(&format!("diff.review.open {}", original.display())); - editor.execute("diff.review.edit 1"); + editor.execute(&format!("review.open {}", original.display())); + editor.execute("review.edit 1"); assert_eq!(editor.context().text, "Original feedback"); - editor.execute("diff.review.summary"); + editor.execute("review.summary"); assert_eq!(editor.context().text, "Keep summary"); let restored = root.join("restored.json"); - editor.execute(&format!("diff.review.save {}", restored.display())); + editor.execute(&format!("review.save {}", restored.display())); assert_eq!( crate::review::Review::load(&restored, &root) .unwrap() @@ -3863,13 +3863,13 @@ fn review_identity_changes_retire_drafts_before_reusing_comment_ids() { "Original feedback" ); - editor.execute("diff.review.new"); - editor.execute("diff.comment"); + editor.execute("review.new"); + editor.execute("comment"); assert_eq!(editor.context().text, ""); type_text(&mut editor, "iSeparate review"); editor.handle(Input::Escape); let separate = root.join("separate.json"); - editor.execute(&format!("diff.review.save {}", separate.display())); + editor.execute(&format!("review.save {}", separate.display())); let document = crate::review::Review::load(&separate, &root).unwrap(); assert_eq!(document.comments[0].body, "Separate review"); assert_eq!(document.summary, ""); @@ -3893,10 +3893,10 @@ fn precise_review_ranges_capture_graphemes_and_navigate_to_their_start() { editor.bind_diff_buffer(); let owner = editor.pane_id(); type_text(&mut editor, keys); - type_text(&mut editor, ":diff.comment"); + type_text(&mut editor, ":comment"); editor.handle(Input::Enter); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", root.join("range.json").display() )); let diff = editor.workspace_diff().unwrap(); @@ -3936,9 +3936,9 @@ fn precise_review_ranges_capture_graphemes_and_navigate_to_their_start() { historical_diff(&mut editor, &root, vec!["before".into()], split_lines(text)); editor.diff.as_mut().unwrap().focus = Focus::After; editor.bind_diff_buffer(); - editor.execute("diff.comment"); + editor.execute("comment"); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", root.join("range.json").display() )); let anchor = &editor.local_review.document.comments[0].anchor; @@ -4019,13 +4019,13 @@ fn local_review_visual_commands_and_palette_preserve_both_side_ranges() { type_text(&mut editor, "Vj"); if palette { editor.handle(Input::Ctrl('p')); - type_text(&mut editor, "diff.comment"); + type_text(&mut editor, "comment"); } else { - type_text(&mut editor, ":diff.comment"); + type_text(&mut editor, ":comment"); } editor.handle(Input::Enter); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", root.join("range.json").display() )); let diff = editor.workspace_diff().unwrap(); @@ -4054,9 +4054,9 @@ fn local_review_visual_commands_and_palette_preserve_both_side_ranges() { .as_mut() .unwrap() .revisions[anchor.side.index()] = "changed-snapshot".into(); - editor.execute("diff.review.overview"); + editor.execute("review.overview"); assert!(editor.context().text.contains("stale")); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); assert_eq!(editor.local_review.document.comments[0].anchor, anchor); } } @@ -4067,10 +4067,10 @@ fn local_review_guards_metadata_and_hidden_native_drafts_against_loss() { let root = directory.0.canonicalize().unwrap(); let mut editor = editor(); historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]); - editor.execute("diff.comment"); + editor.execute("comment"); type_text(&mut editor, "iUnsaved feedback"); editor.handle(Input::Escape); - editor.execute("diff.review.overview"); + editor.execute("review.overview"); let panel = editor.pane_id(); let source_buffer = editor.diff_return_buffer.unwrap(); editor.delete_buffer(source_buffer, false); @@ -4091,7 +4091,7 @@ fn local_review_guards_metadata_and_hidden_native_drafts_against_loss() { editor.execute("q"); assert!(!editor.should_quit()); let invalid = root.join("missing/review.json"); - editor.execute(&format!("diff.review.save {}", invalid.display())); + editor.execute(&format!("review.save {}", invalid.display())); assert!(editor.local_reviews_dirty()); assert!(editor.local_review.document.comments.is_empty()); assert!( @@ -4101,10 +4101,10 @@ fn local_review_guards_metadata_and_hidden_native_drafts_against_loss() { .any(|buffer| buffer.lines == ["Unsaved feedback"]) ); let path = root.join("review.json"); - editor.execute(&format!("diff.review.save {}", path.display())); + editor.execute(&format!("review.save {}", path.display())); assert!(!editor.local_reviews_dirty()); fs::write(&path, "external change").unwrap(); - editor.execute("diff.review.verdict looks_good"); + editor.execute("review.verdict looks_good"); editor.execute("w"); assert_eq!(fs::read_to_string(&path).unwrap(), "external change"); assert!(editor.local_reviews_dirty()); @@ -5483,7 +5483,7 @@ fn diff_review_actions_preserve_readonly_text_history_and_parked_tabs() { assert_eq!(editor.tab_id(), first_tab); assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([0])); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", directory.path("review.json").display() )); editor.execute("diff.close"); @@ -7636,7 +7636,7 @@ mod system_clipboard { editor.set_clipboard_mode(mode).unwrap(); editor.clipboard = vec!["internal".into()]; fixture.external("external"); - editor.execute("diff.review.copy"); + editor.execute("review.copy"); assert_eq!(editor.clipboard, ["internal"]); assert_eq!(fixture.bytes("state"), b"external"); @@ -7647,11 +7647,11 @@ mod system_clipboard { vec!["xe\u{301}👩‍💻".into(), "界z".into()], vec!["new".into()], ); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); type_text(&mut editor, "A UNSAVED"); editor.handle(Input::Escape); let draft = editor.context().text; - editor.execute("diff.review.copy"); + editor.execute("review.copy"); assert_eq!(editor.context().text, draft); assert!(editor.local_reviews_dirty()); let stored = store.load(&root, id).unwrap(); @@ -7662,7 +7662,7 @@ mod system_clipboard { assert_eq!(editor.clipboard, ["internal"]); let owned = editor.owned_system_clipboard.clone(); fixture.fail("fail-write", true); - editor.execute("diff.review.copy"); + editor.execute("review.copy"); assert_eq!(fixture.bytes("state"), expected.as_bytes()); assert_eq!(editor.owned_system_clipboard, owned); } else { @@ -7671,11 +7671,11 @@ mod system_clipboard { assert_eq!(fixture.bytes("state"), b"external"); assert!(fixture.bytes("writes").is_empty()); } - editor.execute("diff.review.copy unexpected"); + editor.execute("review.copy unexpected"); editor.with_pane(editor.diff_owner.unwrap(), |editor| { editor.local_review.document.comments.clear(); }); - editor.execute("diff.review.copy"); + editor.execute("review.copy"); if mode == "system" { assert_eq!(fixture.bytes("state"), expected.as_bytes()); } else { diff --git a/tests/unit/ruby.rs b/tests/unit/ruby.rs index d045c89..7ba1685 100644 --- a/tests/unit/ruby.rs +++ b/tests/unit/ruby.rs @@ -626,33 +626,6 @@ fn config_leader_defaults_custom_keys_validation_and_required_wire_field() { } } -#[test] -fn source_review_actions_and_diff_command_aliases_share_the_core_catalog() { - let fixture = Fixture::new(); - let path = fixture.file( - "config.rb", - r#" -Vamp.configure do |config| - config.bind "C", config.core.comment - config.bind "N", config.core.review.next - config.bind "S", config.core.review.save - raise "missing diff comment alias" unless Vamp.builtin_command?("diff.comment") - raise "missing diff review alias" unless Vamp.builtin_command?("diff.review.edit") - raise "mutable review action" unless config.core.review.frozen? && config.core.comment.frozen? -end -"#, - ); - let loaded = load_config(&path).unwrap(); - let actions: Vec<_> = loaded - .bindings - .iter() - .filter_map(|binding| binding.command.as_deref()) - .collect(); - assert!(actions.contains(&"core.comment")); - assert!(actions.contains(&"core.review.next")); - assert!(actions.contains(&"core.review.save")); -} - #[test] fn git_gutter_config_modes_default_and_rejection() { use crate::gutter::GutterStyle; diff --git a/tests/unit/tui.rs b/tests/unit/tui.rs index 64d0e74..3d627a3 100644 --- a/tests/unit/tui.rs +++ b/tests/unit/tui.rs @@ -8541,7 +8541,7 @@ fn command_input_keeps_characterwise_and_linewise_selection_highlights() { editor.handle(Input::Right); let ranges = editor.selections(); editor.handle(Input::Char(':')); - for ch in "diff.comment".chars() { + for ch in "comment".chars() { editor.handle(Input::Char(ch)); } terminal @@ -8578,10 +8578,10 @@ fn native_visual_selection_overrides_selected_review_range_background() { editor.execute("diff.view.toggle"); } editor.handle(Input::Char('\t')); - editor.execute("diff.comment"); + editor.execute("comment"); let directory = Directory::new(); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", directory.0.join("review.json").display() )); editor.handle(Input::Escape); @@ -8626,7 +8626,7 @@ fn review_editor_floats_over_the_diff_without_changing_workspace_layout() { .find(|(pane, _)| *pane == owner) .unwrap() .1; - editor.execute("diff.comment"); + editor.execute("comment"); let panel = editor.review_panel().expect("focused floating review"); assert_eq!(editor.pane_layout().len(), 1); assert!(!editor.pane_layout().contains(panel)); @@ -8657,14 +8657,14 @@ fn review_editor_floats_over_the_diff_without_changing_workspace_layout() { assert_eq!(editor.review_panel(), Some(panel)); let directory = Directory::new(); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", directory.0.join("review.json").display() )); editor.handle(Input::Escape); assert!(editor.review_panel().is_none()); assert_eq!(editor.pane_id(), owner); assert_eq!(editor.context().text, source_text); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); assert_eq!( editor.context().text, "Retained draft", @@ -8674,7 +8674,7 @@ fn review_editor_floats_over_the_diff_without_changing_workspace_layout() { assert_eq!(editor.review_panel(), Some(panel)); editor.execute("tab.new"); editor.execute("tab.previous"); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); assert_eq!(editor.review_panel(), Some(panel)); assert_eq!(editor.context().text, "Retained draft"); editor.execute("pane.close"); @@ -8694,7 +8694,7 @@ fn review_prose_wraps_by_default_without_changing_text_or_source_wrapping() { let text = format!("{}wrapped_tail", "prose ".repeat(20)); let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 30)).unwrap(); let mut views = BufferViewports::default(); - for command in ["diff.comment", "diff.review.summary"] { + for command in ["comment", "review.summary"] { editor.execute(command); editor.handle(Input::Char('i')); editor.handle(Input::Paste(text.clone())); @@ -8716,20 +8716,20 @@ fn review_prose_wraps_by_default_without_changing_text_or_source_wrapping() { assert_eq!(editor.context().text, text); let directory = Directory::new(); editor.execute(&format!( - "diff.review.save {}", + "review.save {}", directory.0.join("review.json").display() )); editor.handle(Input::Escape); assert!(!editor.word_wrap()); } - editor.execute("diff.review.overview"); + editor.execute("review.overview"); assert!(editor.word_wrap()); editor.handle(Input::Escape); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); editor.execute("word_wrap.toggle"); assert!(!editor.word_wrap()); editor.handle(Input::Escape); - editor.execute("diff.review.edit 1"); + editor.execute("review.edit 1"); assert!( !editor.word_wrap(), "reopening must retain the user's toggle"