@@ -197,25 +197,37 @@ pub fn dedent(text: &str) -> Cow<'_, str> {
197197///
198198/// Lines that consist solely of whitespace are trimmed to a blank line.
199199///
200+ /// Lines that start with formfeeds have the indentation after the formfeeds
201+ /// removed and the formfeeds reinstated
202+ ///
200203/// # Panics
201204/// If the first line is indented by less than the provided indent.
202205pub fn dedent_to ( text : & str , indent : & str ) -> Option < String > {
203206 // Look at the indentation of the first non-empty line, to determine the "baseline" indentation.
204- let mut first_comment = None ;
207+ let mut first_comment_indent = None ;
205208 let existing_indent_len = text
206209 . universal_newlines ( )
207210 . find_map ( |line| {
208- let trimmed = line. trim_whitespace_start ( ) ;
211+ // Following Python's lexer, treat form feed character's at the start of a line
212+ // the same as a line break (reset the indentation)
213+ let trimmed_start_of_line_formfeed = line. trim_start_matches ( '\x0C' ) ;
214+ let trimmed = trimmed_start_of_line_formfeed. trim_whitespace_start ( ) ;
215+
216+ // A whitespace only line
209217 if trimmed. is_empty ( ) {
210- None
211- } else if trimmed. starts_with ( '#' ) && first_comment. is_none ( ) {
212- first_comment = Some ( line. len ( ) - trimmed. len ( ) ) ;
218+ return None ;
219+ }
220+
221+ let indent_len = trimmed_start_of_line_formfeed. len ( ) - trimmed. len ( ) ;
222+
223+ if trimmed. starts_with ( '#' ) && first_comment_indent. is_none ( ) {
224+ first_comment_indent = Some ( indent_len) ;
213225 None
214226 } else {
215- Some ( line . len ( ) - trimmed . len ( ) )
227+ Some ( indent_len )
216228 }
217229 } )
218- . unwrap_or ( first_comment . unwrap_or_default ( ) ) ;
230+ . unwrap_or ( first_comment_indent . unwrap_or_default ( ) ) ;
219231
220232 if existing_indent_len < indent. len ( ) {
221233 return None ;
@@ -225,23 +237,38 @@ pub fn dedent_to(text: &str, indent: &str) -> Option<String> {
225237 let dedent_len = existing_indent_len - indent. len ( ) ;
226238
227239 let mut result = String :: with_capacity ( text. len ( ) + indent. len ( ) ) ;
240+
228241 for line in text. universal_newlines ( ) {
229- let trimmed = line. trim_whitespace_start ( ) ;
230- if trimmed . is_empty ( ) {
231- if let Some ( line_ending ) = line . line_ending ( ) {
232- result . push_str ( & line_ending) ;
233- }
242+ let line_content = line. trim_start_matches ( '\x0C' ) ;
243+ let formfeed_count = line . len ( ) - line_content . len ( ) ;
244+
245+ let line_ending = if let Some ( line_ending ) = line . line_ending ( ) {
246+ line_ending . as_str ( )
234247 } else {
235- // Determine the current indentation level.
236- let current_indent_len = line. len ( ) - trimmed. len ( ) ;
237- if current_indent_len < existing_indent_len {
238- // If the current indentation level is less than the baseline, keep it as is.
239- result. push_str ( line. as_full_str ( ) ) ;
240- } else {
241- // Otherwise, reduce the indentation level.
242- result. push_str ( & line. as_full_str ( ) [ dedent_len..] ) ;
243- }
248+ ""
249+ } ;
250+
251+ let line_without_indent = line. trim_whitespace_start ( ) ;
252+
253+ if line_without_indent. is_empty ( ) {
254+ result. push_str ( line_ending) ;
255+ continue ;
244256 }
257+
258+ // Determine the current indentation level.
259+ let current_indent_len = line_content. len ( ) - line_without_indent. len ( ) ;
260+
261+ if current_indent_len < existing_indent_len {
262+ // If the current indentation level is less than the baseline, keep it as is.
263+ result. push_str ( line. as_full_str ( ) ) ;
264+ continue ;
265+ }
266+ let dedented_content = & line_content[ dedent_len..] ;
267+
268+ let formfeeds = & line[ ..formfeed_count] ;
269+ result. push_str ( formfeeds) ;
270+ result. push_str ( dedented_content) ;
271+ result. push_str ( line_ending) ;
245272 }
246273 Some ( result)
247274}
@@ -576,5 +603,154 @@ mod tests {
576603 " baz"
577604 ] . join ( "\n " ) ;
578605 assert_eq ! ( dedent_to( & x, " " ) , Some ( y) ) ;
606+
607+ let x = [
608+ "\x0C 1" ,
609+ " 2"
610+ ] . join ( "\n " ) ;
611+ let y = [
612+ "\x0C 1" ,
613+ "2"
614+ ] . join ( "\n " ) ;
615+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
616+ }
617+
618+ #[ test]
619+ #[ rustfmt:: skip]
620+ fn dedent_to_returns_none_if_indent_too_large ( ) {
621+ let x = [
622+ " foo" ,
623+ " bar"
624+ ] . join ( "\n " ) ;
625+ assert_eq ! ( dedent_to( & x, " " ) , None ) ;
626+ }
627+
628+ #[ test]
629+ #[ rustfmt:: skip]
630+ fn dedent_to_only_whitespace_lines ( ) {
631+ let x = [
632+ " " ,
633+ "\t " ,
634+ " "
635+ ] . join ( "\n " ) ;
636+ let y = "\n \n " . to_string ( ) ;
637+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
638+ }
639+
640+ #[ test]
641+ #[ rustfmt:: skip]
642+ fn dedent_to_preserves_crlf_for_lines_starting_with_form_feed ( ) {
643+ let x = [
644+ "\x0C 1\r \n " ,
645+ " 2\r \n " ,
646+ ] . join ( "" ) ;
647+ let y = [
648+ "\x0C 1\r \n " ,
649+ "2\r \n " ,
650+ ] . join ( "" ) ;
651+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
652+ }
653+
654+ #[ test]
655+ #[ rustfmt:: skip]
656+ fn dedent_to_preserves_multiple_leading_form_feeds_on_first_line ( ) {
657+ let x = [
658+ "\x0C \x0C 1" ,
659+ " 2" ,
660+ ] . join ( "\n " ) ;
661+ let y = [
662+ "\x0C \x0C 1" ,
663+ "2" ,
664+ ] . join ( "\n " ) ;
665+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
666+ }
667+
668+ #[ test]
669+ #[ rustfmt:: skip]
670+ fn dedent_to_preserves_multiple_leading_form_feeds_on_second_line ( ) {
671+ let x = [
672+ " 1" ,
673+ "\x0C \x0C 2" ,
674+ ] . join ( "\n " ) ;
675+ let y = [
676+ "1" ,
677+ "\x0C \x0C 2" ,
678+ ] . join ( "\n " ) ;
679+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
680+ }
681+
682+ #[ test]
683+ #[ rustfmt:: skip]
684+ fn dedent_to_handles_when_multiple_leading_form_feeds_greater_than_dedent_len ( ) {
685+ let x = [
686+ "\x0C \x0C \x0C \x0C 1" ,
687+ " 2" ,
688+ ] . join ( "\n " ) ;
689+ let y = [
690+ "\x0C \x0C \x0C \x0C 1" ,
691+ "2" ,
692+ ] . join ( "\n " ) ;
693+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
694+ }
695+
696+ #[ test]
697+ #[ rustfmt:: skip]
698+ fn dedent_to_ignores_leading_form_feeds_when_checking_indentation ( ) {
699+ let x = [
700+ " 1" ,
701+ "\x0C \x0C 2" ,
702+ ] . join ( "\n " ) ;
703+ let y = [
704+ "1" ,
705+ "\x0C \x0C 2" ,
706+ ] . join ( "\n " ) ;
707+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
708+ }
709+
710+ #[ test]
711+ #[ rustfmt:: skip]
712+ fn dedent_to_is_idempotent ( ) {
713+ let x = [
714+ " foo" ,
715+ " bar" ,
716+ " " ,
717+ " baz"
718+ ] . join ( "\n " ) ;
719+ let y = [
720+ " foo" ,
721+ " bar" ,
722+ "" ,
723+ " baz"
724+ ] . join ( "\n " ) ;
725+ let first_result = dedent_to ( & x, " " ) . unwrap ( ) ;
726+ assert_eq ! ( dedent_to( & first_result, " " ) , Some ( y) ) ;
727+ }
728+
729+ #[ test]
730+ #[ rustfmt:: skip]
731+ fn dedent_to_preserves_less_indented_later_line ( ) {
732+ let x = [
733+ " foo\n " ,
734+ " bar\n " ,
735+ ] . join ( "" ) ;
736+ let y = [
737+ "foo\n " ,
738+ " bar\n " ,
739+ ] . join ( "" ) ;
740+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
741+ }
742+
743+ #[ test]
744+ #[ rustfmt:: skip]
745+ fn dedent_to_preserves_less_indented_later_line_with_crlf ( ) {
746+ let x = [
747+ " foo\r \n " ,
748+ " bar\r \n " ,
749+ ] . join ( "" ) ;
750+ let y = [
751+ "foo\r \n " ,
752+ " bar\r \n " ,
753+ ] . join ( "" ) ;
754+ assert_eq ! ( dedent_to( & x, "" ) , Some ( y) ) ;
579755 }
580756}
0 commit comments