main

square/leakcanary

Last updated at: 29/12/2023 09:39

LeakTraceWrapper.kt

TLDR

The LeakTraceWrapper file in the Demo Projects project is responsible for performing word wrapping of leak traces. It contains a single wrap method that implements a greedy wrapping algorithm to wrap lines longer than a specified maximum width.

Methods

wrap

This method takes a source multiline string and a maximum width as input and returns the wrapped string. It implements a greedy wrapping algorithm to wrap lines that are longer than the maximum width. The algorithm takes the maximum amount of words that fit within the bounds of the maximum width and wraps the line at the first separator character (SPACE or PERIOD) found. It also handles the addition of underline characters when necessary. All lines start with an offset which includes a decorator character and some level of indentation.

END

package leakcanary.internal.activity.screen

/**
 * 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()
    }
  }
}