tinymist_analysis/syntax/
comment.rs1use itertools::Itertools;
4
5use crate::prelude::*;
6
7pub fn find_module_level_docs(src: &Source) -> Option<String> {
9 crate::log_debug_ct!("finding docs at: {id:?}", id = src.id());
10
11 let root = LinkedNode::new(src.root());
12 for n in root.children() {
13 if n.kind().is_trivia() {
14 continue;
15 }
16
17 return extract_mod_docs_between(&root, 0..n.offset(), true);
18 }
19
20 extract_mod_docs_between(&root, 0..src.text().len(), true)
21}
22
23fn extract_mod_docs_between(
25 node: &LinkedNode,
26 rng: Range<usize>,
27 first_group: bool,
28) -> Option<String> {
29 let mut matcher = DocCommentMatcher {
30 strict: true,
31 ..Default::default()
32 };
33 let nodes = node.children();
34 'scan_comments: for n in nodes {
35 let offset = n.offset();
36 if offset < rng.start {
37 continue 'scan_comments;
38 }
39 if offset >= rng.end {
40 break 'scan_comments;
41 }
42
43 crate::log_debug_ct!(
44 "found comment for docs: {:?}: {:?}",
45 n.kind(),
46 n.leaf_text()
47 );
48 if matcher.process(n.get()) {
49 if first_group {
50 break 'scan_comments;
51 }
52 matcher.comments.clear();
53 }
54 }
55
56 matcher.collect()
57}
58
59pub enum CommentGroupSignal {
61 Hash,
63 Space,
65 LineComment,
67 BlockComment,
69 BreakGroup,
71}
72
73#[derive(Default)]
75pub struct CommentGroupMatcher {
76 newline_count: u32,
77}
78
79impl CommentGroupMatcher {
80 pub fn reset(&mut self) {
83 self.newline_count = 0;
84 }
85
86 pub fn process(&mut self, n: &SyntaxNode) -> CommentGroupSignal {
92 match n.kind() {
93 SyntaxKind::Hash => {
94 self.newline_count = 0;
95
96 CommentGroupSignal::Hash
97 }
98 SyntaxKind::Space => {
99 if n.leaf_text().contains('\n') {
100 self.newline_count += 1;
101 }
102 if self.newline_count > 1 {
103 return CommentGroupSignal::BreakGroup;
104 }
105
106 CommentGroupSignal::Space
107 }
108 SyntaxKind::Parbreak => {
109 self.newline_count = 2;
110 CommentGroupSignal::BreakGroup
111 }
112 SyntaxKind::LineComment => {
113 self.newline_count = 0;
114 CommentGroupSignal::LineComment
115 }
116 SyntaxKind::BlockComment => {
117 self.newline_count = 0;
118 CommentGroupSignal::BlockComment
119 }
120 _ => {
121 self.newline_count = 0;
122 CommentGroupSignal::BreakGroup
123 }
124 }
125 }
126}
127
128enum RawComment {
130 Line(EcoString),
132 Block(EcoString),
134}
135
136#[derive(Default)]
138pub struct DocCommentMatcher {
139 comments: Vec<RawComment>,
141 group_matcher: CommentGroupMatcher,
143 strict: bool,
145}
146
147impl DocCommentMatcher {
148 pub fn reset(&mut self) {
151 self.comments.clear();
152 self.group_matcher.reset();
153 }
154
155 pub fn process(&mut self, n: &SyntaxNode) -> bool {
157 match self.group_matcher.process(n) {
158 CommentGroupSignal::LineComment => {
159 let text = n.leaf_text();
160 if !self.strict || text.starts_with("///") {
161 self.comments.push(RawComment::Line(text.clone()));
162 }
163 }
164 CommentGroupSignal::BlockComment => {
165 let text = n.leaf_text();
166 if !self.strict {
167 self.comments.push(RawComment::Block(text.clone()));
168 }
169 }
170 CommentGroupSignal::BreakGroup => {
171 return true;
172 }
173 CommentGroupSignal::Hash | CommentGroupSignal::Space => {}
174 }
175
176 false
177 }
178
179 pub fn collect(&mut self) -> Option<String> {
181 let comments = &self.comments;
182 if comments.is_empty() {
183 return None;
184 }
185
186 let comments = comments.iter().map(|comment| match comment {
187 RawComment::Line(line) => {
188 line.trim_start_matches('/')
190 }
191 RawComment::Block(block) => {
192 fn remove_comment(text: &str) -> Option<&str> {
193 let mut text = text.strip_prefix("/*")?.strip_suffix("*/")?.trim();
194 if text.starts_with('*') {
196 text = text.strip_prefix('*')?.trim();
197 }
198 Some(text)
199 }
200
201 remove_comment(block).unwrap_or(block.as_str())
202 }
203 });
204 let comments = comments.collect::<Vec<_>>();
205
206 let dedent = comments
207 .iter()
208 .flat_map(|line| {
209 let mut chars = line.chars();
210 let cnt = chars
211 .by_ref()
212 .peeking_take_while(|c| c.is_whitespace())
213 .count();
214 chars.next().map(|_| cnt)
215 })
216 .min()
217 .unwrap_or(0);
218
219 let size_hint = comments.iter().map(|comment| comment.len()).sum::<usize>();
220 let mut comments = comments
221 .iter()
222 .map(|comment| comment.chars().skip(dedent).collect::<String>());
223
224 let res = comments.try_fold(String::with_capacity(size_hint), |mut acc, comment| {
225 if !acc.is_empty() {
226 acc.push('\n');
227 }
228
229 acc.push_str(&comment);
230 Some(acc)
231 });
232
233 self.comments.clear();
234 res
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn test(it: &str) -> String {
243 find_module_level_docs(&Source::detached(it)).unwrap()
244 }
245
246 #[test]
247 fn simple() {
248 assert_eq!(
249 test(
250 r#"/// foo
251/// bar
252#let main() = printf("hello World")"#
253 ),
254 "foo\nbar"
255 );
256 }
257
258 #[test]
259 fn dedent() {
260 assert_eq!(
261 test(
262 r#"/// a
263/// b
264/// c
265#let main() = printf("hello World")"#
266 ),
267 "a\nb\nc"
268 );
269 assert_eq!(
270 test(
271 r#"///a
272/// b
273/// c
274#let main() = printf("hello World")"#
275 ),
276 "a\n b\n c"
277 );
278 }
279
280 #[test]
281 fn issue_1687_postive() {
282 assert_eq!(
283 test(
284 r#"/// Description.
285///
286/// Note.
287#let main() = printf("hello World")"#
288 ),
289 "Description.\n\nNote."
290 );
291 }
292
293 #[test]
294 fn issue_1687_negative() {
295 assert_eq!(
296 test(
297 r#"/// Description.
298///
299/// Note.
300#let main() = printf("hello World")"#
301 ),
302 "Description.\n\nNote."
303 );
304 }
305}