<lambda>null1 package leakcanary.internal.activity.screen
2
3 /**
4 * Performs word wrapping of leak traces.
5 */
6 internal object LeakTraceWrapper {
7 private const val SPACE = '\u0020'
8 private const val TILDE = '\u007E'
9 private const val PERIOD = '\u002E'
10 private const val ZERO_SPACE_WIDTH = '\u200B'
11
12 /**
13 * This implements a greedy wrapping algorithm.
14 *
15 * Each line that is longer than [maxWidth], is wrapped by taking the maximum amount of words that fit
16 * within the bounds delimited by [maxWidth]. This is done by walking back from the character at [maxWidth]
17 * position, until the first separator is found (a [SPACE] or [PERIOD]).
18 *
19 * Additionally, [Underline] characters are tracked and added when necessary.
20 *
21 * Finally, all lines start with an offset which includes a decorator character and some level of
22 * indentation.
23 */
24 fun wrap(
25 sourceMultilineString: String,
26 maxWidth: Int
27 ): String {
28 // Lines without terminating line separators
29 val linesNotWrapped = sourceMultilineString.lines()
30
31 val linesWrapped = mutableListOf<String>()
32
33 for (currentLineIndex in linesNotWrapped.indices) {
34 val currentLine = linesNotWrapped[currentLineIndex]
35
36 if (TILDE in currentLine) {
37 check(currentLineIndex > 0) {
38 "A $TILDE character cannot be placed on the first line of a leak trace"
39 }
40 continue
41 }
42
43 val nextLineWithUnderline = if (currentLineIndex < linesNotWrapped.lastIndex) {
44 linesNotWrapped[currentLineIndex + 1].run { if (TILDE in this) this else null }
45 } else null
46
47 val currentLineTrimmed = currentLine.trimEnd()
48 if (currentLineTrimmed.length <= maxWidth) {
49 linesWrapped += currentLineTrimmed
50 if (nextLineWithUnderline != null) {
51 linesWrapped += nextLineWithUnderline
52 }
53 } else {
54 linesWrapped += wrapLine(currentLineTrimmed, nextLineWithUnderline, maxWidth)
55 }
56 }
57 return linesWrapped.joinToString(separator = "\n") { it.trimEnd() }
58 }
59
60 private fun wrapLine(
61 currentLine: String,
62 nextLineWithUnderline: String?,
63 maxWidth: Int
64 ): List<String> {
65
66 val twoCharPrefixes = mapOf(
67 "├─" to "│ ",
68 "│ " to "│ ",
69 "╰→" to "$ZERO_SPACE_WIDTH ",
70 "$ZERO_SPACE_WIDTH " to "$ZERO_SPACE_WIDTH "
71 )
72
73 val twoCharPrefix = currentLine.substring(0, 2)
74 val prefixPastFirstLine: String
75 val prefixFirstLine: String
76 if (twoCharPrefix in twoCharPrefixes) {
77 val indexOfFirstNonWhitespace =
78 2 + currentLine.substring(2).indexOfFirst { !it.isWhitespace() }
79 prefixFirstLine = currentLine.substring(0, indexOfFirstNonWhitespace)
80 prefixPastFirstLine =
81 twoCharPrefixes[twoCharPrefix] + currentLine.substring(2, indexOfFirstNonWhitespace)
82 } else {
83 prefixFirstLine = ""
84 prefixPastFirstLine = ""
85 }
86
87 var lineRemainingChars = currentLine.substring(prefixFirstLine.length)
88
89 val maxWidthWithoutOffset = maxWidth - prefixFirstLine.length
90
91 val lineWrapped = mutableListOf<String>()
92 var periodsFound = 0
93
94 var updatedUnderlineStart: Int
95 val underlineStart: Int
96
97 if (nextLineWithUnderline != null) {
98 underlineStart = nextLineWithUnderline.indexOf(TILDE)
99 updatedUnderlineStart = underlineStart - prefixFirstLine.length
100 } else {
101 underlineStart = -1
102 updatedUnderlineStart = -1
103 }
104
105 var underlinedLineIndex = -1
106 while (lineRemainingChars.isNotEmpty() && lineRemainingChars.length > maxWidthWithoutOffset) {
107 val stringBeforeLimit = lineRemainingChars.substring(0, maxWidthWithoutOffset)
108
109 val lastIndexOfSpace = stringBeforeLimit.lastIndexOf(SPACE)
110 val lastIndexOfPeriod = stringBeforeLimit.lastIndexOf(PERIOD)
111
112 val lastIndexOfCurrentLine = lastIndexOfSpace.coerceAtLeast(lastIndexOfPeriod).let {
113 if (it == -1) {
114 stringBeforeLimit.lastIndex
115 } else {
116 it
117 }
118 }
119
120 if (lastIndexOfCurrentLine == lastIndexOfPeriod) {
121 periodsFound++
122 }
123
124 val wrapIndex = lastIndexOfCurrentLine + 1
125
126 // remove spaces at the end if any
127 lineWrapped += stringBeforeLimit.substring(0, wrapIndex).trimEnd()
128
129 // This line has an underline and we haven't find its new position after wrapping yet.
130 if (nextLineWithUnderline != null && underlinedLineIndex == -1) {
131 if (lastIndexOfCurrentLine < updatedUnderlineStart) {
132 updatedUnderlineStart -= wrapIndex
133 } else {
134 underlinedLineIndex = lineWrapped.lastIndex
135 }
136 }
137
138 lineRemainingChars = lineRemainingChars.substring(wrapIndex, lineRemainingChars.length)
139 }
140
141 // there are still residual words to be added, if we exit the loop with a non-empty line
142 if (lineRemainingChars.isNotEmpty()) {
143 lineWrapped += lineRemainingChars
144 }
145
146 if (nextLineWithUnderline != null) {
147 if (underlinedLineIndex == -1) {
148 underlinedLineIndex = lineWrapped.lastIndex
149 }
150 val underlineEnd = nextLineWithUnderline.lastIndexOf(TILDE)
151 val underlineLength = underlineEnd - underlineStart + 1
152
153 val spacesBeforeTilde = "$SPACE".repeat(updatedUnderlineStart)
154 val underlineTildes = "$TILDE".repeat(underlineLength)
155 lineWrapped.add(underlinedLineIndex + 1, "$spacesBeforeTilde$underlineTildes")
156 }
157
158 return lineWrapped.mapIndexed { index: Int, line: String ->
159 (if (index == 0) {
160 prefixFirstLine
161 } else {
162 prefixPastFirstLine
163 } + line).trimEnd()
164 }
165 }
166 }