1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Noyalib. All rights reserved.
//! Editing a flow collection that is wrapped across several lines.
//!
//! Hand-written config wraps flow collections one member per line, which
//! reads well and diffs well:
//!
//! ```yaml
//! ports: [
//! 80,
//! 443,
//! ]
//! ```
//!
//! Removing a member has to take the member's *line* with it, not just
//! the member. Leaving the indentation behind produces a line holding
//! nothing but spaces — the value still round-trips, so nothing is
//! corrupt, but `git diff --check`, `yamllint`'s `trailing-spaces` and
//! `editorconfig-checker` all reject that patch. A library whose promise
//! is that an edit touches only what the path names should not hand its
//! caller a diff their own lint refuses. That was #294, reported by
//! @zoosky.
//!
//! The rule is **"the member is alone on its line"**, not "the collection
//! is wrapped". Anything else still holding the line keeps it standing,
//! and this example shows both sides of that line.
//!
//! Run with: `cargo run --example cst_wrapped_flow_edit`
use noyalib::cst::parse_document;
/// Assert an edit's exact bytes, and that it left no line holding only
/// whitespace — the invariant the issue was about.
fn edit(label: &str, src: &str, path: &str, want: &str) {
let mut doc = parse_document(src).expect("parse");
doc.remove(path).expect("remove");
assert_eq!(doc.source(), want, "{label}");
for (n, line) in doc.source().lines().enumerate() {
assert!(
line.is_empty() || !line.trim().is_empty(),
"{label}: line {} is whitespace-only",
n + 1
);
}
println!(" {label}");
for line in want.lines() {
println!(" {}", line.replace(' ', "·"));
}
}
fn main() {
println!("The member owns its line — the line goes with it:\n");
edit(
"remove ports[0] from a wrapped sequence",
"ports: [\n 80,\n 443,\n]\n",
"ports[0]",
"ports: [\n 443,\n]\n",
);
edit(
"remove cfg.a from a wrapped mapping",
"cfg: {\n a: 1,\n b: 2,\n}\n",
"cfg.a",
"cfg: {\n b: 2,\n}\n",
);
println!("\nSomething else holds the line — it stays standing:\n");
edit(
"the opening indicator shares the line",
"ports: [80,\n 443,\n]\n",
"ports[0]",
"ports: [\n 443,\n]\n",
);
edit(
"a sibling member shares the line",
"ports: [\n 80, 443,\n 8080,\n]\n",
"ports[0]",
"ports: [\n 443,\n 8080,\n]\n",
);
edit(
"a trailing comment shares the line",
"ports: [\n 80, # why this port\n 443,\n]\n",
"ports[0]",
"ports: [\n # why this port\n 443,\n]\n",
);
edit(
"single-line collections are untouched",
"ports: [80, 443]\n",
"ports[0]",
"ports: [443]\n",
);
println!("\nThe block path has always answered this the same way:\n");
edit(
"a block sequence item takes its line too",
"ports:\n - 80\n - 443\n",
"ports[0]",
"ports:\n - 443\n",
);
println!("\n(· marks a space, so a stray indent would be visible above.)");
println!("All edits byte-exact, no whitespace-only lines.");
}