1616
1717use  itertools:: Itertools ; 
1818use  pretty:: RcDoc ; 
19+ use  regex:: Regex ; 
1920
2021use  super :: token:: { Comment ,  WrappedToken } ; 
2122
@@ -24,24 +25,37 @@ pub fn add_brackets<'a>(d: RcDoc<'a>, leftp: RcDoc<'a>, rightp: RcDoc<'a>) -> Rc
2425    leftp. append ( d. nest ( 1 ) ) . append ( rightp) 
2526} 
2627
28+ /// Convert a leading comment to an `RcDoc`, adding leading and trailing newlines. 
2729pub  fn  get_leading_comment_doc_from_str < ' a > ( leading_comment :  & str )  -> RcDoc < ' a >  { 
2830    if  leading_comment. is_empty ( )  { 
2931        RcDoc :: nil ( ) 
3032    }  else  { 
31-         let  cs:  RcDoc < ' _ >  = RcDoc :: intersperse ( 
32-             leading_comment
33-                 . trim ( ) 
34-                 . split ( '\n' ) 
35-                 . map ( |c| RcDoc :: text ( c. to_owned ( ) ) ) , 
36-             RcDoc :: hardline ( ) , 
37-         ) ; 
38-         RcDoc :: hardline ( ) . append ( cs) . append ( RcDoc :: hardline ( ) ) 
33+         RcDoc :: hardline ( ) 
34+             . append ( create_multiline_doc ( leading_comment) ) 
35+             . append ( RcDoc :: hardline ( ) ) 
3936    } 
4037} 
4138
42- pub  fn  get_trailing_comment_doc_from_str < ' a > ( trailing_comment :  & str )  -> RcDoc < ' a >  { 
39+ /// Convert multiline text into an `RcDoc`. Both `RcDoc::as_string` and 
40+ /// `RcDoc::text` allow newlines in the text (although the official 
41+ /// documentation says they don't), but the resulting text will maintain its 
42+ /// original indentation instead of the new "pretty" indentation. 
43+ fn  create_multiline_doc < ' a > ( str :  & str )  -> RcDoc < ' a >  { 
44+     RcDoc :: intersperse ( 
45+         str. trim ( ) . split ( '\n' ) . map ( |c| RcDoc :: text ( c. to_owned ( ) ) ) , 
46+         RcDoc :: hardline ( ) , 
47+     ) 
48+ } 
49+ 
50+ /// Convert a trailing comment to an `RcDoc`, adding a trailing newline. 
51+ /// There is no need to use `create_multiline_doc` because a trailing comment 
52+ /// cannot contain newlines. 
53+ pub  fn  get_trailing_comment_doc_from_str < ' a > ( 
54+     trailing_comment :  & str , 
55+     next_doc :  RcDoc < ' a > , 
56+ )  -> RcDoc < ' a >  { 
4357    if  trailing_comment. is_empty ( )  { 
44-         RcDoc :: nil ( ) 
58+         next_doc 
4559    }  else  { 
4660        RcDoc :: space ( ) 
4761            . append ( RcDoc :: text ( trailing_comment. trim ( ) . to_owned ( ) ) ) 
@@ -112,26 +126,83 @@ pub fn get_comment_in_range(span: miette::SourceSpan, tokens: &mut [WrappedToken
112126        . collect ( ) 
113127} 
114128
115- // Wrap doc with comment 
129+ /// Wrap an `RcDoc` with comments. If there is a leading comment, then this 
130+ /// will introduce a newline bat the start of the `RcDoc`. If there is a 
131+ /// trailing comment, then it will introduce a newline at the end. 
116132pub  fn  add_comment < ' a > ( d :  RcDoc < ' a > ,  comment :  Comment ,  next_doc :  RcDoc < ' a > )  -> RcDoc < ' a >  { 
117133    let  leading_comment = comment. leading_comment ; 
118134    let  trailing_comment = comment. trailing_comment ; 
119135    let  leading_comment_doc = get_leading_comment_doc_from_str ( & leading_comment) ; 
120-     let  trailing_comment_doc:  RcDoc < ' _ >  = if  trailing_comment. is_empty ( )  { 
121-         d. append ( next_doc) 
122-     }  else  { 
123-         d. append ( RcDoc :: space ( ) ) 
124-             . append ( RcDoc :: text ( trailing_comment. trim ( ) . to_owned ( ) ) ) 
125-             . append ( RcDoc :: hardline ( ) ) 
126-     } ; 
136+     let  trailing_comment_doc = get_trailing_comment_doc_from_str ( & trailing_comment,  next_doc) ; 
137+     leading_comment_doc. append ( d) . append ( trailing_comment_doc) 
138+ } 
127139
128-     leading_comment_doc. append ( trailing_comment_doc. clone ( ) ) 
140+ /// Remove empty lines from the input string, ignoring the first and last lines. 
141+ /// (Because of how this function is used in `remove_empty_lines`, the first and 
142+ /// last lines may include important spacing information.) This will remove empty 
143+ /// lines  _everywhere_, including in places where that may not be desired 
144+ /// (e.g., in string literals). 
145+ fn  remove_empty_interior_lines ( s :  & str )  -> String  { 
146+     let  mut  new_s = String :: new ( ) ; 
147+     if  s. starts_with ( '\n' )  { 
148+         new_s. push_str ( "\n " ) ; 
149+     } 
150+     new_s. push_str ( 
151+         s. split_inclusive ( '\n' ) 
152+             // in the case where `s` does not end in a newline, `!ss.contains('\n')` 
153+             // preserves whitespace on the last line 
154+             . filter ( |ss| !ss. trim ( ) . is_empty ( )  || !ss. contains ( '\n' ) ) 
155+             . collect :: < Vec < _ > > ( ) 
156+             . join ( "" ) 
157+             . as_str ( ) , 
158+     ) ; 
159+     new_s
129160} 
130161
131- pub  fn  remove_empty_lines ( s :  & str )  -> String  { 
132-     s. lines ( ) 
133-         . filter ( |ss| !ss. trim ( ) . is_empty ( ) ) 
134-         . map ( |s| s. to_owned ( ) ) 
135-         . collect :: < Vec < String > > ( ) 
136-         . join ( "\n " ) 
162+ /// Remove empty lines, safely handling newlines that occur in quotations. 
163+ pub  fn  remove_empty_lines ( text :  & str )  -> String  { 
164+     // PANIC SAFETY: this regex pattern is valid 
165+     #[ allow( clippy:: unwrap_used) ]  
166+     let  comment_regex = Regex :: new ( r"//[^\n]*" ) . unwrap ( ) ; 
167+     // PANIC SAFETY: this regex pattern is valid 
168+     #[ allow( clippy:: unwrap_used) ]  
169+     let  string_regex = Regex :: new ( r#""(\\.|[^"\\])*"[^\n]*"# ) . unwrap ( ) ; 
170+ 
171+     let  mut  index = 0 ; 
172+     let  mut  final_text = String :: new ( ) ; 
173+ 
174+     while  index < text. len ( )  { 
175+         // Check for the next comment and string. The general strategy is to 
176+         // call `remove_empty_interior_lines` on all the text _outside_ of 
177+         // strings. Comments should be skipped to avoid interpreting a quote in 
178+         // a comment as a string. 
179+         let  comment_match = comment_regex. find_at ( text,  index) ; 
180+         let  string_match = string_regex. find_at ( text,  index) ; 
181+         match  ( comment_match,  string_match)  { 
182+             ( Some ( m1) ,  Some ( m2) )  => { 
183+                 // Handle the earlier match 
184+                 let  m = std:: cmp:: min_by_key ( m1,  m2,  |m| m. start ( ) ) ; 
185+                 // PANIC SAFETY: Slicing `text` is safe since `index <= m.start()` and both are within the bounds of `text`. 
186+                 #[ allow( clippy:: indexing_slicing) ]  
187+                 final_text. push_str ( & remove_empty_interior_lines ( & text[ index..m. start ( ) ] ) ) ; 
188+                 final_text. push_str ( m. as_str ( ) ) ; 
189+                 index = m. end ( ) ; 
190+             } 
191+             ( Some ( m) ,  None )  | ( None ,  Some ( m) )  => { 
192+                 // PANIC SAFETY: Slicing `text` is safe since `index <= m.start()` and both are within the bounds of `text`. 
193+                 #[ allow( clippy:: indexing_slicing) ]  
194+                 final_text. push_str ( & remove_empty_interior_lines ( & text[ index..m. start ( ) ] ) ) ; 
195+                 final_text. push_str ( m. as_str ( ) ) ; 
196+                 index = m. end ( ) ; 
197+             } 
198+             ( None ,  None )  => { 
199+                 // PANIC SAFETY: Slicing `text` is safe since `index` is within the bounds of `text`. 
200+                 #[ allow( clippy:: indexing_slicing) ]  
201+                 final_text. push_str ( & remove_empty_interior_lines ( & text[ index..] ) ) ; 
202+                 break ; 
203+             } 
204+         } 
205+     } 
206+     // Trim the final result to account for dangling newlines 
207+     final_text. trim ( ) . to_string ( ) 
137208} 
0 commit comments