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 ( ) ) )
@@ -86,26 +100,83 @@ pub fn get_comment_in_range(start: usize, end: usize, tokens: &mut [WrappedToken
86100 . collect ( )
87101}
88102
89- // Wrap doc with comment
103+ /// Wrap an `RcDoc` with comments. If there is a leading comment, then this
104+ /// will introduce a newline bat the start of the `RcDoc`. If there is a
105+ /// trailing comment, then it will introduce a newline at the end.
90106pub fn add_comment < ' a > ( d : RcDoc < ' a > , comment : Comment , next_doc : RcDoc < ' a > ) -> RcDoc < ' a > {
91107 let leading_comment = comment. leading_comment ;
92108 let trailing_comment = comment. trailing_comment ;
93109 let leading_comment_doc = get_leading_comment_doc_from_str ( & leading_comment) ;
94- let trailing_comment_doc: RcDoc < ' _ > = if trailing_comment. is_empty ( ) {
95- d. append ( next_doc)
96- } else {
97- d. append ( RcDoc :: space ( ) )
98- . append ( RcDoc :: text ( trailing_comment. trim ( ) . to_owned ( ) ) )
99- . append ( RcDoc :: hardline ( ) )
100- } ;
110+ let trailing_comment_doc = get_trailing_comment_doc_from_str ( & trailing_comment, next_doc) ;
111+ leading_comment_doc. append ( d) . append ( trailing_comment_doc)
112+ }
101113
102- leading_comment_doc. append ( trailing_comment_doc. clone ( ) )
114+ /// Remove empty lines from the input string, ignoring the first and last lines.
115+ /// (Because of how this function is used in `remove_empty_lines`, the first and
116+ /// last lines may include important spacing information.) This will remove empty
117+ /// lines _everywhere_, including in places where that may not be desired
118+ /// (e.g., in string literals).
119+ fn remove_empty_interior_lines ( s : & str ) -> String {
120+ let mut new_s = String :: new ( ) ;
121+ if s. starts_with ( '\n' ) {
122+ new_s. push_str ( "\n " ) ;
123+ }
124+ new_s. push_str (
125+ s. split_inclusive ( '\n' )
126+ // in the case where `s` does not end in a newline, `!ss.contains('\n')`
127+ // preserves whitespace on the last line
128+ . filter ( |ss| !ss. trim ( ) . is_empty ( ) || !ss. contains ( '\n' ) )
129+ . collect :: < Vec < _ > > ( )
130+ . join ( "" )
131+ . as_str ( ) ,
132+ ) ;
133+ new_s
103134}
104135
105- pub fn remove_empty_lines ( s : & str ) -> String {
106- s. lines ( )
107- . filter ( |ss| !ss. trim ( ) . is_empty ( ) )
108- . map ( |s| s. to_owned ( ) )
109- . collect :: < Vec < String > > ( )
110- . join ( "\n " )
136+ /// Remove empty lines, safely handling newlines that occur in quotations.
137+ pub fn remove_empty_lines ( text : & str ) -> String {
138+ // PANIC SAFETY: this regex pattern is valid
139+ #[ allow( clippy:: unwrap_used) ]
140+ let comment_regex = Regex :: new ( r"//[^\n]*" ) . unwrap ( ) ;
141+ // PANIC SAFETY: this regex pattern is valid
142+ #[ allow( clippy:: unwrap_used) ]
143+ let string_regex = Regex :: new ( r#""(\\.|[^"\\])*"[^\n]*"# ) . unwrap ( ) ;
144+
145+ let mut index = 0 ;
146+ let mut final_text = String :: new ( ) ;
147+
148+ while index < text. len ( ) {
149+ // Check for the next comment and string. The general strategy is to
150+ // call `remove_empty_interior_lines` on all the text _outside_ of
151+ // strings. Comments should be skipped to avoid interpreting a quote in
152+ // a comment as a string.
153+ let comment_match = comment_regex. find_at ( text, index) ;
154+ let string_match = string_regex. find_at ( text, index) ;
155+ match ( comment_match, string_match) {
156+ ( Some ( m1) , Some ( m2) ) => {
157+ // Handle the earlier match
158+ let m = std:: cmp:: min_by_key ( m1, m2, |m| m. start ( ) ) ;
159+ // PANIC SAFETY: Slicing `text` is safe since `index <= m.start()` and both are within the bounds of `text`.
160+ #[ allow( clippy:: indexing_slicing) ]
161+ final_text. push_str ( & remove_empty_interior_lines ( & text[ index..m. start ( ) ] ) ) ;
162+ final_text. push_str ( m. as_str ( ) ) ;
163+ index = m. end ( ) ;
164+ }
165+ ( Some ( m) , None ) | ( None , Some ( m) ) => {
166+ // PANIC SAFETY: Slicing `text` is safe since `index <= m.start()` and both are within the bounds of `text`.
167+ #[ allow( clippy:: indexing_slicing) ]
168+ final_text. push_str ( & remove_empty_interior_lines ( & text[ index..m. start ( ) ] ) ) ;
169+ final_text. push_str ( m. as_str ( ) ) ;
170+ index = m. end ( ) ;
171+ }
172+ ( None , None ) => {
173+ // PANIC SAFETY: Slicing `text` is safe since `index` is within the bounds of `text`.
174+ #[ allow( clippy:: indexing_slicing) ]
175+ final_text. push_str ( & remove_empty_interior_lines ( & text[ index..] ) ) ;
176+ break ;
177+ }
178+ }
179+ }
180+ // Trim the final result to account for dangling newlines
181+ final_text. trim ( ) . to_string ( )
111182}
0 commit comments