// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.collide.shared.ot;
import static com.google.collide.dto.DocOpComponent.Type.DELETE;
import static com.google.collide.dto.DocOpComponent.Type.INSERT;
import static com.google.collide.dto.DocOpComponent.Type.RETAIN;
import static com.google.collide.dto.DocOpComponent.Type.RETAIN_LINE;
import com.google.collide.dto.DocOp;
import com.google.collide.dto.DocOpComponent;
import com.google.collide.dto.DocOpComponent.Delete;
import com.google.collide.dto.DocOpComponent.Insert;
import com.google.collide.dto.DocOpComponent.Retain;
import com.google.collide.dto.DocOpComponent.RetainLine;
import com.google.collide.dto.shared.DocOpFactory;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.util.StringUtils;
import com.google.common.base.Preconditions;
import java.util.List;
/**
* Utility methods for document operation manipulation.
*
*/
public final class DocOpUtils {
public static void accept(DocOp docOp, DocOpCursor visitor) {
JsonArray<DocOpComponent> components = docOp.getComponents();
for (int i = 0, n = components.size(); i < n; i++) {
acceptComponent(components.get(i), visitor);
}
}
public static void acceptComponent(DocOpComponent component, DocOpCursor visitor) {
switch (component.getType()) {
case DELETE:
visitor.delete(((Delete) component).getText());
break;
case INSERT:
visitor.insert(((Insert) component).getText());
break;
case RETAIN:
Retain retain = (Retain) component;
visitor.retain(retain.getCount(), retain.hasTrailingNewline());
break;
case RETAIN_LINE:
visitor.retainLine(((RetainLine) component).getLineCount());
break;
default:
throw new IllegalArgumentException(
"Unknown doc op component with ordinal " + component.getType());
}
}
public static DocOp createFromTextChange(DocOpFactory factory, TextChange textChange) {
DocOp docOp = factory.createDocOp();
JsonArray<DocOpComponent> components = docOp.getComponents();
int lineNumber = textChange.getLineNumber();
if (lineNumber > 0) {
components.add(factory.createRetainLine(lineNumber));
}
int column = textChange.getColumn();
if (column > 0) {
components.add(factory.createRetain(column, false));
}
String text = textChange.getText();
/*
* Split the potentially multiline text into a component per line
*/
JsonArray<String> lineTexts = StringUtils.split(text, "\n");
// Create components for all but the last line
int nMinusOne = lineTexts.size() - 1;
for (int i = 0; i < nMinusOne; i++) {
components.add(createComponentFromTextChange(factory, textChange, lineTexts.get(i) + "\n"));
}
String lastLineText = lineTexts.get(nMinusOne);
if (!lastLineText.isEmpty()) {
// Create a component for the last line
components.add(createComponentFromTextChange(factory, textChange, lastLineText));
}
// Create a retain, if required
int remainingRetainCount;
int numNewlines = lineTexts.size() - 1;
if (textChange.getType() == TextChange.Type.INSERT) {
Line lastModifiedLine = LineUtils.getLine(textChange.getLine(), numNewlines);
remainingRetainCount = lastModifiedLine.getText().length() - lastLineText.length();
if (numNewlines == 0) {
remainingRetainCount -= column;
}
} else { // DELETE
remainingRetainCount = textChange.getLine().getText().length() - column;
}
// Create a retain line, if required
int docLineCount = textChange.getLine().getDocument().getLineCount();
int numNewlinesFromTextChangeInCurDoc =
textChange.getType() == TextChange.Type.DELETE ? 0 : numNewlines;
int remainingLineCount = docLineCount - (lineNumber + numNewlinesFromTextChangeInCurDoc + 1);
// Add the retain and retain line components
if (remainingRetainCount > 0) {
// This retain has a trailing new line if it is NOT on the last line
components.add(factory.createRetain(remainingRetainCount, remainingLineCount > 0));
}
if (remainingLineCount > 0) {
components.add(factory.createRetainLine(remainingLineCount));
} else {
Preconditions.checkState(remainingLineCount == 0, "How is it negative?");
/*
* If the retainingLineCount calculation resulted in 0, there's still a
* chance that there is a empty last line that needs to be retained. Our
* contract says if the resulting document (that is, the document in its
* state right now) has an empty last line, we should have a RetainLine
* that accounts for it. In addition if the document contained an empty
* last line before the delete we should also emit a retain line.
*
* Since we didn't emit the RetainLine above (since remainingLineCount ==
* 0), we can check and emit one here.
*/
// to check if the document ended in a new line before the change, we check if the change
// endsWith \n and remainingRetainCount = 0;
boolean didDocumentEndInEmptyLineBeforeDelete = textChange.getType() == TextChange.Type.DELETE
&& remainingRetainCount == 0 && text.endsWith("\n");
boolean isLastLineEmptyAfterTextChange =
textChange.getLine().getDocument().getLastLine().getText().length() == 0;
if (isLastLineEmptyAfterTextChange || didDocumentEndInEmptyLineBeforeDelete) {
components.add(factory.createRetainLine(1));
}
}
return docOp;
}
/**
* Creates a single doc op composed of docops converted from a collection
* of text changes. For a single text change use
* {@link #createFromTextChange(DocOpFactory, TextChange)}.
*
* @param factory doc ops factory
* @param textChanges list of changes to convert to doc op
* @return composed doc op, or {@code null} if text changes array is empty
* @throws Composer.ComposeException if error happens during composal
*/
public static DocOp createFromTextChanges(DocOpFactory factory,
JsonArray<TextChange> textChanges) throws Composer.ComposeException {
DocOp result = null;
for (int i = 0, n = textChanges.size(); i < n; i++) {
TextChange textChange = textChanges.get(i);
DocOp curOp = DocOpUtils.createFromTextChange(factory, textChange);
result = result != null ? Composer.compose(factory, result, curOp) : curOp;
}
return result;
}
public static boolean containsMutation(Iterable<DocOp> docOps) {
for (DocOp docOp : docOps) {
if (containsMutation(docOp)) {
return true;
}
}
return false;
}
public static boolean containsMutation(DocOp docOp) {
for (int i = 0; i < docOp.getComponents().size(); i++) {
DocOpComponent component = docOp.getComponents().get(i);
switch (component.getType()) {
case DocOpComponent.Type.DELETE:
case DocOpComponent.Type.INSERT:
return true;
case DocOpComponent.Type.RETAIN:
case DocOpComponent.Type.RETAIN_LINE:
// Retains do not dirty the contents of a file
break;
default:
throw new IllegalArgumentException("Got an unknown doc op type " + component.getType());
}
}
return false;
}
public static String toString(DocOp docOp, boolean verbose) {
StringBuilder sb = new StringBuilder();
for (int i = 0, n = docOp.getComponents().size(); i < n; i++) {
sb.append(toString(docOp.getComponents().get(i), verbose));
}
return sb.toString();
}
public static String toString(DocOpComponent component, boolean verbose) {
switch (component.getType()) {
case DELETE:
String deleteText = ((Delete) component).getText();
return "D(" + toStringForComponentText(deleteText, verbose) + ")";
case INSERT:
String insertText = ((Insert) component).getText();
return "I(" + toStringForComponentText(insertText, verbose) + ")";
case RETAIN:
Retain retain = (Retain) component;
return "R(" + (retain.hasTrailingNewline() ? (retain.getCount() - 1) + "\\n" : ""
+ retain.getCount()) + ")";
case RETAIN_LINE:
return "RL(" + ((RetainLine) component).getLineCount() + ")";
default:
return "?(???)";
}
}
public static String toString(
List<? extends DocOp> docOps, int firstIndex, int lastIndex, boolean verbose) {
StringBuilder sb = new StringBuilder("[");
for (int i = firstIndex; i <= lastIndex; i++) {
DocOp docOp = docOps.get(i);
if (docOp == null) {
sb.append("<null doc op>,");
} else {
sb.append(toString(docOp, verbose)).append(',');
}
}
sb.setLength(sb.length() - 1);
sb.append(']');
return sb.toString();
}
private static DocOpComponent createComponentFromTextChange(
DocOpFactory factory, TextChange textChange, String text) {
switch (textChange.getType()) {
case INSERT:
return factory.createInsert(text);
case DELETE:
return factory.createDelete(text);
default:
throw new IllegalArgumentException(
"Unknown text change type with ordinal " + textChange.getType().ordinal());
}
}
private static String toStringForComponentText(String componentText, boolean verbose) {
if (verbose) {
return componentText.endsWith("\n") ? componentText.substring(0, componentText.length() - 1)
+ "\\n" : componentText;
} else {
return componentText.endsWith("\n") ? (componentText.length() - 1) + "\\n" : ""
+ componentText.length();
}
}
}