<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 }