/
FormatWriter.scala
1954 lines (1811 loc) · 72.3 KB
/
FormatWriter.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package org.scalafmt.internal
import java.nio.CharBuffer
import java.util.regex.Pattern
import org.scalafmt.CompatCollections.JavaConverters._
import org.scalafmt.{Formatted, Scalafmt}
import org.scalafmt.config.{Comments, Docstrings, FormatEvent, ScalafmtConfig}
import org.scalafmt.config.{Newlines, RewriteScala3Settings, TrailingCommas}
import org.scalafmt.rewrite.RedundantBraces
import org.scalafmt.util.TokenOps._
import org.scalafmt.util.{LiteralOps, TreeOps}
import scala.annotation.tailrec
import scala.collection.AbstractIterator
import scala.collection.mutable
import scala.meta.internal.Scaladoc
import scala.meta.internal.parsers.ScaladocParser
import scala.meta.tokens.{Token => T}
import scala.meta.transversers.Traverser
import scala.meta._
/** Produces formatted output from sequence of splits.
*/
class FormatWriter(formatOps: FormatOps) {
import FormatWriter._
import formatOps._
def mkString(state: State): String = {
implicit val sb = new StringBuilder()
val locations = getFormatLocations(state)
styleMap.init.runner.event(FormatEvent.Written(locations))
var delayedAlign = 0
locations.foreach { entry =>
val location = entry.curr
implicit val style: ScalafmtConfig = location.style
val formatToken = location.formatToken
var skipWs = false
formatToken.left match {
case _ if entry.previous.formatToken.meta.formatOff =>
sb.append(formatToken.meta.left.text) // checked the state for left
case _: T.Comment =>
entry.formatComment
case _: T.Interpolation.Part | _: T.Constant.String =>
sb.append(entry.formatMarginized)
case _: T.Constant.Int =>
sb.append(LiteralOps.prettyPrintInteger(formatToken.meta.left.text))
case _: T.Constant.Long =>
sb.append(LiteralOps.prettyPrintInteger(formatToken.meta.left.text))
case _: T.Constant.Float =>
sb.append(LiteralOps.prettyPrintFloat(formatToken.meta.left.text))
case _: T.Constant.Double =>
sb.append(LiteralOps.prettyPrintDouble(formatToken.meta.left.text))
case _ =>
val syntax =
Option(location.replace).getOrElse(formatToken.meta.left.text)
val rewrittenToken = style.rewriteTokens.getOrElse(syntax, syntax)
sb.append(rewrittenToken)
}
location.optionalBraces.toSeq
.sortBy { case (indent, _) => -indent }
.foreach { case (indent, owner) =>
val label = getEndMarkerLabel(owner)
if (label != null) {
val numBlanks = locations
.getBlanks(owner, owner, locations.getNest(owner))
.fold(0) { case (blanks, _, last) =>
val numBlanks = blanks.beforeEndMarker
if (numBlanks > 0) numBlanks
else
locations.extraBlankTokens
.get(last.meta.idx)
.fold(0)(x => if (x > 0) 1 else 0)
}
sb.append(getNewlines(numBlanks))
.append(getIndentation(indent))
.append("end ")
.append(label)
}
}
// missing braces
if (location.missingBracesIndent.nonEmpty) {
location.missingBracesIndent.toSeq
.sorted(Ordering.Int.reverse)
.foreach(i => sb.append('\n').append(getIndentation(i)).append("}"))
if (location.missingBracesOpenOrTuck) {
skipWs = true
sb.append(" ")
} else if (formatToken.right.is[T.RightParen])
skipWs = true
} else if (location.missingBracesOpenOrTuck)
sb.append(" {")
if (!skipWs) delayedAlign = entry.formatWhitespace(delayedAlign)
}
sb.toString()
}
private def getFormatLocations(state: State): FormatLocations = {
val toks = tokens.arr
require(toks.length >= state.depth, "splits !=")
val result = new Array[FormatLocation](state.depth)
@tailrec
def iter(state: State, lineId: Int): Unit =
if (state.depth != 0) {
val prev = state.prev
val idx = prev.depth
val ft = toks(idx)
val newlines =
if (idx == 0) 0
else state.split.modExt.mod.newlines + ft.meta.left.countNL
val newLineId = lineId + newlines
result(idx) = FormatLocation(ft, state, styleMap.at(ft), newLineId)
iter(prev, newLineId)
}
iter(state, 0)
if (state.depth == toks.length) { // format completed
val initStyle = styleMap.init
if (initStyle.dialect.allowSignificantIndentation) {
if (initStyle.rewrite.scala3.removeEndMarkerMaxLines > 0)
checkRemoveEndMarkers(result)
if (initStyle.rewrite.scala3.insertEndMarkerMinLines > 0)
checkInsertEndMarkers(result)
}
if (initStyle.rewrite.insertBraces.minLines > 0)
checkInsertBraces(result)
if (
initStyle.rewrite.rules.contains(RedundantBraces) &&
initStyle.rewrite.redundantBraces.parensForOneLineApply
)
replaceRedundantBraces(result)
}
new FormatLocations(result)
}
private def replaceRedundantBraces(locations: Array[FormatLocation]): Unit = {
// will map closing brace to opening brace and its line offset
val lookup = mutable.Map.empty[Int, (Int, Int)]
def checkApply(t: Tree): Boolean = t.parent match {
case Some(p @ Term.ArgClause(`t` :: Nil, _)) =>
TreeOps.isParentAnApply(p)
case _ => false
}
// iterate backwards, to encounter closing braces first
var idx = locations.length - 1
while (0 <= idx) {
val loc = locations(idx)
val tok = loc.formatToken
tok.left match {
case rb: T.RightBrace => // look for "foo { bar }"
val ok = tok.meta.leftOwner match {
case b: Term.Block =>
checkApply(b) && RedundantBraces.canRewriteBlockWithParens(b) &&
b.parent.exists(tokens.getLast(_) eq tok)
case f: Term.FunctionTerm =>
checkApply(f) && RedundantBraces.canRewriteFuncWithParens(f)
case t @ TreeOps.SingleArgInBraces(_, arg, _) =>
TreeOps.isParentAnApply(t) &&
RedundantBraces.canRewriteStatWithParens(arg)
case _ => false
}
if (ok) {
val beg = tokens(tokens.matching(rb))
lookup.update(beg.meta.idx, tok.meta.idx -> loc.leftLineId)
}
case _: T.LeftBrace =>
val state = loc.state
val style = loc.style
lookup.remove(idx).foreach {
case (end, endOffset)
if endOffset == loc.leftLineId &&
(style.rewrite.trailingCommas.allowFolding ||
style.getTrailingCommas != TrailingCommas.always) =>
val inParentheses = style.spaces.inParentheses
// remove space before "{"
val prevBegState =
if (0 == idx || (state.prev.split.modExt.mod ne Space))
state.prev
else {
val prevloc = locations(idx - 1)
val prevState =
state.prev.copy(split = state.prev.split.withMod(NoSplit))
locations(idx - 1) = prevloc.copy(
shift = prevloc.shift - 1,
state = prevState
)
prevState
}
// update "{"
locations(idx) =
if (inParentheses)
loc.copy(
replace = "(",
state = state.copy(prev = prevBegState)
)
else {
// remove space after "{"
val split = state.split.withMod(NoSplit)
loc.copy(
replace = "(",
shift = loc.shift - 1,
state = state.copy(prev = prevBegState, split = split)
)
}
val prevEndLoc = locations(end - 1)
val prevEndState = prevEndLoc.state
val newPrevEndState =
if (inParentheses) prevEndState
else {
// remove space before "}"
val split = prevEndState.split.withMod(NoSplit)
val newState = prevEndState.copy(split = split)
locations(end - 1) = prevEndLoc
.copy(shift = prevEndLoc.shift - 1, state = newState)
newState
}
// update "}"
val endLoc = locations(end)
locations(end) = endLoc.copy(
replace = ")",
state = endLoc.state.copy(prev = newPrevEndState)
)
case _ =>
}
case _ =>
}
idx -= 1
}
}
private def getOptionalBracesOwner(
floc: FormatLocation,
minBlockStats: Int
): Option[Tree] = {
val ob = formatOps.OptionalBraces.get(floc.formatToken)(floc.style)
ob.flatMap(_.owner).filter {
/* if we add the end marker, it might turn a single-stat expression (or
* block) into a multi-stat block and thus potentially change how that
* parent expression would have been formatted; so, avoid those cases.
*/
_.parent match {
case Some(t: Term.Block) => t.stats.lengthCompare(minBlockStats) >= 0
case Some(_: Template | _: Source | _: Pkg) => true
case _ => false
}
}
}
private def checkRemoveEndMarkers(locations: Array[FormatLocation]): Unit = {
var removedLines = 0
val endMarkers = new mutable.ListBuffer[(Int, Int)]
locations.foreach { x =>
val idx = x.formatToken.meta.idx
val floc = if (removedLines > 0 && x.isNotRemoved) {
val floc = x.copy(leftLineId = x.leftLineId + removedLines)
locations(idx) = floc
floc
} else x
if (endMarkers.nonEmpty && endMarkers(0)._1 == idx) {
val begIdx = endMarkers.remove(0)._2
val endIdx = locations.lastIndexWhere(_.isNotRemoved, idx)
if (endIdx >= 0) {
val bLoc = locations(begIdx)
val eLoc = locations(endIdx)
val span = getLineDiff(bLoc, eLoc)
if (span < bLoc.style.rewrite.scala3.removeEndMarkerMaxLines) {
val loc2 = locations(idx + 2)
locations(idx + 1) = locations(idx + 1).remove
locations(idx + 2) = loc2.remove
locations(endIdx) = eLoc.copy(state = loc2.state)
removedLines += 1
}
}
} else
getOptionalBracesOwner(floc, 3).foreach { owner =>
// do not skip comment lines, as the parser doesn't handle comments
// at end of optional braces region and treats them as outside
val endFt = tokens.nextNonCommentSameLine(tokens.getLast(owner))
val ok = endFt.meta.rightOwner match {
case em: Term.EndMarker => em.parent == owner.parent
case _ => false
}
if (ok) {
// "<left> end name <right>"
val end = endFt.meta.idx
val isStandalone = locations(end).hasBreakAfter &&
end + 2 < locations.length && locations(end + 2).hasBreakAfter
if (isStandalone) {
val settings = floc.style.rewrite.scala3
val idx = settings.countEndMarkerLines match {
case RewriteScala3Settings.EndMarkerLines.lastBlockOnly =>
tokens.nextNonCommentSameLine(floc.formatToken).meta.idx + 1
case RewriteScala3Settings.EndMarkerLines.all =>
tokens.getHead(owner).meta.idx
}
endMarkers.prepend(end -> idx)
}
}
}
}
}
private def checkInsertEndMarkers(locations: Array[FormatLocation]): Unit =
locations.foreach { floc =>
getOptionalBracesOwner(floc, 2).foreach { owner =>
val endFt = tokens.getLast(owner)
val ok = tokens.nextNonComment(endFt).meta.rightOwner match {
case em: Term.EndMarker => em.parent != owner.parent
case _ => true
}
if (ok) {
val end = endFt.meta.idx
val eLoc = locations(end)
val bLoc = locations(tokens.getHead(owner).meta.idx)
val begIndent = bLoc.state.prev.indentation
def appendOwner() =
locations(end) = eLoc.copy(optionalBraces =
eLoc.optionalBraces + (begIndent -> owner)
)
def removeOwner() =
locations(end) =
eLoc.copy(optionalBraces = eLoc.optionalBraces - begIndent)
def processOwner() = {
val settings = floc.style.rewrite.scala3
def okSpan(loc: FormatLocation) =
1 + getLineDiff(loc, eLoc) >= settings.insertEndMarkerMinLines
settings.countEndMarkerLines match {
case RewriteScala3Settings.EndMarkerLines.lastBlockOnly =>
val i = tokens.nextNonCommentSameLine(floc.formatToken).meta.idx
if (okSpan(locations(i + 1))) appendOwner() else removeOwner()
case RewriteScala3Settings.EndMarkerLines.all =>
if (!eLoc.optionalBraces.contains(begIndent) && okSpan(bLoc))
appendOwner()
}
}
if (eLoc.hasBreakAfter) processOwner()
}
}
}
private def checkInsertBraces(locations: Array[FormatLocation]): Unit = {
def checkInfix(tree: Tree): Boolean = tree match {
case ai: Term.ApplyInfix =>
tokens.isEnclosedInParens(ai) ||
tokens.prevNonCommentSameLine(tokens.tokenJustBefore(ai.op)).noBreak &&
checkInfix(ai.lhs) && (ai.argClause.values match {
case head :: Nil => checkInfix(head)
case _ => true
})
case _ => true
}
var addedLines = 0
val willAddLines = new mutable.ListBuffer[Int]
locations.foreach { x =>
val idx = x.formatToken.meta.idx
val floc = if (addedLines > 0 && x.isNotRemoved) {
val floc = x.copy(leftLineId = x.leftLineId - addedLines)
locations(idx) = floc
floc
} else x
if (willAddLines.nonEmpty && willAddLines(0) == idx) {
addedLines += 1
willAddLines.remove(0)
}
@tailrec
def hasBreakAfter(i: Int): Boolean = i < locations.length && {
val x = locations(i)
if (!x.isNotRemoved) hasBreakAfter(i + 1)
else if (x.hasBreakAfter) true
else if (!x.formatToken.right.is[T.Comment]) false
else hasBreakAfter(i + 1)
}
val style = floc.style
val ib = style.rewrite.insertBraces
val ft = floc.formatToken
val ok = !ft.meta.formatOff && ib.minLines > 0 &&
floc.missingBracesIndent.isEmpty
val mb =
if (ok) formatOps.MissingBraces.getBlocks(ft, ib.allBlocks).filter {
case (y, _) => checkInfix(y) && hasBreakAfter(idx)
}
else None
mb.foreach { case (owner, otherBlocks) =>
val endFt = tokens.nextNonCommentSameLine(tokens.getLast(owner))
val end = endFt.meta.idx
val eLoc = locations(end)
val begIndent = floc.state.prev.indentation
def checkSpan: Boolean =
getLineDiff(floc, eLoc) + addedLines >= ib.minLines ||
otherBlocks.exists { case (b, e) =>
val bIdx = tokens.tokenJustBefore(b).meta.idx
val eIdx = tokens.getLast(e).meta.idx
val span = getLineDiff(locations(bIdx), locations(eIdx))
ib.minLines <=
(if (bIdx <= idx && eIdx > idx) span + addedLines else span)
}
if (
!endFt.meta.formatOff && eLoc.hasBreakAfter &&
!eLoc.missingBracesIndent.contains(begIndent) && checkSpan
) {
val addLine = style.newlines.alwaysBeforeElseAfterCurlyIf ||
(endFt.right match {
case _: T.KwElse | _: T.KwCatch | _: T.KwFinally =>
!owner.parent.contains(endFt.meta.rightOwner)
case _ => true
})
if (addLine) willAddLines.prepend(end)
locations(idx) = floc.copy(missingBracesOpenOrTuck = true)
locations(end) = eLoc.copy(
missingBracesOpenOrTuck = !addLine &&
(eLoc.missingBracesIndent.isEmpty || eLoc.missingBracesOpenOrTuck),
missingBracesIndent = eLoc.missingBracesIndent + begIndent
)
}
}
}
}
class FormatLocations(val locations: Array[FormatLocation]) {
val tokenAligns: Map[Int, Int] = alignmentTokens
def foreach(f: Entry => Unit): Unit = {
Iterator.range(0, locations.length).foreach { i =>
val entry = new Entry(i)
if (entry.curr.isNotRemoved) f(entry)
}
}
class Entry(val i: Int) {
val curr = locations(i)
private implicit val style: ScalafmtConfig = curr.style
def previous = locations(math.max(i - 1, 0))
@inline def tok = curr.formatToken
@inline def state = curr.state
@inline def prevState = curr.state.prev
private def appendWhitespace(alignOffset: Int, delayedAlign: Int)(implicit
sb: StringBuilder
): Int = {
val mod = state.split.modExt.mod
def currentAlign = tokenAligns.get(i).fold(0)(_ + alignOffset)
val ws = mod match {
case nl: NewlineT =>
val extraBlanks =
if (i == locations.length - 1) 0
else extraBlankTokens.getOrElse(i, if (nl.isDouble) 1 else 0)
val newlines = getNewlines(extraBlanks)
if (nl.noIndent) newlines
else newlines + getIndentation(state.indentation)
case p: Provided => p.betweenText
case NoSplit if style.align.delayUntilSpace =>
return delayedAlign + currentAlign // RETURNING!
case _ => getIndentation(mod.length + currentAlign + delayedAlign)
}
sb.append(ws)
0
}
def formatWhitespace(delayedAlign: Int)(implicit
sb: StringBuilder
): Int = {
import org.scalafmt.config.TrailingCommas
/* If we are mutating trailing commas ('always' or 'never'), we should
* have removed them first in RewriteTrailingCommas; now we simply need
* to append them in case of 'always', but only when dangling */
def isClosedDelimWithNewline(expectedNewline: Boolean): Boolean = {
getClosedDelimWithNewline(expectedNewline).isDefined
}
def getClosedDelimWithNewline(whenNL: Boolean): Option[FormatToken] = {
@tailrec
def iter(
floc: FormatLocation,
hadNL: Boolean
): Option[FormatToken] = {
val isNL = floc.hasBreakAfter
if (isNL && !whenNL) None
else {
val ft = floc.formatToken
def gotNL = hadNL || isNL
ft.right match {
case _: T.Comment =>
val idx = ft.meta.idx + 1
if (idx == locations.length) None
else iter(locations(idx), gotNL)
case _ =>
val ok = gotNL == whenNL && TreeOps
.rightIsCloseDelimForTrailingComma(tok.left, ft, whenNL)
if (ok) Some(ft) else None
}
}
}
iter(curr, false)
}
@inline def ws(offset: Int) = appendWhitespace(offset, delayedAlign)
val noExtraOffset =
!dialect.allowTrailingCommas ||
tok.left.is[T.Comment] ||
previous.formatToken.meta.formatOff
if (noExtraOffset)
ws(0)
else
style.getTrailingCommas match {
// remove comma if no newline
case TrailingCommas.keep
if tok.left.is[T.Comma] && isClosedDelimWithNewline(false) =>
sb.setLength(sb.length - 1)
if (!tok.right.is[T.RightParen]) ws(1)
else if (style.spaces.inParentheses) {
sb.append(getIndentation(1 + delayedAlign)); 0
} else delayedAlign
// append comma if newline
case TrailingCommas.always
if !tok.left.is[T.Comma] && isClosedDelimWithNewline(true) =>
sb.append(',')
ws(-1)
// append comma if newline and multiple args
case TrailingCommas.multiple
if !tok.left.is[T.Comma] && getClosedDelimWithNewline(true)
.exists(isCloseDelimForTrailingCommasMultiple) =>
sb.append(',')
ws(-1)
case _ => ws(0)
}
}
def formatMarginized: String = {
val text = tok.meta.left.text
val tupleOpt = tok.left match {
case _ if !style.assumeStandardLibraryStripMargin => None
case _ if !tok.meta.left.hasNL => None
case _: T.Constant.String =>
TreeOps.getStripMarginChar(tok.meta.leftOwner).map { pipe =>
def isPipeFirstChar = text.find(_ != '"').contains(pipe)
val noAlign = !style.align.stripMargin || curr.hasBreakBefore
val thisOffset =
if (style.align.stripMargin) if (isPipeFirstChar) 3 else 2
else style.indent.main
val prevIndent =
if (noAlign) prevState.indentation
else prevState.prev.column + prevState.prev.split.length
(pipe, thisOffset + prevIndent)
}
case _: T.Interpolation.Part =>
TreeOps.getStripMarginCharForInterpolate(tok.meta.leftOwner).map {
val alignPipeOffset = if (style.align.stripMargin) 1 else 0
(_, prevState.indentation + alignPipeOffset)
}
case _ => None
}
tupleOpt.fold(text) { case (pipe, indent) =>
val spaces = getIndentation(indent)
getStripMarginPattern(pipe).matcher(text).replaceAll(spaces)
}
}
def formatComment(implicit sb: StringBuilder): Unit = {
val text = tok.meta.left.text
if (text.startsWith("//"))
new FormatSlc(text).format()
else if (text == "/**/")
sb.append(text)
else if (isDocstring(text))
formatDocstring(text)
else
new FormatMlc(text).format()
}
private def formatOnelineDocstring(
text: String
)(implicit sb: StringBuilder): Boolean = {
curr.isStandalone && {
val matcher = onelineDocstring.matcher(text)
matcher.matches() && (style.docstrings.oneline match {
case Docstrings.Oneline.fold => true
case Docstrings.Oneline.unfold => false
case Docstrings.Oneline.keep =>
matcher.start(1) == -1 && matcher.start(3) == -1
}) && {
val content = matcher.group(2)
val folding = style.docstrings.wrap match {
case Docstrings.Wrap.yes =>
content.length <= // 7 is the length of "/** " and " */"
style.docstringsWrapMaxColumn - prevState.indentation - 7
case _ => true
}
if (folding) sb.append("/** ").append(content).append(" */")
folding
}
}
}
private def formatDocstring(
text: String
)(implicit sb: StringBuilder): Unit = {
if (style.docstrings.style eq Docstrings.Preserve) sb.append(text)
else if (!formatOnelineDocstring(text))
new FormatMlDoc(text).format()
}
private abstract class FormatCommentBase(
protected val maxColumn: Int,
protected val extraIndent: Int = 1,
protected val leadingMargin: Int = 0
)(implicit sb: StringBuilder) {
protected final val breakBefore = curr.hasBreakBefore
protected final val indent = prevState.indentation
// extra 1 is for "*" (in "/*" or " *") or "/" (in "//")
protected final val maxLength = maxColumn - indent - extraIndent - 1
protected final def getFirstLineLength =
if (breakBefore) leadingMargin
else
prevState.prev.column - prevState.prev.indentation +
prevState.split.length
protected final def canRewrite =
style.comments.wrap match {
case Comments.Wrap.no => false
case Comments.Wrap.trailing => curr.hasBreakAfter
case Comments.Wrap.standalone => breakBefore && curr.hasBreakAfter
}
protected final type WordIter = Iterator[String]
protected class WordFormatter(
appendLineBreak: () => Unit,
extraMargin: String = " ",
prefixFirstWord: String => String = _ => ""
) {
final def apply(
iter: WordIter,
lineLength: Int,
atLineBeg: Boolean,
needSpaceIfAtLineBeg: Boolean = true
): Int = iterate(
iter,
sb.length - lineLength,
0,
atLineBeg,
needSpaceIfAtLineBeg
)
@tailrec
private def iterate(
iter: WordIter,
lineBeg: Int,
linesSoFar: Int,
atLineBeg: Boolean = false,
needSpaceIfAtLineBeg: Boolean = false
): Int = if (iter.hasNext) {
val word = iter.next()
var lines = linesSoFar
var nextLineBeg = lineBeg
def firstWordPrefix = prefixFirstWord(word)
def nextLineLength = 1 + word.length + sb.length - lineBeg
if (atLineBeg) {
if (needSpaceIfAtLineBeg) sb.append(' ')
sb.append(firstWordPrefix)
} else if (nextLineLength <= maxLength) {
sb.append(' ')
} else {
appendLineBreak()
lines += 1
nextLineBeg = sb.length
sb.append(extraMargin)
sb.append(firstWordPrefix)
}
sb.append(word)
iterate(iter, nextLineBeg, lines)
} else linesSoFar
}
protected def terminateMlc(begpos: Int, lines: Int): Unit = {
if (lines == 0 && style.comments.wrapSingleLineMlcAsSlc)
sb.setCharAt(begpos - 1, '/')
else sb.append(" */")
}
protected def append(csq: CharSequence, beg: Int, end: Int) =
sb.append(CharBuffer.wrap(csq, beg, end))
}
private class FormatSlc(text: String)(implicit sb: StringBuilder)
extends FormatCommentBase(style.maxColumn) {
def format(): Unit = {
val trimmed = removeTrailingWhiteSpace(text)
val isCommentedOut = prevState.split.modExt.mod match {
case m: NewlineT if m.noIndent => true
case _ => indent == 0
}
if (isCommentedOut) sb.append(trimmed)
else {
val nonSlash = trimmed.indexWhere(_ != '/')
val hasSpace = nonSlash < 0 || // else space not needed
Character.isWhitespace(trimmed.charAt(nonSlash))
val column = prevState.column - text.length +
trimmed.length + (if (hasSpace) 0 else 1)
if (column > maxColumn && canRewrite) reFormat(trimmed)
else if (hasSpace) sb.append(trimmed)
else {
append(trimmed, 0, nonSlash).append(' ')
append(trimmed, nonSlash, trimmed.length)
}
}
}
private def reFormat(text: String): Unit = {
val useSlc =
breakBefore && style.comments.wrapStandaloneSlcAsSlc
val appendLineBreak: () => Unit =
if (useSlc) {
val spaces: String = getIndentation(indent)
() => sb.append('\n').append(spaces).append("//")
} else {
val spaces: String = getIndentation(indent + 1)
() => sb.append('\n').append(spaces).append('*')
}
val contents = text.substring(2).trim
val wordIter = splitAsIterator(slcDelim)(contents)
sb.append(if (useSlc) "//" else "/*")
val curlen = sb.length
val wf = new WordFormatter(appendLineBreak)
val lines = wf(wordIter, getFirstLineLength, breakBefore)
if (!useSlc) terminateMlc(curlen, lines)
}
}
private class FormatMlc(text: String)(implicit sb: StringBuilder)
extends FormatCommentBase(style.maxColumn) {
private val spaces: String = getIndentation(indent + 1)
def format(): Unit = {
// don't rewrite comments which contain nested comments
if (canRewrite && text.lastIndexOf("/*") == 0) {
val sectionIter = new SectIter {
private val lineIter = {
val header = mlcHeader.matcher(text)
val beg = if (header.lookingAt()) header.end() else 2
val contents = text.substring(beg, text.length - 2)
splitAsIterator(mlcLineDelim)(contents).buffered
}
private def paraEnds: Boolean = lineIter.head.isEmpty
override def hasNext = lineIter.hasNext
override def next() = new ParaIter
class ParaIter extends AbstractIterator[WordIter] {
private var hasPara: Boolean = true
override def hasNext: Boolean =
hasPara && lineIter.hasNext && {
hasPara = !paraEnds
if (!hasPara)
do lineIter.next() while (lineIter.hasNext && paraEnds)
hasPara
}
override def next() =
new ParaLineIter().flatMap(splitAsIterator(slcDelim))
class ParaLineIter extends AbstractIterator[String] {
private var hasLine: Boolean = true
override def hasNext: Boolean = hasLine
override def next(): String = {
val head = lineIter.next()
hasLine = lineIter.hasNext && !paraEnds &&
!mlcParagraphEnd.matcher(head).find() &&
!mlcParagraphBeg.matcher(lineIter.head).find()
head
}
}
}
}
sb.append("/*")
val curlen = sb.length
val lines = iterSections(sectionIter)
terminateMlc(curlen, lines)
} else {
val trimmed = removeTrailingWhiteSpace(text)
sb.append(leadingAsteriskSpace.matcher(trimmed).replaceAll(spaces))
}
}
private def appendLineBreak(): Unit = {
sb.append('\n').append(spaces).append('*')
}
private val wf = new WordFormatter(appendLineBreak)
private type ParaIter = Iterator[WordIter]
private def iterParagraphs(
iter: ParaIter,
firstLineLen: Int,
atLineBeg: Boolean
): Int = {
var lines = wf(iter.next(), firstLineLen, atLineBeg)
while (iter.hasNext) {
appendLineBreak()
lines += 1 + wf(iter.next(), leadingMargin, true)
}
lines
}
private type SectIter = Iterator[ParaIter]
private def iterSections(iter: SectIter): Int = {
var lines =
iterParagraphs(iter.next(), getFirstLineLength, breakBefore)
while (iter.hasNext) {
appendLineBreak()
appendLineBreak()
lines += 2 + iterParagraphs(iter.next(), leadingMargin, true)
}
lines
}
}
private class FormatMlDoc(isWrap: Boolean)(text: String)(implicit
sb: StringBuilder
) extends FormatCommentBase(
if (isWrap) style.docstringsWrapMaxColumn else style.maxColumn,
if (style.docstrings.style eq Docstrings.SpaceAsterisk) 2 else 1,
if (style.docstrings.style eq Docstrings.AsteriskSpace) 1 else 0
) {
def this(text: String)(implicit sb: StringBuilder) = this(
(style.docstrings.wrap eq Docstrings.Wrap.yes) && curr.isStandalone
)(text)
private val spaces: String = getIndentation(indent + extraIndent)
private val margin = getIndentation(1 + leadingMargin)
def format(): Unit = {
val docOpt =
if (isWrap) ScaladocParser.parse(tok.meta.left.text) else None
docOpt.fold(formatNoWrap())(formatWithWrap)
}
private def formatWithWrap(doc: Scaladoc): Unit = {
sb.append("/**")
val sbLen =
if (style.docstrings.skipFirstLineIf(false)) {
appendBreak()
0 // force margin but not extra asterisk
} else {
sb.append(' ')
sb.length
}
val paras = doc.para.iterator
paras.foreach { para =>
para.terms.foreach {
formatTerm(_, margin, sbNonEmpty = sb.length != sbLen)
}
if (paras.hasNext) appendBreak()
}
if (sb.length == sbLen) sb.append('*')
sb.append('/')
}
private def formatTerm(
term: Scaladoc.Term,
termIndent: String,
sbNonEmpty: Boolean
): Unit = {
def forceFirstLine(): Unit = {
// don't output on top line
// lists/fenced code blocks are sensitive to margin
sb.setLength(sb.length - 1) // remove space
appendBreak()
}
if (sbNonEmpty) sb.append(termIndent)
term match {
case t: Scaladoc.CodeBlock =>
sb.append("{{{")
val nested = t.code.headOption.exists(_.endsWith("// scala"))
formatCodeBlock(nested, t.code, margin, isRelative = false)
sb.append(termIndent).append("}}}")
appendBreak()
case t: Scaladoc.MdCodeBlock =>
// first spaces (after asterisk) on all lines must align
if (!sbNonEmpty && leadingMargin != 0) {
forceFirstLine()
sb.append(termIndent)
}
sb.append(t.fence)
if (t.info.nonEmpty) {
sb.append(t.info.head)
t.info.tail.foreach(x => sb.append(' ').append(x))
}
val nested = t.info.headOption.contains("scala")
formatCodeBlock(nested, t.code, termIndent, isRelative = true)
sb.append(termIndent).append(t.fence)
appendBreak()
case t: Scaladoc.Heading =>
val delimiter = "=" * t.level
sb.append(delimiter).append(t.title).append(delimiter)
appendBreak()
case t: Scaladoc.Tag =>
sb.append(t.tag.tag)
t.label.foreach(x => sb.append(' ').append(x.syntax))
appendBreak()
if (t.desc.nonEmpty) {
val tagIndent = getIndentation(2 + termIndent.length)
t.desc.foreach(formatTerm(_, tagIndent, sbNonEmpty = true))
}
case t: Scaladoc.ListBlock =>
// outputs margin space and appends new line, too
// therefore, let's start by "rewinding"
if (sbNonEmpty || leadingMargin == 0) {
sb.setLength(sb.length - termIndent.length)
} else {
forceFirstLine()
}
val listIndent = // shift initial only by 2
if (termIndent ne margin) termIndent
else getIndentation(margin.length + 2)
formatListBlock(listIndent)(t)
case t: Scaladoc.Text =>
formatTextAfterMargin(t, termIndent)
case t: Scaladoc.Table =>
formatTable(t, termIndent)
}
}
private def formatTextAfterMargin(
text: Scaladoc.Text,
termIndent: String
): Unit = {
def prefixFirstWord(word: String): String = {
def likeNonText = {
word.startsWith("```") || word.startsWith("~~~") || // code fence
word.startsWith("@") || // tag
word.startsWith("=") || // heading
word.startsWith("|") || word.startsWith("+-") || // table
word == "-" || // list, this and next
word.length == 2 && word(1) == '.' && "1aiI".contains(word(0))
}
if (likeNonText) "\\" else "" // escape if parser can be confused
}
val wf = new WordFormatter(appendBreak, termIndent, prefixFirstWord)
val words = text.parts.iterator.map(_.syntax)
wf(words, termIndent.length, true, false)
appendBreak()
}
private def formatCodeBlock(
nested: Boolean,
code: Seq[String],
termIndent: String,
isRelative: Boolean
): Unit = {
val ok = nested && formatScalaCodeBlock(code, termIndent)
if (!ok) formatCodeBlock(code, termIndent, isRelative)
}
private def formatCodeBlock(
code: Seq[String],
termIndent: String,
isRelative: Boolean
): Unit = {
val offsetOpt =
if (isRelative) None
else {
val minSpaces = code.foldLeft(Int.MaxValue) { (res, x) =>
val matcher = docstringLeadingSpace.matcher(x)
if (matcher.lookingAt()) math.min(res, matcher.end())
else if (x.nonEmpty) 0
else res
}
if (minSpaces < Int.MaxValue) {
val offset = minSpaces - termIndent.length()
val spaces = if (offset > 0) getIndentation(offset) else ""
Some((spaces, minSpaces))
} else None
}
appendBreak()
code.foreach { x =>