LeakTraceWrapper.kt
TLDR
The provided file LeakTraceWrapper.kt
is a Kotlin file in the org.leakcanary.util
package. It contains the implementation of a word wrapping algorithm for leak traces.
Methods
wrap
This method performs the word wrapping of leak traces. It takes a source multiline string and a maximum width as input, and returns the wrapped string.
END
package org.leakcanary.util
/**
* Performs word wrapping of leak traces.
*/
internal object LeakTraceWrapper {
private const val SPACE = '\u0020'
private const val TILDE = '\u007E'
private const val PERIOD = '\u002E'
private const val ZERO_SPACE_WIDTH = '\u200B'
/**
* This implements a greedy wrapping algorithm.
*
* Each line that is longer than [maxWidth], is wrapped by taking the maximum amount of words that fit
* within the bounds delimited by [maxWidth]. This is done by walking back from the character at [maxWidth]
* position, until the first separator is found (a [SPACE] or [PERIOD]).
*
* Additionally, [Underline] characters are tracked and added when necessary.
*
* Finally, all lines start with an offset which includes a decorator character and some level of
* indentation.
*/
fun wrap(
sourceMultilineString: String,
maxWidth: Int
): String {
// Lines without terminating line separators
val linesNotWrapped = sourceMultilineString.lines()
val linesWrapped = mutableListOf<String>()
for (currentLineIndex in linesNotWrapped.indices) {
val currentLine = linesNotWrapped[currentLineIndex]
if (TILDE in currentLine) {
check(currentLineIndex > 0) {
"A $TILDE character cannot be placed on the first line of a leak trace"
}
continue
}
val nextLineWithUnderline = if (currentLineIndex < linesNotWrapped.lastIndex) {
linesNotWrapped[currentLineIndex + 1].run { if (TILDE in this) this else null }
} else null
val currentLineTrimmed = currentLine.trimEnd()
if (currentLineTrimmed.length <= maxWidth) {
linesWrapped += currentLineTrimmed
if (nextLineWithUnderline != null) {
linesWrapped += nextLineWithUnderline
}
} else {
linesWrapped += wrapLine(currentLineTrimmed, nextLineWithUnderline, maxWidth)
}
}
return linesWrapped.joinToString(separator = "\n") { it.trimEnd() }
}
private fun wrapLine(
currentLine: String,
nextLineWithUnderline: String?,
maxWidth: Int
): List<String> {
val twoCharPrefixes = mapOf(
"├─" to "│ ",
"│ " to "│ ",
"╰→" to "$ZERO_SPACE_WIDTH ",
"$ZERO_SPACE_WIDTH " to "$ZERO_SPACE_WIDTH "
)
val twoCharPrefix = currentLine.substring(0, 2)
val prefixPastFirstLine: String
val prefixFirstLine: String
if (twoCharPrefix in twoCharPrefixes) {
val indexOfFirstNonWhitespace =
2 + currentLine.substring(2).indexOfFirst { !it.isWhitespace() }
prefixFirstLine = currentLine.substring(0, indexOfFirstNonWhitespace)
prefixPastFirstLine =
twoCharPrefixes[twoCharPrefix] + currentLine.substring(2, indexOfFirstNonWhitespace)
} else {
prefixFirstLine = ""
prefixPastFirstLine = ""
}
var lineRemainingChars = currentLine.substring(prefixFirstLine.length)
val maxWidthWithoutOffset = maxWidth - prefixFirstLine.length
val lineWrapped = mutableListOf<String>()
var periodsFound = 0
var updatedUnderlineStart: Int
val underlineStart: Int
if (nextLineWithUnderline != null) {
underlineStart = nextLineWithUnderline.indexOf(TILDE)
updatedUnderlineStart = underlineStart - prefixFirstLine.length
} else {
underlineStart = -1
updatedUnderlineStart = -1
}
var underlinedLineIndex = -1
while (lineRemainingChars.isNotEmpty() && lineRemainingChars.length > maxWidthWithoutOffset) {
val stringBeforeLimit = lineRemainingChars.substring(0, maxWidthWithoutOffset)
val lastIndexOfSpace = stringBeforeLimit.lastIndexOf(SPACE)
val lastIndexOfPeriod = stringBeforeLimit.lastIndexOf(PERIOD)
val lastIndexOfCurrentLine = lastIndexOfSpace.coerceAtLeast(lastIndexOfPeriod).let {
if (it == -1) {
stringBeforeLimit.lastIndex
} else {
it
}
}
if (lastIndexOfCurrentLine == lastIndexOfPeriod) {
periodsFound++
}
val wrapIndex = lastIndexOfCurrentLine + 1
// remove spaces at the end if any
lineWrapped += stringBeforeLimit.substring(0, wrapIndex).trimEnd()
// This line has an underline and we haven't find its new position after wrapping yet.
if (nextLineWithUnderline != null && underlinedLineIndex == -1) {
if (lastIndexOfCurrentLine < updatedUnderlineStart) {
updatedUnderlineStart -= wrapIndex
} else {
underlinedLineIndex = lineWrapped.lastIndex
}
}
lineRemainingChars = lineRemainingChars.substring(wrapIndex, lineRemainingChars.length)
}
// there are still residual words to be added, if we exit the loop with a non-empty line
if (lineRemainingChars.isNotEmpty()) {
lineWrapped += lineRemainingChars
}
if (nextLineWithUnderline != null) {
if (underlinedLineIndex == -1) {
underlinedLineIndex = lineWrapped.lastIndex
}
val underlineEnd = nextLineWithUnderline.lastIndexOf(TILDE)
val underlineLength = underlineEnd - underlineStart + 1
val spacesBeforeTilde = "$SPACE".repeat(updatedUnderlineStart)
val underlineTildes = "$TILDE".repeat(underlineLength)
lineWrapped.add(underlinedLineIndex + 1, "$spacesBeforeTilde$underlineTildes")
}
return lineWrapped.mapIndexed { index: Int, line: String ->
(if (index == 0) {
prefixFirstLine
} else {
prefixPastFirstLine
} + line).trimEnd()
}
}
}