feat: configure personal pull request review indicators
This commit is contained in:
@@ -433,6 +433,7 @@ fn pull_page(closed: bool, rows: &[(u64, &str, &str)], has_more: bool) -> Outcom
|
||||
},
|
||||
),
|
||||
date.into(),
|
||||
PullRequestReviewState::None,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
@@ -440,6 +441,51 @@ fn pull_page(closed: bool, rows: &[(u64, &str, &str)], has_more: bool) -> Outcom
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_review_refresh_preserves_identity_sorting_and_raw_search_labels() {
|
||||
let mut state = DiffState::pull_requests("origin".into());
|
||||
state.take_request();
|
||||
state.apply_result(
|
||||
1,
|
||||
Ok(pull_page(
|
||||
false,
|
||||
&[(1, "First", "1"), (2, "Second", "2")],
|
||||
true,
|
||||
)),
|
||||
);
|
||||
state.handle(Input::End);
|
||||
for review_state in [
|
||||
PullRequestReviewState::Approved,
|
||||
PullRequestReviewState::Requested,
|
||||
PullRequestReviewState::None,
|
||||
] {
|
||||
let mut page = pull_page(false, &[(1, "First updated", "3")], true);
|
||||
if let Outcome::PullRequestPage {
|
||||
result: Ok(page), ..
|
||||
} = &mut page
|
||||
{
|
||||
page.entries[0].2 = review_state;
|
||||
}
|
||||
state.apply_result(1, Ok(page));
|
||||
let selected = state.entries().nth(state.selected).unwrap();
|
||||
assert_eq!(selected.label, "#1 First updated");
|
||||
assert_eq!(state.pull_request_review_state(selected), review_state);
|
||||
assert_eq!(state.entries().count(), 2);
|
||||
assert_eq!(state.entries().next().unwrap().target, selected.target);
|
||||
}
|
||||
state.handle(Input::Char('🙏'));
|
||||
assert_eq!(state.entries().count(), 0);
|
||||
state.handle(Input::Backspace);
|
||||
for ch in "#1 First updated".chars() {
|
||||
state.handle(Input::Char(ch));
|
||||
}
|
||||
assert_eq!(state.entries().count(), 1);
|
||||
assert_eq!(
|
||||
state.pull_request_review_state(&catalog().entries[0]),
|
||||
PullRequestReviewState::None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_categories_keep_query_and_select_loaded_rows_before_completion() {
|
||||
let mut state = DiffState::pull_requests("origin".into());
|
||||
|
||||
+309
-1
@@ -174,7 +174,9 @@ fn unavailable_viewer_or_metadata_never_fabricates_personal_approval() {
|
||||
let forge = parse(url, None).unwrap();
|
||||
for unavailable_viewer in [true, false] {
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"number":7,"iid":7,"additions":0,"deletions":0})],
|
||||
&[json!({"number":7,"iid":7,"additions":0,"deletions":0,
|
||||
"requested_reviewers":[{"login":"viewer","id":42}],
|
||||
"reviewers":[{"username":"viewer"}]})],
|
||||
&mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return if unavailable_viewer {
|
||||
@@ -194,6 +196,8 @@ fn unavailable_viewer_or_metadata_never_fabricates_personal_approval() {
|
||||
},
|
||||
);
|
||||
assert!(metadata.values().all(|details| !details.approved));
|
||||
assert!(metadata.values().all(|details| details.review_requested
|
||||
== (provider == Provider::Github && !unavailable_viewer)));
|
||||
if provider == Provider::Gitea {
|
||||
assert_eq!(metadata[&7].size, Some((0, 0)));
|
||||
}
|
||||
@@ -201,6 +205,310 @@ fn unavailable_viewer_or_metadata_never_fabricates_personal_approval() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_outstanding_requests_use_list_users_independently_of_review_metadata() {
|
||||
let forge = parse("https://github.com/owner/repo", None).unwrap();
|
||||
for (reviewers, expected) in [
|
||||
(json!([{"login":"VIEWER"}]), true), // Direct or re-requested after approval.
|
||||
(json!([]), false), // Completed or removed requests are absent.
|
||||
(json!([{"login":"another-user"}]), false),
|
||||
(Value::Null, false),
|
||||
] {
|
||||
for denied in [false, true] {
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"number":7,"requested_reviewers":reviewers,
|
||||
"requested_teams":[{"slug":"viewer"}],"additions":3,"deletions":4})],
|
||||
&mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"login":"viewer"}));
|
||||
}
|
||||
assert!(body.is_some());
|
||||
if denied {
|
||||
return Err("GraphQL denied".into());
|
||||
}
|
||||
Ok(
|
||||
json!({"data":{"repository":{"p7":{"additions":3,"deletions":4,
|
||||
"reviews":{"nodes":[{"state":"APPROVED","author":{"login":"viewer"}}]}}}}}),
|
||||
)
|
||||
},
|
||||
);
|
||||
assert_eq!(metadata[&7].review_requested, expected);
|
||||
assert_eq!(metadata[&7].approved, !denied);
|
||||
assert_eq!(metadata[&7].size, Some((3, 4)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_outstanding_requests_require_personal_unfinished_review_and_follow_pages() {
|
||||
let forge = parse("https://gitlab.com/group/repo", None).unwrap();
|
||||
let mut values: Vec<_> = (1..=10)
|
||||
.map(|id| json!({"iid":id,"reviewers":[{"username":"viewer"}]}))
|
||||
.collect();
|
||||
values[8]["reviewers"] = json!([]); // Removed reviewer.
|
||||
values[9]["reviewers"] = json!([{"username":"another-user"}]);
|
||||
let mut request_reads = 0;
|
||||
let metadata = forge.metadata_with(&values, &mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"username":"viewer"}));
|
||||
}
|
||||
let body: Value = serde_json::from_str(body.unwrap()).unwrap();
|
||||
let query = body["query"].as_str().unwrap();
|
||||
assert!(!query.contains("mutation"));
|
||||
if !query.contains("reviewState") {
|
||||
return Ok(json!({"data":{"project":{
|
||||
"p1":{"diffStatsSummary":{"additions":5,"deletions":6}},
|
||||
"approved":{"nodes":[{"iid":"1"},{"iid":"4"}],"pageInfo":{"hasNextPage":false}}
|
||||
}}}));
|
||||
}
|
||||
request_reads += 1;
|
||||
assert!(query.contains("reviewers(first:20"));
|
||||
assert!(!query.contains("p9:") && !query.contains("p10:"));
|
||||
if query.contains("after:\"next\"") {
|
||||
return Ok(json!({"data":{"project":{"p2":{"reviewers":{
|
||||
"nodes":[{"username":"viewer","mergeRequestInteraction":{"reviewState":"REVIEW_STARTED"}}],
|
||||
"pageInfo":{"hasNextPage":false}
|
||||
}}}}}));
|
||||
}
|
||||
let mut entries = serde_json::Map::new();
|
||||
for (id, state) in [(1, "UNREVIEWED"), (3, "REVIEWED"), (4, "APPROVED"),
|
||||
(5, "REQUESTED_CHANGES"), (6, "UNAPPROVED"), (7, ""), (8, "UNKNOWN")] {
|
||||
entries.insert(format!("p{id}"), json!({"reviewers":{
|
||||
"nodes":[{"username":"viewer","mergeRequestInteraction":{"reviewState":state}}],
|
||||
"pageInfo":{"hasNextPage":false}
|
||||
}}));
|
||||
}
|
||||
entries.insert("p2".into(), json!({"reviewers":{
|
||||
"nodes":[{"username":"another-user","mergeRequestInteraction":{"reviewState":"UNREVIEWED"}}],
|
||||
"pageInfo":{"hasNextPage":true,"endCursor":"next"}
|
||||
}}));
|
||||
Ok(json!({"data":{"project":entries}}))
|
||||
});
|
||||
assert_eq!(request_reads, 2);
|
||||
assert_eq!(
|
||||
metadata
|
||||
.iter()
|
||||
.filter_map(|(id, details)| details.review_requested.then_some(*id))
|
||||
.collect::<Vec<_>>(),
|
||||
[1, 2]
|
||||
);
|
||||
assert!(metadata[&1].approved && metadata[&4].approved);
|
||||
assert_eq!(metadata[&1].size, Some((5, 6)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_request_failures_preserve_approval_and_statistics() {
|
||||
let forge = parse("https://gitlab.com/group/repo", None).unwrap();
|
||||
for denied in [
|
||||
Err("review states denied".into()),
|
||||
Ok(json!({"data":{"project":{
|
||||
"p7":{"reviewers":null}
|
||||
}}})),
|
||||
Ok(json!({"data":{"project":{"p7":{"reviewers":{"nodes":[
|
||||
{"username":"viewer","mergeRequestInteraction":{"reviewState":null}}
|
||||
]}}}}})),
|
||||
] {
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"iid":7,"reviewers":[{"username":"viewer"}]})],
|
||||
&mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"username":"viewer"}));
|
||||
}
|
||||
let body: Value = serde_json::from_str(body.unwrap()).unwrap();
|
||||
if body["query"].as_str().unwrap().contains("reviewState") {
|
||||
return denied.clone();
|
||||
}
|
||||
Ok(json!({"data":{"project":{
|
||||
"p7":{"diffStatsSummary":{"additions":0,"deletions":1}},
|
||||
"approved":{"nodes":[{"iid":"7"}],"pageInfo":{"hasNextPage":false}}
|
||||
}}}))
|
||||
},
|
||||
);
|
||||
assert!(!metadata[&7].review_requested);
|
||||
assert!(metadata[&7].approved);
|
||||
assert_eq!(metadata[&7].size, Some((0, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_request_batches_cover_fifty_rows_without_exceeding_complexity_limit() {
|
||||
let forge = parse("https://gitlab.com/group/repo", None).unwrap();
|
||||
let values: Vec<_> = (1..=50)
|
||||
.map(|id| {
|
||||
json!({
|
||||
"iid":id,"reviewers":[{"username":"viewer"}]
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut batches = 0;
|
||||
let metadata = forge.metadata_with(&values, &mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"username":"viewer"}));
|
||||
}
|
||||
let body: Value = serde_json::from_str(body.unwrap()).unwrap();
|
||||
let query = body["query"].as_str().unwrap();
|
||||
if !query.contains("reviewState") {
|
||||
return Err("independent metadata unavailable".into());
|
||||
}
|
||||
batches += 1;
|
||||
assert_eq!(query.matches("reviewers(first:20)").count(), 10);
|
||||
let entries: serde_json::Map<_, _> = (1..=50)
|
||||
.filter(|id| query.contains(&format!("p{id}:mergeRequest")))
|
||||
.map(|id| (format!("p{id}"), json!({"reviewers":{
|
||||
"nodes":[{"username":"viewer","mergeRequestInteraction":{"reviewState":"UNREVIEWED"}}],
|
||||
"pageInfo":{"hasNextPage":false}
|
||||
}}))).collect();
|
||||
Ok(json!({"data":{"project":entries}}))
|
||||
});
|
||||
assert_eq!(batches, 5);
|
||||
assert_eq!(metadata.len(), 50);
|
||||
assert!(metadata.values().all(|details| details.review_requested));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitea_requests_ignore_list_assignments_and_use_current_direct_review_rows() {
|
||||
let forge = parse("https://codeberg.org/owner/repo", None).unwrap();
|
||||
let own = |id, state: &str| {
|
||||
json!({
|
||||
"id":id,"user":{"id":42},"team":null,"state":state,"dismissed":false
|
||||
})
|
||||
};
|
||||
for (reviews, requested, approved) in [
|
||||
(json!([own(10, "REQUEST_REVIEW")]), true, false),
|
||||
(json!([own(10, "APPROVED")]), false, true), // Completed or removed request row.
|
||||
(json!([own(10, "COMMENT")]), false, false),
|
||||
(json!([own(10, "REQUEST_CHANGES")]), false, false),
|
||||
(
|
||||
json!([own(10, "APPROVED"), own(20, "REQUEST_REVIEW")]),
|
||||
true,
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!([
|
||||
own(20, "REQUEST_REVIEW"),
|
||||
own(10, "APPROVED"),
|
||||
own(30, "PENDING")
|
||||
]),
|
||||
true,
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!([own(10, "REQUEST_REVIEW"), own(20, "COMMENT")]),
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
json!([own(10, "REQUEST_REVIEW"), own(20, "APPROVED")]),
|
||||
false,
|
||||
true,
|
||||
),
|
||||
(
|
||||
json!([{"id":10,"user":{"id":7},"state":"REQUEST_REVIEW","dismissed":false}]),
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
json!([{"id":10,"user":{"id":42},"team":{"id":1},"state":"REQUEST_REVIEW","dismissed":false}]),
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
json!([{"id":20,"user":{"id":42},"state":"REQUEST_REVIEW"},
|
||||
own(10, "APPROVED")]),
|
||||
false,
|
||||
true,
|
||||
),
|
||||
(json!([]), false, false),
|
||||
] {
|
||||
let mut pages = vec![reviews, json!([])].into_iter();
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"number":7,"additions":1,"deletions":2,"requested_reviewers":[{"id":42}]})],
|
||||
&mut |url, body| {
|
||||
assert!(body.is_none());
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"id":42}));
|
||||
}
|
||||
pages.next().ok_or_else(|| "unexpected page".into())
|
||||
},
|
||||
);
|
||||
assert_eq!(metadata[&7].review_requested, requested);
|
||||
assert_eq!(metadata[&7].approved, approved);
|
||||
assert_eq!(metadata[&7].size, Some((1, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitea_incomplete_request_pages_do_not_fabricate_requests_or_lose_list_statistics() {
|
||||
let forge = parse("https://codeberg.org/owner/repo", None).unwrap();
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"number":7,"additions":0,"deletions":2})],
|
||||
&mut |url, _| {
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"id":42}));
|
||||
}
|
||||
if url.ends_with("page=1") {
|
||||
return Ok(json!([{"id":10,"user":{"id":42},"team":null,
|
||||
"state":"REQUEST_REVIEW","dismissed":false}]));
|
||||
}
|
||||
Err("remaining reviews denied".into())
|
||||
},
|
||||
);
|
||||
assert!(!metadata[&7].review_requested);
|
||||
assert_eq!(metadata[&7].size, Some((0, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summaries_keep_outstanding_requests_and_approvals_independent() {
|
||||
for (url, provider) in [
|
||||
("https://github.com/owner/repo", Provider::Github),
|
||||
("https://gitlab.com/owner/repo", Provider::Gitlab),
|
||||
("https://codeberg.org/owner/repo", Provider::Gitea),
|
||||
] {
|
||||
let forge = parse(url, None).unwrap();
|
||||
let page = forge.page_with(false, 1, |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Ok(json!({"id":42,"login":"viewer","username":"viewer"}));
|
||||
}
|
||||
if let Some(body) = body {
|
||||
let body: Value = serde_json::from_str(body).unwrap();
|
||||
if body["query"].as_str().unwrap().contains("reviewState") {
|
||||
return Ok(json!({"data":{"project":{"p7":{"reviewers":{
|
||||
"nodes":[{"username":"viewer","mergeRequestInteraction":{"reviewState":"UNREVIEWED"}}],
|
||||
"pageInfo":{"hasNextPage":false}
|
||||
}}}}}));
|
||||
}
|
||||
return if provider == Provider::Github {
|
||||
Ok(json!({"data":{"repository":{"p7":{"additions":1,"deletions":2,
|
||||
"reviews":{"nodes":[{"state":"APPROVED","author":{"login":"viewer"}}]}}}}}))
|
||||
} else {
|
||||
Ok(json!({"data":{"project":{
|
||||
"p7":{"diffStatsSummary":{"additions":1,"deletions":2}},
|
||||
"approved":{"nodes":[{"iid":"7"}],"pageInfo":{"hasNextPage":false}}
|
||||
}}}))
|
||||
};
|
||||
}
|
||||
if url.contains("/7/reviews?") {
|
||||
return Ok(if url.ends_with("page=1") {
|
||||
json!([
|
||||
{"id":10,"user":{"id":42},"state":"APPROVED","dismissed":false},
|
||||
{"id":20,"user":{"id":42},"state":"REQUEST_REVIEW","dismissed":false}
|
||||
])
|
||||
} else {
|
||||
json!([])
|
||||
});
|
||||
}
|
||||
Ok(json!([{"number":7,"iid":7,"title":"Request again","created_at":"2024-01-01T00:00:00Z",
|
||||
"state":if provider == Provider::Gitlab { "opened" } else { "open" },
|
||||
"merged":false,"merged_at":null,"user":{"login":"author"},"author":{"username":"author"},
|
||||
"requested_reviewers":[{"login":"viewer","id":42}],"reviewers":[{"username":"viewer"}],
|
||||
"additions":1,"deletions":2}]))
|
||||
}).unwrap();
|
||||
assert_eq!(page.entries.len(), 1);
|
||||
assert!(page.entries[0].approved && page.entries[0].review_requested);
|
||||
assert!(page.entries[0].description.contains("+1 -2"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_summaries_keep_open_closed_and_merged_states_distinct() {
|
||||
for provider in [Provider::Github, Provider::Gitlab, Provider::Gitea] {
|
||||
|
||||
+83
-4
@@ -106,7 +106,7 @@ fn review_directory_resolves_config_paths_and_keeps_session_startup_lazy() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_directory_wire_field_is_required_nullable_and_validated() {
|
||||
fn review_directory_and_review_symbol_wire_fields_are_required_and_validated() {
|
||||
let mut frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {}, "bindings": [],
|
||||
"review_directory": null, "leader": "space",
|
||||
@@ -119,6 +119,7 @@ fn review_directory_wire_field_is_required_nullable_and_validated() {
|
||||
"diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"resolve_history_layout": "columns",
|
||||
"pull_request_review_requested_symbol": "🙏", "pull_request_approved_symbol": "👍",
|
||||
"diff_follow_working_copy": true
|
||||
});
|
||||
for value in [serde_json::Value::Null, serde_json::json!("reviews")] {
|
||||
@@ -136,10 +137,83 @@ fn review_directory_wire_field_is_required_nullable_and_validated() {
|
||||
frame["review_directory"] = value;
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
|
||||
}
|
||||
frame["review_directory"] = serde_json::Value::Null;
|
||||
for field in [
|
||||
"pull_request_review_requested_symbol",
|
||||
"pull_request_approved_symbol",
|
||||
] {
|
||||
for value in ["", "👩🚀 R", "ok"] {
|
||||
frame[field] = serde_json::json!(value);
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_ok());
|
||||
}
|
||||
for value in [
|
||||
serde_json::json!(null),
|
||||
serde_json::json!(false),
|
||||
serde_json::json!(7),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
serde_json::json!("\0"),
|
||||
serde_json::json!("\n"),
|
||||
serde_json::json!("\t"),
|
||||
serde_json::json!("\u{7f}"),
|
||||
serde_json::json!("\u{85}"),
|
||||
] {
|
||||
frame[field] = value;
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
|
||||
}
|
||||
frame.as_object_mut().unwrap().remove(field);
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
|
||||
frame[field] = serde_json::json!("");
|
||||
}
|
||||
frame.as_object_mut().unwrap().remove("review_directory");
|
||||
assert!(serde_json::from_value::<Response>(frame).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_symbols_load_defaults_custom_and_empty_and_reject_invalid_ruby_values() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file("config.rb", "Vamp.configure { |_| }");
|
||||
let config = load_config(&path).unwrap();
|
||||
assert_eq!(config.pull_request_review_requested_symbol, "🙏");
|
||||
assert_eq!(config.pull_request_approved_symbol, "👍");
|
||||
for symbol in ["", "👩🚀 R", "ok"] {
|
||||
let value = serde_json::to_string(symbol).unwrap();
|
||||
let path = fixture.file("config.rb", &format!(
|
||||
"Vamp.configure {{ |c| c.pull_request_review_requested_symbol = {value}; c.pull_request_approved_symbol = {value} }}"
|
||||
));
|
||||
let config = load_config(&path).unwrap();
|
||||
assert_eq!(config.pull_request_review_requested_symbol, symbol);
|
||||
assert_eq!(config.pull_request_approved_symbol, symbol);
|
||||
}
|
||||
for field in [
|
||||
"pull_request_review_requested_symbol",
|
||||
"pull_request_approved_symbol",
|
||||
] {
|
||||
for value in [
|
||||
"nil",
|
||||
"false",
|
||||
"7",
|
||||
"[]",
|
||||
"{}",
|
||||
":ok",
|
||||
r#""\n""#,
|
||||
r#""\t""#,
|
||||
r#""\0""#,
|
||||
r#""\u007f""#,
|
||||
r#""\u0085""#,
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |c| c.{field} = {value} }}"),
|
||||
);
|
||||
assert!(
|
||||
load_config(&path).unwrap_err().contains(field),
|
||||
"{field}: {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_spaces_follow_active_language_and_preserve_native_edit_semantics() {
|
||||
let fixture = Fixture::new();
|
||||
@@ -544,9 +618,9 @@ fn config_leader_defaults_custom_keys_validation_and_required_wire_field() {
|
||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||
}
|
||||
for frame in [
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true,"resolve_history_layout":"columns"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true,"resolve_history_layout":"columns"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true,"resolve_history_layout":"columns"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true,"pull_request_review_requested_symbol":"🙏","pull_request_approved_symbol":"👍","resolve_history_layout":"columns"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true,"pull_request_review_requested_symbol":"🙏","pull_request_approved_symbol":"👍","resolve_history_layout":"columns"}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"review_directory":null,"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true,"pull_request_review_requested_symbol":"🙏","pull_request_approved_symbol":"👍","resolve_history_layout":"columns"}"#,
|
||||
] {
|
||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||
}
|
||||
@@ -632,6 +706,7 @@ end
|
||||
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"resolve_history_layout": "columns",
|
||||
"pull_request_review_requested_symbol": "🙏", "pull_request_approved_symbol": "👍",
|
||||
"diff_follow_working_copy": true
|
||||
});
|
||||
assert!(serde_json::from_value::<Response>(frame.clone()).is_ok());
|
||||
@@ -773,6 +848,7 @@ end"#,
|
||||
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"resolve_history_layout": "columns",
|
||||
"pull_request_review_requested_symbol": "🙏", "pull_request_approved_symbol": "👍",
|
||||
"diff_follow_working_copy": true
|
||||
});
|
||||
for value in ["all", "leading"] {
|
||||
@@ -1036,6 +1112,7 @@ fn config_diff_excerpt_counts_accept_custom_values_and_reject_invalid_values() {
|
||||
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"resolve_history_layout": "columns",
|
||||
"pull_request_review_requested_symbol": "🙏", "pull_request_approved_symbol": "👍",
|
||||
"diff_follow_working_copy": true
|
||||
});
|
||||
for setting in ["diff_context_lines", "diff_expand_lines"] {
|
||||
@@ -1148,6 +1225,7 @@ fn config_scroll_distance_accepts_nil_and_counts_and_rejects_invalid_values() {
|
||||
"diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"resolve_history_layout": "columns",
|
||||
"pull_request_review_requested_symbol": "🙏", "pull_request_approved_symbol": "👍",
|
||||
"diff_follow_working_copy": true, "scroll_distance": 0
|
||||
});
|
||||
for distance in [None, Some(0), Some(7), Some(usize::MAX)] {
|
||||
@@ -1582,6 +1660,7 @@ fn lsp_wire_configuration_is_resolved_after_decoding() {
|
||||
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"resolve_history_layout": "columns",
|
||||
"pull_request_review_requested_symbol": "🙏", "pull_request_approved_symbol": "👍",
|
||||
"diff_follow_working_copy": true
|
||||
});
|
||||
let Response::Config(config) = serde_json::from_value::<Response>(frame.clone()).unwrap()
|
||||
|
||||
@@ -1481,6 +1481,23 @@ end
|
||||
assert_eq!(session.editor().lines(), &["a!bc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_pull_request_symbols_reach_editor() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.file("buffer.txt", "abc");
|
||||
let session = fixture.open(
|
||||
r#"Vamp.configure do |config|
|
||||
config.pull_request_review_requested_symbol = "👩🚀 review"
|
||||
config.pull_request_approved_symbol = ""
|
||||
end"#,
|
||||
);
|
||||
assert_eq!(
|
||||
session.editor().pull_request_review_requested_symbol,
|
||||
"👩🚀 review"
|
||||
);
|
||||
assert_eq!(session.editor().pull_request_approved_symbol, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_display_preferences_survive_reopening() {
|
||||
use crate::{diff::ViewMode, panes::SplitAxis};
|
||||
|
||||
+119
-4
@@ -3515,6 +3515,7 @@ fn pull_request_picker_renders_all_rows_without_a_pinned_offset() {
|
||||
},
|
||||
},
|
||||
format!("2026-01-{:02}T00:00:00Z", 20 - index),
|
||||
crate::diff::PullRequestReviewState::None,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
@@ -3526,17 +3527,17 @@ fn pull_request_picker_renders_all_rows_without_a_pinned_offset() {
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("Pull requests"));
|
||||
assert!(text.contains("Open") && text.contains("Closed") && text.contains("All"));
|
||||
assert!(text.contains("> #00 Pull request"));
|
||||
assert!(text.contains("> #00 Pull request"));
|
||||
assert!(!text.contains("pinned"));
|
||||
assert!(text.contains("[partial]"));
|
||||
diff.handle(Input::End);
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("> #19 Pull request"));
|
||||
assert!(text.contains("> #19 Pull request"));
|
||||
assert!(!text.contains("#00 Pull request"));
|
||||
diff.handle(Input::Home);
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
assert!(screen_text(&terminal).contains("> #00 Pull request"));
|
||||
assert!(screen_text(&terminal).contains("> #00 Pull request"));
|
||||
diff.handle(Input::Char('\t'));
|
||||
diff.apply_result(
|
||||
2,
|
||||
@@ -3555,10 +3556,124 @@ fn pull_request_picker_renders_all_rows_without_a_pinned_offset() {
|
||||
diff.handle(Input::Char('\t'));
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("> #01 Pull request"));
|
||||
assert!(text.contains("> #01 Pull request"));
|
||||
assert!(!text.contains("#00 Pull request") && !text.contains("#19 Pull request"));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn pull_request_review_symbols_use_terminal_cells_and_keep_the_selection_arrow_independent() {
|
||||
use crate::diff::{
|
||||
Category, Outcome, PullRequestPage, PullRequestReviewState, RepoKind, Revision,
|
||||
RevisionTarget,
|
||||
};
|
||||
let mut diff = DiffState::pull_requests("origin".into());
|
||||
diff.take_request();
|
||||
diff.apply_result(
|
||||
1,
|
||||
Ok(Outcome::PullRequestPage {
|
||||
closed: false,
|
||||
result: Ok(PullRequestPage {
|
||||
kind: RepoKind::Git,
|
||||
remote: "origin".into(),
|
||||
has_more: false,
|
||||
entries: [
|
||||
PullRequestReviewState::Requested,
|
||||
PullRequestReviewState::Approved,
|
||||
PullRequestReviewState::None,
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, state)| {
|
||||
(
|
||||
Revision {
|
||||
target: RevisionTarget::PullRequest {
|
||||
remote: "origin".into(),
|
||||
number: index as u64,
|
||||
},
|
||||
label: format!("#{index} title"),
|
||||
description: format!("metadata{index}"),
|
||||
category: Category::Open,
|
||||
},
|
||||
(3 - index).to_string(),
|
||||
state,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
for (requested, approved, with_editor) in [
|
||||
("🙏", "👍", false),
|
||||
("🙏", "👍", true),
|
||||
("👩🚀 R", "界", true),
|
||||
("ask", "ok", true),
|
||||
("", "👍", true),
|
||||
("🙏", "", true),
|
||||
("", "", true),
|
||||
] {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.pull_request_review_requested_symbol = requested.into();
|
||||
editor.pull_request_approved_symbol = approved.into();
|
||||
let slot = requested.width().max(approved.width());
|
||||
let slot = slot + usize::from(slot > 0);
|
||||
for width in [100, 44] {
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, 24)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
panels.begin_frame();
|
||||
draw_diff(
|
||||
frame,
|
||||
&diff,
|
||||
frame.area(),
|
||||
"↑/↓",
|
||||
"h/j/k/l",
|
||||
&mut panels,
|
||||
with_editor.then_some(&editor),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let mut number_column = None;
|
||||
for (index, symbol) in [requested, approved, ""].into_iter().enumerate() {
|
||||
let (x, y) = (0..24)
|
||||
.find_map(|y| {
|
||||
(0..width).find_map(|x| {
|
||||
(buffer[(x, y)].symbol() == "#"
|
||||
&& buffer[(x + 1, y)].symbol() == index.to_string())
|
||||
.then_some((x, y))
|
||||
})
|
||||
})
|
||||
.expect("all review states render");
|
||||
assert_eq!(*number_column.get_or_insert(x), x);
|
||||
let arrow_x = x - (2 + slot) as u16;
|
||||
assert_eq!(
|
||||
buffer[(arrow_x, y)].symbol(),
|
||||
if index == 0 { ">" } else { " " }
|
||||
);
|
||||
if !symbol.is_empty() {
|
||||
assert_eq!(
|
||||
buffer[(arrow_x + 2, y)].symbol(),
|
||||
symbol.graphemes(true).next().unwrap()
|
||||
);
|
||||
} else {
|
||||
for blank in arrow_x + 2..x {
|
||||
assert_eq!(buffer[(blank, y)].symbol(), " ");
|
||||
}
|
||||
}
|
||||
if width == 100 {
|
||||
assert_eq!(
|
||||
buffer[(x, y + 1)].symbol(),
|
||||
"m",
|
||||
"metadata aligns with #number"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(!screen_text(&terminal).contains('✓'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_excerpts_render_original_lines_and_expand_without_losing_changes() {
|
||||
|
||||
Reference in New Issue
Block a user