yaml_edit/nodes/comment.rs
1//! Comment trivia.
2
3use super::{fresh_token, SyntaxNode, SyntaxToken};
4use crate::lex::SyntaxKind;
5
6/// A single comment token in a YAML document.
7///
8/// Comments are trivia: they are preserved in the syntax tree but do not affect
9/// the parsed value. Use [`YamlFile::comments`](crate::YamlFile::comments) or
10/// [`Document::comments`](crate::Document::comments) to iterate over them.
11#[derive(Clone, PartialEq, Eq, Hash)]
12pub struct Comment(pub(crate) SyntaxToken);
13
14impl Comment {
15 /// Wrap a syntax token as a comment, if it is a `COMMENT` token.
16 pub(crate) fn cast(token: SyntaxToken) -> Option<Self> {
17 (token.kind() == SyntaxKind::COMMENT).then_some(Comment(token))
18 }
19
20 /// The full comment text, including the leading `#`.
21 ///
22 /// For `# hello` this returns `"# hello"`.
23 pub fn text(&self) -> &str {
24 self.0.text()
25 }
26
27 /// The comment content with the leading `#` and a single optional space removed.
28 ///
29 /// For `# hello` this returns `"hello"`; for `#hello` it returns `"hello"`.
30 /// Trailing whitespace is left intact.
31 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 /// Get the text range of this comment as a rowan [`TextRange`](rowan::TextRange).
40 ///
41 /// The range spans the full comment text, including the leading `#`.
42 pub fn text_range(&self) -> rowan::TextRange {
43 self.0.text_range()
44 }
45
46 /// Replace the full comment text, including the leading `#`.
47 ///
48 /// The replacement must be a single valid comment token starting with `#`
49 /// and containing no newline; otherwise this panics, since writing it back
50 /// would corrupt the tree.
51 ///
52 /// # Example
53 /// ```
54 /// use yaml_edit::Document;
55 /// use std::str::FromStr;
56 ///
57 /// let doc = Document::from_str("key: value # teh value\n").unwrap();
58 /// let comment = doc.comments().next().unwrap();
59 /// comment.set_text("# the value");
60 /// assert_eq!(doc.to_string(), "key: value # the value\n");
61 /// ```
62 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 super::replace_children(&parent, index..index + 1, vec![new_token.into()]);
78 }
79
80 /// Replace the comment content, preserving the leading `# ` marker.
81 ///
82 /// This is the counterpart to [`content`](Self::content): it sets the text
83 /// after the `#` and a single space. Use [`set_text`](Self::set_text) when
84 /// you need full control over the marker and spacing.
85 ///
86 /// # Example
87 /// ```
88 /// use yaml_edit::Document;
89 /// use std::str::FromStr;
90 ///
91 /// let doc = Document::from_str("key: value # teh value\n").unwrap();
92 /// let comment = doc.comments().next().unwrap();
93 /// comment.set_content("the value");
94 /// assert_eq!(doc.to_string(), "key: value # the value\n");
95 /// ```
96 pub fn set_content(&self, content: &str) {
97 self.set_text(&format!("# {content}"));
98 }
99
100 /// Access the underlying syntax token.
101 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
120/// Iterate over every comment in `node` and its descendants, in source order.
121pub(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}