yaml_edit/nodes/
comment.rs1use super::{fresh_token, SyntaxNode, SyntaxToken};
4use crate::lex::SyntaxKind;
5
6#[derive(Clone, PartialEq, Eq, Hash)]
12pub struct Comment(pub(crate) SyntaxToken);
13
14impl Comment {
15 pub(crate) fn cast(token: SyntaxToken) -> Option<Self> {
17 (token.kind() == SyntaxKind::COMMENT).then_some(Comment(token))
18 }
19
20 pub fn text(&self) -> &str {
24 self.0.text()
25 }
26
27 pub fn content(&self) -> &str {
32 let text = self.0.text();
33 let without_hash = text.strip_prefix('#').unwrap_or(text);
34 without_hash.strip_prefix(' ').unwrap_or(without_hash)
35 }
36
37 crate::nodes::ast_node_spans!("comment");
38
39 pub fn text_range(&self) -> rowan::TextRange {
43 self.0.text_range()
44 }
45
46 pub fn set_text(&self, text: &str) {
63 assert!(
64 text.starts_with('#'),
65 "comment text must start with '#', got {text:?}"
66 );
67 assert!(
68 !text.contains('\n') && !text.contains('\r'),
69 "comment text must not contain a newline, got {text:?}"
70 );
71 let parent = self
72 .0
73 .parent()
74 .expect("a comment token always has a parent node");
75 let index = self.0.index();
76 let new_token = fresh_token(SyntaxKind::COMMENT, text);
77 parent.splice_children(index..index + 1, vec![new_token.into()]);
78 }
79
80 pub fn set_content(&self, content: &str) {
97 self.set_text(&format!("# {content}"));
98 }
99
100 pub fn syntax(&self) -> &SyntaxToken {
102 &self.0
103 }
104}
105
106impl std::fmt::Debug for Comment {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.debug_struct("Comment")
109 .field("text", &self.text())
110 .finish()
111 }
112}
113
114impl std::fmt::Display for Comment {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}", self.0.text())
117 }
118}
119
120pub(crate) fn comments(node: &SyntaxNode) -> impl Iterator<Item = Comment> {
122 node.descendants_with_tokens()
123 .filter_map(|element| element.into_token())
124 .filter_map(Comment::cast)
125}
126
127#[cfg(test)]
128mod tests {
129 use crate::YamlFile;
130 use std::str::FromStr;
131
132 #[test]
133 fn iterates_comments_in_order() {
134 let text = "# header\nkey: value # trailing\n# footer\n";
135 let file = YamlFile::from_str(text).unwrap();
136 let texts: Vec<_> = file.comments().map(|c| c.text().to_string()).collect();
137 assert_eq!(texts, vec!["# header", "# trailing", "# footer"]);
138 }
139
140 #[test]
141 fn content_strips_hash_and_single_space() {
142 let file = YamlFile::from_str("#nospace\n# one space\n# two spaces\n").unwrap();
143 let contents: Vec<_> = file.comments().map(|c| c.content().to_string()).collect();
144 assert_eq!(contents, vec!["nospace", "one space", " two spaces"]);
145 }
146
147 #[test]
148 fn no_comments_yields_empty() {
149 let file = YamlFile::from_str("key: value\n").unwrap();
150 assert_eq!(file.comments().count(), 0);
151 }
152
153 #[test]
154 fn positions_are_one_indexed() {
155 let text = "key: value # trailing\n";
156 let file = YamlFile::from_str(text).unwrap();
157 let comment = file.comments().next().unwrap();
158 let start = comment.start_position(text);
159 assert_eq!(start.line, 1);
160 assert_eq!(start.column, 12);
161 }
162
163 #[test]
164 fn text_range_covers_full_comment() {
165 let text = "key: value # trailing\n";
166 let file = YamlFile::from_str(text).unwrap();
167 let comment = file.comments().next().unwrap();
168 let range = comment.text_range();
169 assert_eq!(&text[range], "# trailing");
170 }
171
172 #[test]
173 fn set_text_replaces_in_place() {
174 let file = YamlFile::from_str("key: value # teh value\n").unwrap();
175 let comment = file.comments().next().unwrap();
176 comment.set_text("# the value");
177 assert_eq!(file.to_string(), "key: value # the value\n");
178 }
179
180 #[test]
181 fn set_content_preserves_marker() {
182 let file = YamlFile::from_str("key: value #teh value\n").unwrap();
183 let comment = file.comments().next().unwrap();
184 comment.set_content("the value");
185 assert_eq!(file.to_string(), "key: value # the value\n");
186 }
187
188 #[test]
189 fn set_text_leaves_other_comments_intact() {
190 let file = YamlFile::from_str("# header\nkey: value # teh value\n# footer\n").unwrap();
191 let target = file.comments().nth(1).unwrap();
192 target.set_text("# the value");
193 let texts: Vec<_> = file.comments().map(|c| c.text().to_string()).collect();
194 assert_eq!(texts, vec!["# header", "# the value", "# footer"]);
195 }
196
197 #[test]
198 #[should_panic(expected = "must start with '#'")]
199 fn set_text_rejects_missing_hash() {
200 let file = YamlFile::from_str("key: value # comment\n").unwrap();
201 file.comments().next().unwrap().set_text("no hash");
202 }
203
204 #[test]
205 #[should_panic(expected = "must not contain a newline")]
206 fn set_text_rejects_newline() {
207 let file = YamlFile::from_str("key: value # comment\n").unwrap();
208 file.comments().next().unwrap().set_text("# a\n# b");
209 }
210}