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