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 * http://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.javapoet; 17 18 import java.io.IOException; 19 20 import static com.squareup.javapoet.Util.checkNotNull; 21 22 /** 23 * Implements soft line wrapping on an appendable. To use, append characters using {@link #append} 24 * or soft-wrapping spaces using {@link #wrappingSpace}. 25 */ 26 final class LineWrapper { 27 private final RecordingAppendable out; 28 private final String indent; 29 private final int columnLimit; 30 private boolean closed; 31 32 /** Characters written since the last wrapping space that haven't yet been flushed. */ 33 private final StringBuilder buffer = new StringBuilder(); 34 35 /** The number of characters since the most recent newline. Includes both out and the buffer. */ 36 private int column = 0; 37 38 /** 39 * -1 if we have no buffering; otherwise the number of {@code indent}s to write after wrapping. 40 */ 41 private int indentLevel = -1; 42 43 /** 44 * Null if we have no buffering; otherwise the type to pass to the next call to {@link #flush}. 45 */ 46 private FlushType nextFlush; 47 LineWrapper(Appendable out, String indent, int columnLimit)48 LineWrapper(Appendable out, String indent, int columnLimit) { 49 checkNotNull(out, "out == null"); 50 this.out = new RecordingAppendable(out); 51 this.indent = indent; 52 this.columnLimit = columnLimit; 53 } 54 55 /** @return the last emitted char or {@link Character#MIN_VALUE} if nothing emitted yet. */ lastChar()56 char lastChar() { 57 return out.lastChar; 58 } 59 60 /** Emit {@code s}. This may be buffered to permit line wraps to be inserted. */ append(String s)61 void append(String s) throws IOException { 62 if (closed) throw new IllegalStateException("closed"); 63 64 if (nextFlush != null) { 65 int nextNewline = s.indexOf('\n'); 66 67 // If s doesn't cause the current line to cross the limit, buffer it and return. We'll decide 68 // whether or not we have to wrap it later. 69 if (nextNewline == -1 && column + s.length() <= columnLimit) { 70 buffer.append(s); 71 column += s.length(); 72 return; 73 } 74 75 // Wrap if appending s would overflow the current line. 76 boolean wrap = nextNewline == -1 || column + nextNewline > columnLimit; 77 flush(wrap ? FlushType.WRAP : nextFlush); 78 } 79 80 out.append(s); 81 int lastNewline = s.lastIndexOf('\n'); 82 column = lastNewline != -1 83 ? s.length() - lastNewline - 1 84 : column + s.length(); 85 } 86 87 /** Emit either a space or a newline character. */ wrappingSpace(int indentLevel)88 void wrappingSpace(int indentLevel) throws IOException { 89 if (closed) throw new IllegalStateException("closed"); 90 91 if (this.nextFlush != null) flush(nextFlush); 92 column++; // Increment the column even though the space is deferred to next call to flush(). 93 this.nextFlush = FlushType.SPACE; 94 this.indentLevel = indentLevel; 95 } 96 97 /** Emit a newline character if the line will exceed it's limit, otherwise do nothing. */ zeroWidthSpace(int indentLevel)98 void zeroWidthSpace(int indentLevel) throws IOException { 99 if (closed) throw new IllegalStateException("closed"); 100 101 if (column == 0) return; 102 if (this.nextFlush != null) flush(nextFlush); 103 this.nextFlush = FlushType.EMPTY; 104 this.indentLevel = indentLevel; 105 } 106 107 /** Flush any outstanding text and forbid future writes to this line wrapper. */ close()108 void close() throws IOException { 109 if (nextFlush != null) flush(nextFlush); 110 closed = true; 111 } 112 113 /** Write the space followed by any buffered text that follows it. */ flush(FlushType flushType)114 private void flush(FlushType flushType) throws IOException { 115 switch (flushType) { 116 case WRAP: 117 out.append('\n'); 118 for (int i = 0; i < indentLevel; i++) { 119 out.append(indent); 120 } 121 column = indentLevel * indent.length(); 122 column += buffer.length(); 123 break; 124 case SPACE: 125 out.append(' '); 126 break; 127 case EMPTY: 128 break; 129 default: 130 throw new IllegalArgumentException("Unknown FlushType: " + flushType); 131 } 132 133 out.append(buffer); 134 buffer.delete(0, buffer.length()); 135 indentLevel = -1; 136 nextFlush = null; 137 } 138 139 private enum FlushType { 140 WRAP, SPACE, EMPTY; 141 } 142 143 /** A delegating {@link Appendable} that records info about the chars passing through it. */ 144 static final class RecordingAppendable implements Appendable { 145 private final Appendable delegate; 146 147 char lastChar = Character.MIN_VALUE; 148 RecordingAppendable(Appendable delegate)149 RecordingAppendable(Appendable delegate) { 150 this.delegate = delegate; 151 } 152 append(CharSequence csq)153 @Override public Appendable append(CharSequence csq) throws IOException { 154 int length = csq.length(); 155 if (length != 0) { 156 lastChar = csq.charAt(length - 1); 157 } 158 return delegate.append(csq); 159 } 160 append(CharSequence csq, int start, int end)161 @Override public Appendable append(CharSequence csq, int start, int end) throws IOException { 162 CharSequence sub = csq.subSequence(start, end); 163 return append(sub); 164 } 165 append(char c)166 @Override public Appendable append(char c) throws IOException { 167 lastChar = c; 168 return delegate.append(c); 169 } 170 } 171 } 172