feat: copy diff review comments to configured clipboard

This commit is contained in:
Valdemar Lorenzen
2026-10-20 12:16:22 +02:00
parent eae3bdba16
commit a5a246db77
11 changed files with 248 additions and 81 deletions
+23 -77
View File
@@ -3,10 +3,9 @@ use std::{
ffi::{OsStr, OsString},
path::{Path, PathBuf},
};
use unicode_segmentation::UnicodeSegmentation;
use vamp::{
repository,
review::{Anchor, Review, Store},
review::{Review, Store},
ruby,
};
@@ -79,24 +78,6 @@ impl Request {
}
}
fn excerpt(anchor: &Anchor) -> String {
// Positions use extended graphemes, not bytes or Unicode scalar values.
let offset = |position: vamp::plugin::TextPosition| {
let line = anchor.content.split('\n').nth(position.row).unwrap_or("");
anchor
.content
.split('\n')
.take(position.row)
.map(|line| line.len() + 1)
.sum::<usize>()
+ line
.grapheme_indices(true)
.nth(position.column)
.map_or(line.len(), |(index, _)| index)
};
anchor.content[offset(anchor.start)..offset(anchor.end)].to_owned()
}
fn envelope(review: &Review, all: bool, comments: bool) -> Value {
let resolved = review
.comments
@@ -116,7 +97,7 @@ fn envelope(review: &Review, all: bool, comments: bool) -> Value {
json!({"id": comment.id, "body": comment.body, "resolved": comment.resolved,
"anchor": { "revision": comment.anchor.revision, "path": comment.anchor.path,
"side": comment.anchor.side, "start": comment.anchor.start, "end": comment.anchor.end },
"excerpt": excerpt(&comment.anchor)})
"excerpt": comment.anchor.excerpt()})
}).collect();
}
value
@@ -131,7 +112,7 @@ pub fn run(request: &Request, config: Option<&Path>) -> Result<(), String> {
let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
let root = repository::discover_root(request.repo.as_deref().unwrap_or(&cwd))?;
let store = Store::open_read_only(&directory)?;
let output = if let Some(id) = request.id {
let reviews = if let Some(id) = request.id {
let mut review = store
.as_ref()
.ok_or("Review does not exist in this repository")?
@@ -145,76 +126,41 @@ pub fn run(request: &Request, config: Option<&Path>) -> Result<(), String> {
review.comments.remove(index);
Store::open(&directory)?.save(&mut review)?;
}
envelope(&review, request.all, true)
vec![review]
} else {
let reviews = match store {
Some(store) => store
.list(&root)?
.into_iter()
.map(|review| envelope(&review, true, false))
.collect::<Vec<_>>(),
match store {
Some(store) => store.list(&root)?,
None => Vec::new(),
};
json!({"repository": root, "reviews": reviews})
}
};
if request.json {
let output = if request.id.is_some() {
envelope(&reviews[0], request.all, true)
} else {
let reviews = reviews
.iter()
.map(|review| envelope(review, true, false))
.collect::<Vec<_>>();
json!({"repository": root, "reviews": reviews})
};
println!(
"{}",
serde_json::to_string_pretty(&output).map_err(|error| error.to_string())?
);
} else if request.id.is_some() {
print!("{}", reviews[0].format_text(request.all));
} else {
println!("Repository: {}", root.display());
if let Some(reviews) = output["reviews"].as_array() {
if reviews.is_empty() {
println!("No stored reviews.");
}
for review in reviews {
print_review(review);
}
} else {
print_review(&output);
for comment in output["comments"].as_array().unwrap() {
let anchor = &comment["anchor"];
println!(
"\nComment {} [{}] {} ({}) {}:{}–{}:{} @ {}",
comment["id"],
if comment["resolved"] == true {
"resolved"
} else {
"unresolved"
},
anchor["path"].as_str().unwrap(),
anchor["side"].as_str().unwrap(),
anchor["start"]["row"],
anchor["start"]["column"],
anchor["end"]["row"],
anchor["end"]["column"],
anchor["revision"].as_str().unwrap()
);
println!(
"{}\n{}",
comment["excerpt"].as_str().unwrap(),
comment["body"].as_str().unwrap()
);
}
if reviews.is_empty() {
println!("No stored reviews.");
}
for review in reviews {
print!("{}", review.summary_text());
}
}
Ok(())
}
fn print_review(review: &Value) {
println!(
"Review {} (revision {}): {}\n Verdict: {}; {} unresolved, {} resolved ({} total)",
review["id"],
review["revision"],
review["summary"].as_str().unwrap(),
review["verdict"].as_str().unwrap_or("none"),
review["counts"]["unresolved"],
review["counts"]["resolved"],
review["counts"]["total"]
);
}
#[cfg(test)]
#[path = "../tests/unit/review_cli.rs"]
mod tests;