1919
2020import com .google .common .base .CharMatcher ;
2121import com .google .common .base .Verify ;
22+ import com .google .common .collect .DiscreteDomain ;
2223import com .google .common .collect .Range ;
2324import com .google .common .collect .RangeSet ;
2425import com .google .common .collect .TreeRangeMap ;
4142import com .sun .tools .javac .util .Position .LineMap ;
4243import java .util .ArrayDeque ;
4344import java .util .ArrayList ;
45+ import java .util .Collection ;
4446import java .util .Collections ;
4547import java .util .Comparator ;
4648import java .util .Deque ;
@@ -72,53 +74,67 @@ public final class StringWrapper {
7274 * adjacent string literals that share a line.
7375 */
7476 static String wrap (final int columnLimit , String input , Formatter formatter ) throws FormatterException {
75- String result = wrapOnce (columnLimit , input , formatter );
76- for (int round = 1 ; round < MAX_ROUNDS && !result .equals (input ); round ++) {
77+ return wrap (columnLimit , new Source (input , null ), formatter ).text ();
78+ }
79+
80+ /**
81+ * Reflows, as {@link #wrap(int, String, Formatter)} does, only the string literals and text blocks that overlap
82+ * the given character ranges: when part of a file is formatted, the long strings in the rest of it stay as they
83+ * are.
84+ */
85+ static String wrap (final int columnLimit , String input , RangeSet <Integer > characterRanges , Formatter formatter )
86+ throws FormatterException {
87+ return wrap (columnLimit , new Source (input , characterRanges ), formatter ).text ();
88+ }
89+
90+ private static Source wrap (final int columnLimit , Source input , Formatter formatter ) throws FormatterException {
91+ Source result = wrapOnce (columnLimit , input , formatter );
92+ for (int round = 1 ; round < MAX_ROUNDS && !result .text ().equals (input .text ()); round ++) {
7793 input = result ;
7894 result = wrapOnce (columnLimit , input , formatter );
7995 }
8096 return result ;
8197 }
8298
83- private static String wrapOnce (final int columnLimit , String input , Formatter formatter ) throws FormatterException {
84- if (!needWrapping (columnLimit , input )) {
99+ private static Source wrapOnce (final int columnLimit , Source input , Formatter formatter ) throws FormatterException {
100+ if (!needWrapping (columnLimit , input . text () )) {
85101 // fast path
86102 return input ;
87103 }
88104
89105 // The pieces continue the way the formatter continues a chain of +: four columns times the multiplier of its
90106 // style, eight in OJF.
91107 var continuationIndent = 4 * formatter .options ().indentationMultiplier ();
92- TreeRangeMap <Integer , String > replacements = getReflowReplacements (columnLimit , continuationIndent , input );
108+ TreeRangeMap <Integer , String > replacements = input . reflowReplacements (columnLimit , continuationIndent );
93109 if (replacements .asMapOfRanges ().isEmpty ()) {
94110 // needWrapping is an over-approximation, so there may be nothing to do after all.
95111 return input ;
96112 }
97- String firstPass =
98- formatter .formatSource (input , replacements .asMapOfRanges ().keySet ());
113+ Source firstPass = input .format (formatter , replacements .asMapOfRanges ().keySet ());
99114
100- if (!firstPass .equals (input )) {
115+ if (!firstPass .text (). equals (input . text () )) {
101116 // If formatting the replacement ranges resulted in a change, recalculate the replacements on
102117 // the updated input.
103118 input = firstPass ;
104- replacements = getReflowReplacements (columnLimit , continuationIndent , input );
119+ replacements = input . reflowReplacements (columnLimit , continuationIndent );
105120 }
106121
107- String result = applyReplacements ( input , replacements );
122+ Source result = input . apply ( replacements );
108123
109124 // Format again, because broken strings might now fit on the first line in case of assignments
110- String secondPass = formatter . formatSource ( result , rangesAfterAppliedReplacements (replacements ));
125+ Source secondPass = result . format ( formatter , rangesAfterAppliedReplacements (replacements ));
111126
112- if (!secondPass .equals (result )) {
113- replacements = getReflowReplacements (columnLimit , continuationIndent , secondPass );
114- result = applyReplacements ( secondPass , replacements );
127+ if (!secondPass .text (). equals (result . text () )) {
128+ replacements = secondPass . reflowReplacements (columnLimit , continuationIndent );
129+ result = secondPass . apply ( replacements );
115130 }
116131
117132 {
118133 // We really don't want bugs in this pass to change the behaviour of programs we're
119134 // formatting, so check that the pretty-printed AST is the same before and after reformatting.
120- String expected = parse (input , /* allowStringFolding= */ true ).toString ();
121- String actual = parse (result , /* allowStringFolding= */ true ).toString ();
135+ String expected =
136+ parse (input .text (), /* allowStringFolding= */ true ).toString ();
137+ String actual = parse (result .text (), /* allowStringFolding= */ true ).toString ();
122138 if (!expected .equals (actual )) {
123139 throw new FormatterException (String .format (
124140 "Something has gone terribly wrong. Please file a bug: "
@@ -131,6 +147,82 @@ private static String wrapOnce(final int columnLimit, String input, Formatter fo
131147 return result ;
132148 }
133149
150+ /**
151+ * The source between two passes of {@link #wrapOnce}, with the character ranges of it that the passes may change:
152+ * the ranges being formatted, moved along with every change, or {@code null} when the whole file is.
153+ */
154+ private record Source (String text , @ Nullable RangeSet <Integer > ranges ) {
155+
156+ /** The replacements of {@link #getReflowReplacements}, less those for literals outside the ranges. */
157+ TreeRangeMap <Integer , String > reflowReplacements (int columnLimit , int continuationIndent )
158+ throws FormatterException {
159+ TreeRangeMap <Integer , String > replacements = getReflowReplacements (columnLimit , continuationIndent , text );
160+ RangeSet <Integer > within = ranges ;
161+ if (within == null ) {
162+ return replacements ;
163+ }
164+ TreeRangeMap <Integer , String > inRanges = TreeRangeMap .create ();
165+ replacements .asMapOfRanges ().forEach ((literal , replacement ) -> {
166+ if (within .intersects (literal )) {
167+ inRanges .put (literal , replacement );
168+ }
169+ });
170+ return inRanges ;
171+ }
172+
173+ /** Formats the given ranges, as {@link Formatter#formatSource(String, Collection)} does. */
174+ Source format (Formatter formatter , Collection <Range <Integer >> rangesToFormat ) throws FormatterException {
175+ List <Replacement > replacements = formatter .getFormatReplacements (text , rangesToFormat );
176+ return new Source (Utils .applyReplacements (text , replacements ), moved (replacements ));
177+ }
178+
179+ Source apply (TreeRangeMap <Integer , String > replacements ) {
180+ List <Replacement > applied = replacements .asMapOfRanges ().entrySet ().stream ()
181+ .map (entry -> Replacement .create (
182+ entry .getKey ().lowerEndpoint (), entry .getKey ().upperEndpoint (), entry .getValue ()))
183+ .toList ();
184+ return new Source (applyReplacements (text , replacements ), moved (applied ));
185+ }
186+
187+ private @ Nullable RangeSet <Integer > moved (List <Replacement > replacements ) {
188+ return ranges == null ? null : moveRanges (ranges , replacements );
189+ }
190+ }
191+
192+ /**
193+ * Where the given character ranges are once the replacements, sorted and without overlaps, have been applied. A
194+ * range grows to take in all of a replacement that overlaps it, or that inserts text at its start.
195+ */
196+ static RangeSet <Integer > moveRanges (RangeSet <Integer > ranges , List <Replacement > replacements ) {
197+ RangeSet <Integer > moved = TreeRangeSet .create ();
198+ for (Range <Integer > range : ranges .asRanges ()) {
199+ Range <Integer > canonical = range .canonical (DiscreteDomain .integers ());
200+ moved .add (Range .closedOpen (
201+ movePosition (canonical .lowerEndpoint (), replacements , /* start= */ true ),
202+ movePosition (canonical .upperEndpoint (), replacements , /* start= */ false )));
203+ }
204+ return moved ;
205+ }
206+
207+ private static int movePosition (int position , List <Replacement > replacements , boolean start ) {
208+ int shift = 0 ;
209+ for (Replacement replacement : replacements ) {
210+ int lower = replacement .getReplaceRange ().lowerEndpoint ();
211+ int upper = replacement .getReplaceRange ().upperEndpoint ();
212+ int length = replacement .getReplacementString ().length ();
213+ if (upper < position || (upper == position && lower < position )) {
214+ // Before the position.
215+ shift += length - (upper - lower );
216+ } else if (lower < position ) {
217+ // Around the position, which moves to the replacement's start or end.
218+ return lower + shift + (start ? 0 : length );
219+ } else {
220+ break ;
221+ }
222+ }
223+ return position + shift ;
224+ }
225+
134226 private static Set <Range <Integer >> rangesAfterAppliedReplacements (TreeRangeMap <Integer , String > replacements ) {
135227 var outputRanges = new LinkedHashSet <Range <Integer >>();
136228 int offset = 0 ;
0 commit comments