1 /* 2 * Copyright (C) 2016 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.kotlinpoet 17 18 import java.io.Closeable 19 20 /** 21 * Implements soft line wrapping on an appendable. To use, append characters using 22 * [LineWrapper.append], which will replace spaces with newlines where necessary. Use 23 * [LineWrapper.appendNonWrapping] to append a string that never wraps. 24 */ 25 internal class LineWrapper( 26 private val out: Appendable, 27 private val indent: String, 28 private val columnLimit: Int, 29 ) : Closeable { 30 31 private var closed = false 32 33 /** 34 * Segments of the current line to be joined by spaces or wraps. Never empty, but contains a lone 35 * empty string if no data has been emitted since the last newline. 36 */ 37 private val segments = mutableListOf("") 38 39 /** Number of indents in wraps. -1 if the current line has no wraps. */ 40 private var indentLevel = -1 41 42 /** Optional prefix that will be prepended to wrapped lines. */ 43 private var linePrefix = "" 44 45 /** @return whether or not there are pending segments for the current line. */ 46 val hasPendingSegments get() = segments.size != 1 || segments[0].isNotEmpty() 47 48 /** Emit `s` replacing its spaces with line wraps as necessary. */ appendnull49 fun append(s: String, indentLevel: Int = -1, linePrefix: String = "") { 50 check(!closed) { "closed" } 51 52 var pos = 0 53 while (pos < s.length) { 54 when (s[pos]) { 55 ' ' -> { 56 // Each space starts a new empty segment. 57 this.indentLevel = indentLevel 58 this.linePrefix = linePrefix 59 segments += "" 60 pos++ 61 } 62 63 '\n' -> { 64 // Each newline emits the current segments. 65 newline() 66 pos++ 67 } 68 69 '·' -> { 70 // Render · as a non-breaking space. 71 segments[segments.size - 1] += " " 72 pos++ 73 } 74 75 else -> { 76 var next = s.indexOfAny(SPECIAL_CHARACTERS, pos) 77 if (next == -1) next = s.length 78 segments[segments.size - 1] += s.substring(pos, next) 79 pos = next 80 } 81 } 82 } 83 } 84 85 /** Emit `s` leaving spaces as-is. */ appendNonWrappingnull86 fun appendNonWrapping(s: String) { 87 check(!closed) { "closed" } 88 require(!s.contains("\n")) 89 90 segments[segments.size - 1] += s 91 } 92 newlinenull93 fun newline() { 94 check(!closed) { "closed" } 95 96 emitCurrentLine() 97 out.append("\n") 98 indentLevel = -1 99 } 100 101 /** Flush any outstanding text and forbid future writes to this line wrapper. */ closenull102 override fun close() { 103 emitCurrentLine() 104 closed = true 105 } 106 emitCurrentLinenull107 private fun emitCurrentLine() { 108 foldUnsafeBreaks() 109 110 var start = 0 111 var columnCount = segments[0].length 112 113 for (i in 1..<segments.size) { 114 val segment = segments[i] 115 val newColumnCount = columnCount + 1 + segment.length 116 117 // If this segment doesn't fit in the current run, print the current run and start a new one. 118 if (newColumnCount > columnLimit) { 119 emitSegmentRange(start, i) 120 start = i 121 columnCount = segment.length + indent.length * indentLevel 122 continue 123 } 124 125 columnCount = newColumnCount 126 } 127 128 // Print the last run. 129 emitSegmentRange(start, segments.size) 130 131 segments.clear() 132 segments += "" 133 } 134 emitSegmentRangenull135 private fun emitSegmentRange(startIndex: Int, endIndex: Int) { 136 // If this is a wrapped line we need a newline and an indent. 137 if (startIndex > 0) { 138 out.append("\n") 139 for (i in 0..<indentLevel) { 140 out.append(indent) 141 } 142 out.append(linePrefix) 143 } 144 145 // Emit each segment separated by spaces. 146 out.append(segments[startIndex]) 147 for (i in startIndex + 1..<endIndex) { 148 out.append(" ") 149 out.append(segments[i]) 150 } 151 } 152 153 /** 154 * Any segment that starts with '+' or '-' can't have a break preceding it. Combine it with the 155 * preceding segment. Note that this doesn't apply to the first segment. 156 */ foldUnsafeBreaksnull157 private fun foldUnsafeBreaks() { 158 var i = 1 159 while (i < segments.size) { 160 val segment = segments[i] 161 if (UNSAFE_LINE_START.matches(segment)) { 162 segments[i - 1] = segments[i - 1] + " " + segments[i] 163 segments.removeAt(i) 164 if (i > 1) i-- 165 } else { 166 i++ 167 } 168 } 169 } 170 171 companion object { 172 private val UNSAFE_LINE_START = Regex("\\s*[-+].*") 173 private val SPECIAL_CHARACTERS = " \n·".toCharArray() 174 } 175 } 176