feat: fence review excerpts with language tags

This commit is contained in:
2026-10-25 00:13:35 +02:00
parent c1bff63111
commit 815fcccbe8
6 changed files with 94 additions and 5 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ The review CLI discovers the Git/Jujutsu checkout from `--repo` (default: curren
Use the persistent **Review ID** printed by `list`, not a list position. IDs are shared across repositories; listing filters them to the selected checkout, so its first review need not be ID 1. Human-readable listings include a command to show each review. The internal save revision is omitted from text but remains in JSON; it is not a Git/Jujutsu revision. `show all` is not supported: use `show ID --all` to include resolved comments in that review. Use the persistent **Review ID** printed by `list`, not a list position. IDs are shared across repositories; listing filters them to the selected checkout, so its first review need not be ID 1. Human-readable listings include a command to show each review. The internal save revision is omitted from text but remains in JSON; it is not a Git/Jujutsu revision. `show all` is not supported: use `show ID --all` to include resolved comments in that review.
`show` includes unresolved comments by default; `--all` includes resolved comments too. Human-readable output is the default. JSON includes the canonical repository root, storage review ID/revision, summary/verdict, total/resolved/unresolved counts, and comments with exact zero-based, end-exclusive grapheme anchors and selected source excerpts—not whole source files. `list --json` returns the repository and review metadata/counts without comments. Explicit editor JSON exports still contain complete anchor source text. `show` includes unresolved comments by default; `--all` includes resolved comments too. Human-readable output is the default, with source excerpts in language-tagged Markdown code blocks. JSON includes the canonical repository root, storage review ID/revision, summary/verdict, total/resolved/unresolved counts, and comments with exact zero-based, end-exclusive grapheme anchors and raw selected source excerpts—not whole source files or Markdown fences. `list --json` returns the repository and review metadata/counts without comments. Explicit editor JSON exports still contain complete anchor source text.
`list` and `show` are read-only. `delete REVIEW_ID COMMENT_ID` immediately commits deletion of one resolved or unresolved comment. `resolve REVIEW_ID COMMENT_ID` instead marks that comment resolved, preserving its body and original anchor. Repeating resolution succeeds without writing or advancing the review revision. Both commands print the updated review with its remaining unresolved comments (`--json` is supported); use `show REVIEW_ID --all` to verify resolved feedback. Both IDs are required; missing reviews/comments and repository mismatches fail without changing stored reviews or creating missing storage. Deleting the last comment keeps the review, summary, and verdict. Concurrent saves use the same revision-conflict protection as the editor; an already-open editor must reload before saving after a CLI change. Existing JSON exports are not modified. `list` and `show` are read-only. `delete REVIEW_ID COMMENT_ID` immediately commits deletion of one resolved or unresolved comment. `resolve REVIEW_ID COMMENT_ID` instead marks that comment resolved, preserving its body and original anchor. Repeating resolution succeeds without writing or advancing the review revision. Both commands print the updated review with its remaining unresolved comments (`--json` is supported); use `show REVIEW_ID --all` to verify resolved feedback. Both IDs are required; missing reviews/comments and repository mismatches fail without changing stored reviews or creating missing storage. Deleting the last comment keeps the review, summary, and verdict. Concurrent saves use the same revision-conflict protection as the editor; an already-open editor must reload before saving after a CLI change. Existing JSON exports are not modified.
+1 -1
View File
@@ -7,7 +7,7 @@
- if some code would be added, it would display in green with a plus - if some code would be added, it would display in green with a plus
- this would be the case for both panes - this would be the case for both panes
- probably something similar for three-pane mode would make sense. - probably something similar for three-pane mode would make sense.
- [ ] improve: code commenting format. example of comment in current format: - [x] improve: code commenting format. example of comment in current format:
<attachment> <attachment>
Comment 14 [unresolved] source/sema/resolve.hon (after) 22:9–22:19 @ working:a3b4159a340b3546e2108bc2dc0e7c302fc6d84b Comment 14 [unresolved] source/sema/resolve.hon (after) 22:9–22:19 @ working:a3b4159a340b3546e2108bc2dc0e7c302fc6d84b
[8]Builtin [8]Builtin
+2
View File
@@ -102,6 +102,8 @@ Ordinary-source review checks cover unsaved Unicode selections, line/empty-line
Review clipboard checks cover System/Separate routing with isolated providers, exact Unicode excerpts, resolved/stale comments across files, retained unsaved drafts, unchanged storage revisions, and empty/invalid/failed-copy preservation. Live PTY smoke imports a review, copies it, pastes into an empty buffer, and verifies the saved bytes exactly match CLI `show --all`; default CLI output still excludes resolved comments. Review clipboard checks cover System/Separate routing with isolated providers, exact Unicode excerpts, resolved/stale comments across files, retained unsaved drafts, unchanged storage revisions, and empty/invalid/failed-copy preservation. Live PTY smoke imports a review, copies it, pastes into an empty buffer, and verifies the saved bytes exactly match CLI `show --all`; default CLI output still excludes resolved comments.
Review text-format checks cover language-tagged excerpts, Honey's `hon` fence alias, special filenames and frozen shebang detection, unknown languages, Unicode, trailing-newline preservation, and nested backtick fences. Live PTY/CLI smoke imports `.hon` and nested Markdown comments, copies/pastes them into a saved buffer, compares its exact bytes with `show --all`, and verifies unresolved filtering, raw JSON excerpts, unchanged source bytes and an unchanged stored review revision.
SQLite review checks cover exact anchors across restarts and revision changes, repository isolation, independent connections and optimistic conflicts, locked/unwritable stores, incompatible schema preservation, relational constraints, full unsigned comment IDs, stable comment order, bounded/corrupt rows, rollback after partial comment insertion, private file permissions, and XDG/config path resolution. Schema 1 document storage is rejected without modification. Editor checks require explicit writes for comment/summary text, discard unsaved text on dismissal without retaining discarded undo history, prevent metadata autosaves from committing hidden drafts, persist verdicts/deletions, restore a sole review, choose among multiple reviews, and keep exports independent from autosaves. Test fixtures use isolated review directories. Live PTY smoke verifies that Escape leaves no new stored comment, `:w` persists exact text, and pane close discards a later edit; reopening and writing again preserves the saved body. SQLite review checks cover exact anchors across restarts and revision changes, repository isolation, independent connections and optimistic conflicts, locked/unwritable stores, incompatible schema preservation, relational constraints, full unsigned comment IDs, stable comment order, bounded/corrupt rows, rollback after partial comment insertion, private file permissions, and XDG/config path resolution. Schema 1 document storage is rejected without modification. Editor checks require explicit writes for comment/summary text, discard unsaved text on dismissal without retaining discarded undo history, prevent metadata autosaves from committing hidden drafts, persist verdicts/deletions, restore a sole review, choose among multiple reviews, and keep exports independent from autosaves. Test fixtures use isolated review directories. Live PTY smoke verifies that Escape leaves no new stored comment, `:w` persists exact text, and pane close discards a later edit; reopening and writing again preserves the saved body.
Manual-resolution checks cover immediate persistence, reopen/export/import, independent stale status, conflict-retained drafts, and incompatible version preservation. CLI checks cover filename-safe dispatch, Unicode excerpts, unresolved filtering/counts, absent storage, read-only access and permissions, symlink rejection, and incompatible schema preservation. Manual-resolution checks cover immediate persistence, reopen/export/import, independent stale status, conflict-retained drafts, and incompatible version preservation. CLI checks cover filename-safe dispatch, Unicode excerpts, unresolved filtering/counts, absent storage, read-only access and permissions, symlink rejection, and incompatible schema preservation.
+2
View File
@@ -513,6 +513,8 @@ Comment and summary text is committed only by **`:w`** or **`:review.save`**, wh
`: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.
CLI text output and `:review.copy` wrap each source excerpt in a Markdown code block, with its detected language tag (`hon` for `.hon`, `rust` for `.rs`, and so on). Detection uses the frozen source path and first-line shebang, not current disk contents; unknown languages use an untagged block. Fences grow beyond any backtick run in the excerpt, so nested Markdown cannot end the block early. Feedback follows outside the code block. Exact excerpt text, including existing final newlines, is preserved; a missing final newline gains only the separator required before the closing fence. JSON excerpts and stored anchors remain raw and unchanged.
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. 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.
Use `vamp --review resolve REVIEW_ID COMMENT_ID [--repo PATH] [--json]` after addressing and verifying feedback to mark it resolved without changing its body or original anchor. The command prints the updated review's remaining unresolved comments and full counts; `show REVIEW_ID --all` still includes the resolved original. Repeating resolution succeeds without a write or revision bump. Both IDs must be positive and refer to the selected repository; invalid targets and absent storage fail without creating or changing stored data. Like deletion, resolution rejects stale saves from an editor holding the previous review revision. Use `vamp --review resolve REVIEW_ID COMMENT_ID [--repo PATH] [--json]` after addressing and verifying feedback to mark it resolved without changing its body or original anchor. The command prints the updated review's remaining unresolved comments and full counts; `show REVIEW_ID --all` still includes the resolved original. Repeating resolution succeeds without a write or revision bump. Both IDs must be positive and refer to the selected repository; invalid targets and absent storage fail without creating or changing stored data. Like deletion, resolution rejects stale saves from an editor holding the previous review revision.
+21 -3
View File
@@ -279,9 +279,24 @@ impl Review {
.filter(|comment| all || !comment.resolved) .filter(|comment| all || !comment.resolved)
{ {
let anchor = &comment.anchor; let anchor = &comment.anchor;
let excerpt = anchor.excerpt();
let language =
match crate::syntax::language_id(Some(&anchor.path), anchor.content.lines().next())
{
Some("honey") => "hon",
language => language.unwrap_or(""),
};
let fence_width = 3.max(
excerpt
.split(|ch| ch != '`')
.map(str::len)
.max()
.unwrap_or(0)
+ 1,
);
writeln!( writeln!(
text, text,
"\nComment {} [{}] {} ({}) {}:{}–{}:{} @ {}\n{}\n{}", "\nComment {} [{}] {} ({}) {}:{}–{}:{} @ {}",
comment.id, comment.id,
if comment.resolved { if comment.resolved {
"resolved" "resolved"
@@ -298,10 +313,13 @@ impl Review {
anchor.end.row, anchor.end.row,
anchor.end.column, anchor.end.column,
anchor.revision, anchor.revision,
anchor.excerpt(),
comment.body,
) )
.unwrap(); .unwrap();
write!(text, "{:`<fence_width$}{language}\n{excerpt}", "").unwrap();
if !excerpt.ends_with('\n') {
text.push('\n');
}
writeln!(text, "{:`<fence_width$}\n{}", "", comment.body).unwrap();
} }
text text
} }
+67
View File
@@ -1,5 +1,72 @@
use super::*; use super::*;
#[test]
fn text_excerpts_use_detected_languages_and_cannot_close_their_own_fences() {
use crate::plugin::TextPosition;
for (path, content, start_row, expected) in [
(
"source.hon",
"[8]Builtin",
0,
"```hon\n[8]Builtin\n```\nFeedback **outside**\n",
),
(
"source.rs",
"let value = \"界\";\n",
0,
"```rust\nlet value = \"界\";\n```\nFeedback **outside**\n",
),
(
"Makefile",
"all:\n\techo ok",
0,
"```makefile\nall:\n\techo ok\n```\nFeedback **outside**\n",
),
(
"script",
"#!/usr/bin/env python3\nprint('界')\n",
1,
"```python\nprint('界')\n```\nFeedback **outside**\n",
),
(
"unknown.data",
"e\u{301}👩‍💻\n界",
0,
"```\ne\u{301}👩‍💻\n界\n```\nFeedback **outside**\n",
),
(
"example.md",
"```rust\n````\n```",
0,
"`````markdown\n```rust\n````\n```\n`````\nFeedback **outside**\n",
),
] {
let mut review = Review::default();
review.comments.push(Comment {
id: 1,
resolved: false,
body: "Feedback **outside**".into(),
anchor: Anchor {
revision: "snapshot".into(),
path: path.into(),
side: Side::After,
start: TextPosition {
row: start_row,
column: 0,
},
end: TextPosition {
row: content.split('\n').count() - 1,
column: content.rsplit('\n').next().unwrap().graphemes(true).count(),
},
content: content.into(),
},
});
let text = review.format_text(false);
assert!(text.ends_with(expected), "{path}: {text}");
}
}
struct Directory(PathBuf); struct Directory(PathBuf);
impl Drop for Directory { impl Drop for Directory {
fn drop(&mut self) { fn drop(&mut self) {