Skip to main content

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    /// # Errors
49    ///
50    /// Returns a [`CommentTextError`], leaving the tree unchanged, if `text`
51    /// does not start with `#` or contains a line break: either would turn
52    /// the token into something other than a single comment.
53    ///
54    /// Afterwards this handle refers to the new comment token.
55    ///
56    /// # Example
57    /// ```
58    /// use yaml_edit::Document;
59    /// use std::str::FromStr;
60    ///
61    /// let doc = Document::from_str("key: value # teh value\n").unwrap();
62    /// let mut comment = doc.comments().next().unwrap();
63    /// comment.set_text("# the value").unwrap();
64    /// assert_eq!(doc.to_string(), "key: value # the value\n");
65    /// ```
66    pub fn set_text(&mut self, text: &str) -> Result<(), CommentTextError> {
67        if !text.starts_with('#') {
68            return Err(CommentTextError::MissingHash);
69        }
70        if text.contains(['\n', '\r']) {
71            return Err(CommentTextError::LineBreak);
72        }
73        let parent = self
74            .0
75            .parent()
76            .expect("a comment token always has a parent node");
77        let index = self.0.index();
78        let new_token = fresh_token(SyntaxKind::COMMENT, text);
79        super::replace_children(&parent, index..index + 1, vec![new_token.into()]);
80        self.0 = parent
81            .children_with_tokens()
82            .nth(index)
83            .and_then(|element| element.into_token())
84            .expect("the replacement comment sits at the old comment's index");
85        Ok(())
86    }
87
88    /// Replace the comment content, preserving the leading `# ` marker.
89    ///
90    /// This is the counterpart to [`content`](Self::content): it sets the text
91    /// after the `#` and a single space. Use [`set_text`](Self::set_text) when
92    /// you need full control over the marker and spacing.
93    ///
94    /// # Errors
95    ///
96    /// Returns [`CommentTextError::LineBreak`] if `content` contains a line
97    /// break.
98    ///
99    /// # Example
100    /// ```
101    /// use yaml_edit::Document;
102    /// use std::str::FromStr;
103    ///
104    /// let doc = Document::from_str("key: value # teh value\n").unwrap();
105    /// let mut comment = doc.comments().next().unwrap();
106    /// comment.set_content("the value").unwrap();
107    /// assert_eq!(doc.to_string(), "key: value # the value\n");
108    /// ```
109    pub fn set_content(&mut self, content: &str) -> Result<(), CommentTextError> {
110        self.set_text(&format!("# {content}"))
111    }
112
113    /// Access the underlying syntax token.
114    pub fn syntax(&self) -> &SyntaxToken {
115        &self.0
116    }
117}
118
119/// Why [`Comment::set_text`] or [`Comment::set_content`] refused new text.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum CommentTextError {
122    /// The text does not start with `#`.
123    MissingHash,
124    /// The text contains a line break.
125    LineBreak,
126}
127
128impl std::fmt::Display for CommentTextError {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            CommentTextError::MissingHash => write!(f, "comment text must start with '#'"),
132            CommentTextError::LineBreak => write!(f, "comment text must not contain a line break"),
133        }
134    }
135}
136
137impl std::error::Error for CommentTextError {}
138
139impl std::fmt::Debug for Comment {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_struct("Comment")
142            .field("text", &self.text())
143            .finish()
144    }
145}
146
147impl std::fmt::Display for Comment {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(f, "{}", self.0.text())
150    }
151}
152
153/// Iterate over every comment in `node` and its descendants, in source order.
154pub(crate) fn comments(node: &SyntaxNode) -> impl Iterator<Item = Comment> {
155    node.descendants_with_tokens()
156        .filter_map(|element| element.into_token())
157        .filter_map(Comment::cast)
158}