/** * Copyright (c) 2012 Cloudsmith Inc. and other contributors, as listed below. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Cloudsmith * */ package org.cloudsmith.xtext.textflow; import org.cloudsmith.xtext.dommodel.formatter.context.IFormattingContext; import com.google.inject.Inject; /** * A text flow that measures the appended content but that does not contain the actual text. * */ public class MeasuredTextFlow extends AbstractTextFlow { private int lastWasBreak; private boolean lastWasSpace; private int numberOfBreaks; private int currentLineWidth; private int maxWidth; private int lastLineWidth; private int lastUsedIndent; private CharSequence currentRun; private int pendingIndent; private boolean indentFirstLine; private int indentAtRunStart; @Inject public MeasuredTextFlow(IFormattingContext formattingContext) { super(formattingContext); this.lastWasBreak = 0; // false; this.lastWasSpace = false; numberOfBreaks = 0; currentLineWidth = 0; maxWidth = 0; pendingIndent = 0; indentAtRunStart = 0; } /** * Copy constructor * * @param original */ public MeasuredTextFlow(MeasuredTextFlow original) { super(original); this.lastWasBreak = original.lastWasBreak; this.lastWasSpace = original.lastWasSpace; this.numberOfBreaks = original.numberOfBreaks; this.currentLineWidth = original.currentLineWidth; this.lastUsedIndent = original.lastUsedIndent; this.currentRun = original.currentRun; this.pendingIndent = original.pendingIndent; this.indentFirstLine = original.indentFirstLine; this.indentAtRunStart = original.indentAtRunStart; } @Override public ITextFlow appendBreaks(int count, boolean verbatim) { if(currentRun != null) { doText(currentRun, verbatim); currentRun = null; indentAtRunStart = this.getIndentation(); } lastWasSpace = true; if(count <= 0) return this; lastWasBreak += count; // = true; numberOfBreaks += count; maxWidth = Math.max(maxWidth, currentLineWidth); lastLineWidth = currentLineWidth == 0 ? lastLineWidth : currentLineWidth; currentLineWidth = 0; // verbatim break simply means, no indentation pendingIndent = verbatim ? 0 : indent; return this; } @Override public ITextFlow appendSpaces(int count) { if(currentRun != null) { CharSequence run = currentRun; currentRun = null; doText(run, true); indentAtRunStart = this.getIndentation(); } emit(count); lastWasSpace = true; return this; } /** * Must be called from a derived method since this method performs measuring. * * @param s * the text that will be emitted. */ @Override protected void doText(CharSequence s, boolean verbatim) { emit(s.length()); if(s.length() > 0) lastWasSpace = false; } private void emit(int count) { if(count == 0) return; // do not change state if(lastWasBreak > 0) { lastWasBreak = 0; // false; lastUsedIndent = pendingIndent; // was indent, does not work when buffering currentLineWidth += pendingIndent; // was indent, does not work when buffering } currentLineWidth += Math.max(0, count); } @Override public boolean endsWithBreak() { return lastWasBreak > 0 && currentRun == null; } @Override public ITextFlow ensureBreaks(int count) { if(!endsWithBreak()) return appendBreaks(count, false); int missingBreaks = Math.max(0, count - lastWasBreak); if(missingBreaks == 0) pendingIndent = indent; else return appendBreaks(missingBreaks, false); return this; } @Override public int getAppendLinePosition() { if(lastWasBreak > 0) return pendingIndent + (currentRun == null ? 0 : currentRun.length()); return currentLineWidth + getRunWidth(); } protected CharSequence getCurrentRun() { return currentRun == null ? "" : currentRun; } @Override public int getEndBreakCount() { // if there is pending text, then the lastWasBreak is pending and not at the end at all // report as 0 return currentRun != null ? 0 : lastWasBreak; } @Override public int getHeight() { if(isEmpty()) return 0; if(lastWasBreak > 0) return numberOfBreaks; return numberOfBreaks + 1; } @Override public int getLastUsedIndentation() { return lastUsedIndent / indentSize; } protected int getPendingIndent() { return pendingIndent; } private int getRunWidth() { return currentRun == null ? 0 : currentRun.length(); } @Override public int getWidth() { // break only, or break + unprocessed if(lastWasBreak > 0) return Math.max(maxWidth, currentRun == null ? 0 : currentRun.length() + pendingIndent); // something else than break processed, but there can be unprocessed return Math.max(maxWidth, currentLineWidth + getRunWidth()); } @Override public int getWidthOfLastLine() { if(lastWasBreak > 0) return currentRun == null ? lastLineWidth : pendingIndent + currentRun.length(); return currentLineWidth + getRunWidth(); } @Override public boolean isEmpty() { return getWidth() == 0 && numberOfBreaks == 0; } @Override public boolean isIndentFirstLine() { return indentFirstLine; } /** * This implementation buffers non-breakable sequences and performs auto line wrapping if <code>verbatim</code> is <code>false</code>. * When output is <i>verbatim</i> pending output is flushed, and new output is immediately processed, and no * automatic line wrapping will take place. */ @Override protected void processTextSequence(CharSequence s, boolean verbatim) { if(s == null || s.length() == 0) return; // no text, do nothing if(verbatim) { if(currentRun != null) { doText(currentRun, false); // if there was a current run, it is not verbatim currentRun = null; indentAtRunStart = this.getIndentation(); } doText(s, true); return; } currentRun = CharSequences.concatenate(currentRun, s); // handles null if(shouldLineBeWrapped(currentRun)) { // wrap indent, output text, restore indent CharSequence processRun = currentRun; currentRun = null; int tmpIndentation = getIndentation(); setIndentation(indentAtRunStart); // changeIndentation(getWrapIndentation()); appendBreak(); setIndentation(tmpIndentation); super.processTextSequence(processRun, verbatim); // changeIndentation(-getWrapIndentation()); } // do nothing, just keep the currentRun } @Override public ITextFlow setIndentation(int count) { indent = Math.max(0, count * indentSize); return this; } @Override public void setIndentFirstLine(boolean flag) { indentFirstLine = flag; // if not empty, just remembers the flag if(!isEmpty()) return; if(flag) pendingIndent = indent; lastWasBreak = 1; // fake a break } /** * Returns true if the text would cause text to be wider that the preferred max width, and placing * it on the next line with the current indent would either make it fit or cause less overrun. * Otherwise false is returned. * * @param s * @return true if the given characters should be placed on the next line */ protected boolean shouldLineBeWrapped(CharSequence s) { final int textLength = s.length(); final int pos = lastWasBreak > 0 ? pendingIndent // indent for preceding break : currentLineWidth; int unwrappedWidth = textLength + pos; if(unwrappedWidth > getPreferredMaxWidth()) { if(!(lastWasBreak > 0 || lastWasSpace)) return false; // not allowed to wrap // note: use indent here, it is the indent for next break made int wrappedWidth = textLength + (indent + getWrapIndentation()) * indentSize; return wrappedWidth < unwrappedWidth; } return false; } }