xref: /aosp_15_r20/external/kotlinpoet/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/LineWrapper.kt (revision 3c321d951dd070fb96f8ba59e952ffc3131379a0)
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