sea_core/formatter/
comments.rs1use std::collections::BTreeMap;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct Comment {
11 pub text: String,
13 pub line: usize,
15 pub is_trailing: bool,
17}
18
19pub fn extract_comments(source: &str) -> BTreeMap<usize, Vec<Comment>> {
24 let mut comments: BTreeMap<usize, Vec<Comment>> = BTreeMap::new();
25
26 for (line_idx, line) in source.lines().enumerate() {
27 let line_num = line_idx + 1;
28 let trimmed = line.trim();
29
30 if let Some(comment_start) = trimmed.find("//") {
32 let before_comment = &line[..line.find("//").unwrap_or(0)];
33 let is_trailing = !before_comment.trim().is_empty();
34
35 let comment_text = trimmed[comment_start + 2..].trim();
36
37 comments.entry(line_num).or_default().push(Comment {
38 text: comment_text.to_string(),
39 line: line_num,
40 is_trailing,
41 });
42 }
43 }
44
45 comments
46}
47
48pub fn associate_comments_with_lines(
53 comments: &BTreeMap<usize, Vec<Comment>>,
54 declaration_lines: &[usize],
55) -> BTreeMap<usize, Vec<Comment>> {
56 let mut associated: BTreeMap<usize, Vec<Comment>> = BTreeMap::new();
57
58 let mut decl_lines = declaration_lines.to_vec();
60 decl_lines.sort();
61
62 for &decl_line in &decl_lines {
64 let mut leading_comments = Vec::new();
65
66 let mut check_line = decl_line.saturating_sub(1);
68 while check_line > 0 {
69 if let Some(line_comments) = comments.get(&check_line) {
70 let non_trailing: Vec<_> = line_comments
72 .iter()
73 .filter(|c| !c.is_trailing)
74 .cloned()
75 .collect();
76
77 if non_trailing.is_empty() {
78 break;
79 }
80
81 for c in non_trailing.into_iter().rev() {
83 leading_comments.insert(0, c);
84 }
85 check_line -= 1;
86 } else {
87 break;
88 }
89 }
90
91 if !leading_comments.is_empty() {
92 associated.insert(decl_line, leading_comments);
93 }
94 }
95
96 associated
97}
98
99#[derive(Debug, Clone)]
101pub struct CommentedSource {
102 pub source: String,
104 pub comments: BTreeMap<usize, Vec<Comment>>,
106 pub file_header_comments: Vec<Comment>,
108}
109
110impl CommentedSource {
111 pub fn new(source: &str) -> Self {
113 let comments = extract_comments(source);
114
115 let mut file_header_comments = Vec::new();
117 for (line_idx, line) in source.lines().enumerate() {
118 let line_num = line_idx + 1;
119 let trimmed = line.trim();
120
121 if trimmed.is_empty() {
122 continue;
123 }
124
125 if trimmed.starts_with("//") {
126 if let Some(line_comments) = comments.get(&line_num) {
127 file_header_comments.extend(line_comments.iter().cloned());
128 }
129 } else {
130 break;
132 }
133 }
134
135 Self {
136 source: source.to_string(),
137 comments,
138 file_header_comments,
139 }
140 }
141
142 pub fn leading_comments_for(&self, line: usize) -> Vec<&Comment> {
144 let mut result = Vec::new();
146 let mut check_line = line.saturating_sub(1);
147
148 while check_line > 0 {
149 if let Some(comments) = self.comments.get(&check_line) {
150 let non_trailing: Vec<_> = comments.iter().filter(|c| !c.is_trailing).collect();
151
152 if non_trailing.is_empty() {
153 break;
154 }
155
156 for c in non_trailing.into_iter().rev() {
157 result.insert(0, c);
158 }
159 check_line -= 1;
160 } else {
161 break;
162 }
163 }
164
165 result
166 }
167
168 pub fn has_comments(&self) -> bool {
170 !self.comments.is_empty()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_extract_line_comments() {
180 let source = r#"
181// This is a comment
182Entity "Foo"
183"#;
184 let comments = extract_comments(source);
185 assert_eq!(comments.len(), 1);
186 let line2_comments = comments.get(&2).unwrap();
187 assert_eq!(line2_comments[0].text, "This is a comment");
188 assert!(!line2_comments[0].is_trailing);
189 }
190
191 #[test]
192 fn test_extract_trailing_comments() {
193 let source = r#"Entity "Foo" // trailing comment
194"#;
195 let comments = extract_comments(source);
196 let line1_comments = comments.get(&1).unwrap();
197 assert_eq!(line1_comments[0].text, "trailing comment");
198 assert!(line1_comments[0].is_trailing);
199 }
200
201 #[test]
202 fn test_multiple_comments() {
203 let source = r#"
204// Comment 1
205// Comment 2
206Entity "Foo"
207"#;
208 let comments = extract_comments(source);
209 assert_eq!(comments.len(), 2);
210 assert!(comments.contains_key(&2));
211 assert!(comments.contains_key(&3));
212 }
213
214 #[test]
215 fn test_commented_source() {
216 let source = r#"// File header comment
217Entity "Foo"
218"#;
219 let cs = CommentedSource::new(source);
220 assert_eq!(cs.file_header_comments.len(), 1);
221 assert_eq!(cs.file_header_comments[0].text, "File header comment");
222 }
223}