1use crate::iter::IterDelimited;
2use solar::parse::{
3 ast::{CommentKind, Span},
4 interface::{BytePos, CharPos, SourceMap, source_map::SourceFile},
5 lexer::token::RawTokenKind as TokenKind,
6};
7use std::fmt;
8
9mod comment;
10pub use comment::{Comment, CommentStyle};
11
12pub mod inline_config;
13
14pub const DISABLE_START: &str = "forgefmt: disable-start";
15pub const DISABLE_END: &str = "forgefmt: disable-end";
16
17pub struct Comments {
18 comments: std::collections::VecDeque<Comment>,
19}
20
21impl fmt::Debug for Comments {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.write_str("Comments")?;
24 f.debug_list().entries(self.iter()).finish()
25 }
26}
27
28impl Comments {
29 pub fn new(
30 sf: &SourceFile,
31 sm: &SourceMap,
32 normalize_cmnts: bool,
33 group_cmnts: bool,
34 tab_width: Option<usize>,
35 ) -> Self {
36 let gatherer = CommentGatherer::new(sf, sm, normalize_cmnts, tab_width).gather();
37
38 Self {
39 comments: if group_cmnts { gatherer.group().into() } else { gatherer.comments.into() },
40 }
41 }
42
43 pub fn peek(&self) -> Option<&Comment> {
44 self.comments.front()
45 }
46
47 #[allow(clippy::should_implement_trait)]
48 pub fn next(&mut self) -> Option<Comment> {
49 self.comments.pop_front()
50 }
51
52 pub fn iter(&self) -> impl Iterator<Item = &Comment> {
53 self.comments.iter()
54 }
55
56 pub fn push_front(&mut self, cmnt: Comment) {
64 self.comments.push_front(cmnt)
65 }
66
67 pub fn peek_trailing(
72 &self,
73 sm: &SourceMap,
74 span_pos: BytePos,
75 next_pos: Option<BytePos>,
76 ) -> Option<(&Comment, usize)> {
77 let span_line = sm.lookup_char_pos(span_pos).line;
78 for (i, cmnt) in self.iter().enumerate() {
79 let comment_line = sm.lookup_char_pos(cmnt.pos()).line;
81 if comment_line != span_line {
82 break;
83 }
84
85 if cmnt.pos() < span_pos {
87 continue;
88 }
89
90 if cmnt.pos() >= next_pos.unwrap_or_else(|| cmnt.pos() + BytePos(1)) {
92 break;
93 }
94
95 match cmnt.style {
97 CommentStyle::Mixed => continue,
98 CommentStyle::Trailing => return Some((cmnt, i)),
99 _ => break,
100 }
101 }
102 None
103 }
104}
105
106struct CommentGatherer<'ast> {
107 sf: &'ast SourceFile,
108 sm: &'ast SourceMap,
109 text: &'ast str,
110 start_bpos: BytePos,
111 pos: usize,
112 comments: Vec<Comment>,
113 code_to_the_left: bool,
114 disabled_block_depth: usize,
115 tab_width: Option<usize>,
116}
117
118impl<'ast> CommentGatherer<'ast> {
119 fn new(
120 sf: &'ast SourceFile,
121 sm: &'ast SourceMap,
122 normalize_cmnts: bool,
123 tab_width: Option<usize>,
124 ) -> Self {
125 Self {
126 sf,
127 sm,
128 text: sf.src.as_str(),
129 start_bpos: sf.start_pos,
130 pos: 0,
131 comments: Vec::new(),
132 code_to_the_left: false,
133 disabled_block_depth: if normalize_cmnts { 0 } else { 1 },
134 tab_width,
135 }
136 }
137
138 fn gather(mut self) -> Self {
140 for token in solar::parse::Cursor::new(&self.text[self.pos..]) {
141 self.process_token(token);
142 }
143 self
144 }
145
146 fn group(self) -> Vec<Comment> {
151 let mut processed = Vec::new();
152 let mut cursor = self.comments.into_iter().peekable();
153
154 while let Some(mut current) = cursor.next() {
155 if current.kind == CommentKind::Line
156 && (current.style.is_trailing() || current.style.is_isolated())
157 {
158 let mut ref_line = self.sm.lookup_char_pos(current.span.hi()).line;
159 while let Some(next_comment) = cursor.peek() {
160 if !next_comment.style.is_isolated()
161 || next_comment.kind != CommentKind::Line
162 || ref_line + 1 != self.sm.lookup_char_pos(next_comment.span.lo()).line
163 {
164 break;
165 }
166
167 let next_to_merge = cursor.next().unwrap();
168 current.lines.extend(next_to_merge.lines);
169 current.span = current.span.to(next_to_merge.span);
170 ref_line += 1;
171 }
172 }
173
174 processed.push(current);
175 }
176
177 processed
178 }
179
180 fn make_span(&self, range: std::ops::Range<usize>) -> Span {
182 Span::new(self.start_bpos + range.start as u32, self.start_bpos + range.end as u32)
183 }
184
185 fn process_token(&mut self, token: solar::parse::lexer::token::RawToken) {
187 let token_range = self.pos..self.pos + token.len as usize;
188 let span = self.make_span(token_range.clone());
189 let token_text = &self.text[token_range];
190
191 if token_text.trim_start().contains(DISABLE_START) {
193 self.disabled_block_depth += 1;
194 } else if token_text.trim_start().contains(DISABLE_END) {
195 self.disabled_block_depth -= 1;
196 }
197
198 match token.kind {
199 TokenKind::Whitespace => {
200 if let Some(mut idx) = token_text.find('\n') {
201 self.code_to_the_left = false;
202
203 while let Some(next_newline) = token_text[idx + 1..].find('\n') {
204 idx += 1 + next_newline;
205 let pos = self.pos + idx;
206 self.comments.push(Comment {
207 is_doc: false,
208 kind: CommentKind::Line,
209 style: CommentStyle::BlankLine,
210 lines: vec![],
211 span: self.make_span(pos..pos),
212 });
213 if self.disabled_block_depth == 0 {
215 break;
216 }
217 }
218 }
219 }
220 TokenKind::BlockComment { is_doc, .. } => {
221 let code_to_the_right = !matches!(
222 self.text[self.pos + token.len as usize..].chars().next(),
223 Some('\r' | '\n')
224 );
225 let style = match (self.code_to_the_left, code_to_the_right) {
226 (_, true) => CommentStyle::Mixed,
227 (false, false) => CommentStyle::Isolated,
228 (true, false) => CommentStyle::Trailing,
229 };
230 let kind = CommentKind::Block;
231
232 let pos_in_file = self.start_bpos + BytePos(self.pos as u32);
234 let line_begin_in_file = line_begin_pos(self.sf, pos_in_file);
235 let line_begin_pos = (line_begin_in_file - self.start_bpos).to_usize();
236 let mut col = CharPos(self.text[line_begin_pos..self.pos].chars().count());
237
238 if !is_doc {
241 col = token_text.lines().skip(1).fold(col, |min, line| {
242 if line.is_empty() {
243 return min;
244 }
245 std::cmp::min(
246 CharPos(line.chars().count() - line.trim_start().chars().count()),
247 min,
248 )
249 })
250 };
251
252 let lines = self.split_block_comment_into_lines(token_text, is_doc, col);
253 self.comments.push(Comment { is_doc, kind, style, lines, span })
254 }
255 TokenKind::LineComment { is_doc } => {
256 let line =
257 if self.disabled_block_depth != 0 { token_text } else { token_text.trim_end() };
258 self.comments.push(Comment {
259 is_doc,
260 kind: CommentKind::Line,
261 style: if self.code_to_the_left {
262 CommentStyle::Trailing
263 } else {
264 CommentStyle::Isolated
265 },
266 lines: vec![line.into()],
267 span,
268 });
269 }
270 _ => {
271 self.code_to_the_left = true;
272 }
273 }
274 self.pos += token.len as usize;
275 }
276
277 fn split_block_comment_into_lines(
279 &self,
280 text: &str,
281 is_doc: bool,
282 col: CharPos,
283 ) -> Vec<String> {
284 if self.disabled_block_depth != 0 {
286 return vec![text.into()];
287 }
288
289 let mut res: Vec<String> = vec![];
290 let mut lines = text.lines();
291 if let Some(line) = lines.next() {
292 let line = line.trim_end();
293 if is_doc && let Some((_, second)) = line.split_once("/**") {
295 res.push("/**".to_string());
296 if !second.trim().is_empty() {
297 let line = normalize_block_comment_ws(second, col).trim_end();
298 if let Some((first, _)) = line.split_once("*/") {
300 if !first.trim().is_empty() {
301 res.push(format_doc_block_comment(first.trim_end(), self.tab_width));
302 }
303 res.push(" */".to_string());
304 } else {
305 res.push(format_doc_block_comment(line.trim_end(), self.tab_width));
306 }
307 }
308 } else {
309 res.push(line.to_string());
310 }
311 }
312
313 for (pos, line) in lines.delimited() {
314 let line = normalize_block_comment_ws(line, col).trim_end().to_string();
315 if !is_doc {
316 res.push(line);
317 continue;
318 }
319 if !pos.is_last {
320 res.push(format_doc_block_comment(&line, self.tab_width));
321 } else {
322 if let Some((first, _)) = line.split_once("*/")
324 && !first.trim().is_empty()
325 {
326 res.push(format_doc_block_comment(first.trim_end(), self.tab_width));
327 }
328 res.push(" */".to_string());
329 }
330 }
331 res
332 }
333}
334
335fn all_whitespace(s: &str, col: CharPos) -> Option<usize> {
339 let mut idx = 0;
340 for (i, ch) in s.char_indices().take(col.to_usize()) {
341 if !ch.is_whitespace() {
342 return None;
343 }
344 idx = i + ch.len_utf8();
345 }
346 Some(idx)
347}
348
349fn first_non_whitespace(s: &str) -> Option<usize> {
352 let mut len = 0;
353 for (i, ch) in s.char_indices() {
354 if ch.is_whitespace() {
355 len = ch.len_utf8()
356 } else {
357 return if i == 0 { Some(0) } else { Some(i + 1 - len) };
358 }
359 }
360 None
361}
362
363fn normalize_block_comment_ws(s: &str, col: CharPos) -> &str {
366 let len = s.len();
367 if let Some(col) = all_whitespace(s, col) {
368 return if col < len { &s[col..] } else { "" };
369 }
370 if let Some(col) = first_non_whitespace(s) {
371 return &s[col..];
372 }
373 s
374}
375
376fn format_doc_block_comment(line: &str, tab_width: Option<usize>) -> String {
378 if line.is_empty() {
379 return (" *").to_string();
380 }
381
382 if let Some((_, rest_of_line)) = line.split_once("*") {
383 if rest_of_line.is_empty() {
384 (" *").to_string()
385 } else if let Some(tab_width) = tab_width {
386 let mut normalized = String::from(" *");
387 line_with_tabs(
388 &mut normalized,
389 rest_of_line,
390 tab_width,
391 Some(Consolidation::MinOneTab),
392 );
393 normalized
394 } else {
395 format!(" *{rest_of_line}",)
396 }
397 } else if let Some(tab_width) = tab_width {
398 let mut normalized = String::from(" *\t");
399 line_with_tabs(&mut normalized, line, tab_width, Some(Consolidation::WithoutSpaces));
400 normalized
401 } else {
402 format!(" * {line}")
403 }
404}
405
406pub enum Consolidation {
407 MinOneTab,
408 WithoutSpaces,
409}
410
411pub fn line_with_tabs(
416 output: &mut String,
417 line: &str,
418 tab_width: usize,
419 strategy: Option<Consolidation>,
420) {
421 let first_non_ws = line.find(|c| c != ' ' && c != '\t').unwrap_or(line.len());
423 let (leading_ws, rest_of_line) = line.split_at(first_non_ws);
424
425 let total_width =
427 leading_ws.chars().fold(0, |width, c| width + if c == ' ' { 1 } else { tab_width });
428 let (mut num_tabs, mut num_spaces) = (total_width / tab_width, total_width % tab_width);
429
430 match strategy {
432 Some(Consolidation::MinOneTab) => {
433 if num_tabs == 0 && num_spaces != 0 {
434 (num_tabs, num_spaces) = (1, 0);
435 } else if num_spaces != 0 {
436 (num_tabs, num_spaces) = (num_tabs + 1, 0);
437 }
438 }
439 Some(Consolidation::WithoutSpaces) => {
440 if num_spaces != 0 {
441 (num_tabs, num_spaces) = (num_tabs + 1, 0);
442 }
443 }
444 None => (),
445 };
446
447 output.extend(std::iter::repeat_n('\t', num_tabs));
449 output.extend(std::iter::repeat_n(' ', num_spaces));
450 output.push_str(rest_of_line);
451}
452
453pub fn estimate_line_width(line: &str, tab_width: usize) -> usize {
455 line.chars().fold(0, |width, c| width + if c == '\t' { tab_width } else { 1 })
456}
457
458fn line_begin_pos(sf: &SourceFile, pos: BytePos) -> BytePos {
460 let pos = sf.relative_position(pos);
461 let line_index = sf.lookup_line(pos).unwrap();
462 let line_start_pos = sf.lines()[line_index];
463 sf.absolute_position(line_start_pos)
464}