// 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.document;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonStringMap;
import com.google.collide.shared.document.Document.LineCountListener;
import com.google.collide.shared.document.Document.LineListener;
import com.google.collide.shared.document.Document.TextListener;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
// TODO: need the preferred newline characters for the doc
/**
* Document model for the code editor.
*
* The document is modeled using a linked list of lines. (This allows for very fast line insertions
* and still good performance for other common editor operations.)
*
* During a text change, listeners will be called in this order:
* <ul>
* <li>{@link Anchor.ShiftListener}</li>
* <li>{@link Anchor.RemoveListener}</li>
* <li>{@link LineCountListener}</li>
* <li>{@link LineListener}</li>
* <li>{@link TextListener}</li>
* </ul>
*/
public class Document implements DocumentMutator {
/**
* A listener that is called when the number of lines in the document changes.
*
* See the callback ordering documented in {@link Document}.
*/
public interface LineCountListener {
void onLineCountChanged(Document document, int lineCount);
}
/**
* A listener that is called when a line is added or removed from the
* document.
*
* Note: In the case of a multiline insertion/deletion, this will be called
* once.
*
* See the callback ordering documented in {@link Document}.
*/
public interface LineListener {
/**
* @param lineNumber the line number of the first item in {@code addedLines}
* @param addedLines a contiguous list of lines that were added
*/
void onLineAdded(Document document, int lineNumber, JsonArray<Line> addedLines);
/**
* @param lineNumber the previous line number of the first item in
* {@code removedLines}
* @param removedLines a contiguous list of (now detached) lines that were
* removed
*/
void onLineRemoved(Document document, int lineNumber, JsonArray<Line> removedLines);
}
/**
* A listener that is called when a text change occurs within a document.
*
* See the callback ordering documented in {@link Document}.
*/
public interface TextListener {
/**
* Note: You should not mutate the document within this callback, as this is
* not supported yet and can lead to other clients having stale position
* information inside the {@code textChanges}.
*
* Note: The {@link TextChange} contains a reference to the live
* {@link Line} from the document model. If you hold on to a reference after
* {@link #onTextChange} returns, beware that the contents of the
* {@link Line} could change, invalidating some of the state in the
* {@link TextChange}.
*/
void onTextChange(Document document, JsonArray<TextChange> textChanges);
}
/**
* A listener which is called before any changes are actually made to the
* document and any anchors are moved.
*/
public interface PreTextListener {
/**
* Note: You should not mutate the document within this callback, as this is
* not supported yet and can lead to other clients having stale position
* information inside the {@code textChanges}.
*
* <p>
* This callback is called synchronously with document mutations, the less
* work you can do the better.
*
* @param line The line the text change will take place on.
* @param lineNumber The line number of the line.
* @param column The column the text change will start at.
* @param text The text which is either being inserted or deleted.
* @param type The type of {@link TextChange} that will be occurring.
*/
void onPreTextChange(Document document,
TextChange.Type type,
Line line,
int lineNumber,
int column,
String text);
}
public static Document createEmpty() {
return new Document();
}
public static Document createFromString(
String contents) {
Document doc = createEmpty();
doc.insertText(doc.getFirstLine(), 0, 0, contents);
return doc;
}
private static int idCounter = 0;
private final AnchorManager anchorManager;
private Line firstLine;
private Line lastLine;
private int lineCount = 1;
private final ListenerManager<LineListener> lineListenerManager;
private final ListenerManager<LineCountListener> lineCountListenerManager;
private final LineFinder lineFinder;
private final DocumentMutatorImpl documentMutator;
private final ListenerManager<TextListener> textListenerManager;
private final ListenerManager<PreTextListener> preTextListenerManager;
private final int id = idCounter++;
private final JsonStringMap<Object> tags = JsonCollections.createMap();
private Document() {
firstLine = lastLine = Line.create(this, "");
firstLine.setAttached(true);
anchorManager = new AnchorManager();
documentMutator = new DocumentMutatorImpl(this);
lineListenerManager = ListenerManager.create();
lineCountListenerManager = ListenerManager.create();
lineFinder = new LineFinder(this);
textListenerManager = ListenerManager.create();
preTextListenerManager = ListenerManager.create();
}
public String asText() {
StringBuilder sb = new StringBuilder();
for (Line line = firstLine; line != null; line = line.getNextLine()) {
sb.append(line.getText());
}
return sb.toString();
}
@Override
public TextChange deleteText(Line line, int column, int deleteCount) {
return documentMutator.deleteText(line, column, deleteCount);
}
@Override
public TextChange deleteText(Line line, int lineNumber, int column, int deleteCount) {
return documentMutator.deleteText(line, lineNumber, column, deleteCount);
}
public AnchorManager getAnchorManager() {
return anchorManager;
}
public Line getFirstLine() {
return firstLine;
}
public LineInfo getFirstLineInfo() {
return new LineInfo(firstLine, 0);
}
public Line getLastLine() {
return lastLine;
}
public LineInfo getLastLineInfo() {
return new LineInfo(lastLine, getLastLineNumber());
}
public int getLastLineNumber() {
return lineCount - 1;
}
public int getLineCount() {
return lineCount;
}
public ListenerRegistrar<LineCountListener> getLineCountListenerRegistrar() {
return lineCountListenerManager;
}
public LineFinder getLineFinder() {
return lineFinder;
}
public ListenerRegistrar<LineListener> getLineListenerRegistrar() {
return lineListenerManager;
}
public String getText(Line line, int column, int count) {
assert column < line.getText().length();
StringBuilder s =
new StringBuilder(StringUtils.substringGuarded(line.getText(), column, count));
int remainingCount = count - s.length();
line = line.getNextLine();
while (remainingCount > 0 && line != null) {
String capturedLineText = StringUtils.substringGuarded(line.getText(), 0, remainingCount);
s.append(capturedLineText);
remainingCount -= capturedLineText.length();
line = line.getNextLine();
}
return s.toString();
}
public ListenerRegistrar<TextListener> getTextListenerRegistrar() {
return textListenerManager;
}
public ListenerRegistrar<PreTextListener> getPreTextListenerRegistrar() {
return preTextListenerManager;
}
@Override
public TextChange insertText(Line line, int column, String text) {
return documentMutator.insertText(line, column, text);
}
@Override
public TextChange insertText(Line line, int lineNumber, int column, String text) {
return documentMutator.insertText(line, lineNumber, column, text);
}
@Override
public TextChange insertText(Line line, int lineNumber, int column, String text,
boolean canReplaceSelection) {
return documentMutator.insertText(line, lineNumber, column, text, canReplaceSelection);
}
@Override
public String toString() {
return asText();
}
public String asDebugString() {
StringBuilder sb = new StringBuilder("Line count: " + getLineCount() + "\n");
for (Line line = firstLine; line != null; line = line.getNextLine()) {
sb.append(line.getText()).append("---\n");
}
return sb.toString();
}
public int getId() {
return id;
}
/**
* @see Line#putTag(String, Object)
*/
public <T> void putTag(String key, T value) {
tags.put(key, value);
}
/**
* @see Line#getTag(String)
*/
@SuppressWarnings("unchecked")
public <T> T getTag(String key) {
return (T) tags.get(key);
}
void commitLineCountChange(int lineCountDelta) {
if (lineCountDelta != 0) {
lineCount += lineCountDelta;
lineCountListenerManager.dispatch(new Dispatcher<Document.LineCountListener>() {
@Override
public void dispatch(LineCountListener listener) {
listener.onLineCountChanged(Document.this, lineCount);
}
});
}
}
void dispatchLineAdded(final int lineNumber, final JsonArray<Line> addedLines) {
lineListenerManager.dispatch(new Dispatcher<Document.LineListener>() {
@Override
public void dispatch(LineListener listener) {
listener.onLineAdded(Document.this, lineNumber, addedLines);
}
});
}
void dispatchLineRemoved(final int lineNumber, final JsonArray<Line> removedLines) {
lineListenerManager.dispatch(new Dispatcher<Document.LineListener>() {
@Override
public void dispatch(LineListener listener) {
listener.onLineRemoved(Document.this, lineNumber, removedLines);
}
});
}
void dispatchTextChange(final JsonArray<TextChange> textChanges) {
textListenerManager.dispatch(new Dispatcher<Document.TextListener>() {
@Override
public void dispatch(TextListener listener) {
listener.onTextChange(Document.this, textChanges);
}
});
}
void dispatchPreTextChange(final TextChange.Type type, final Line line, final int lineNumber,
final int column, final String text) {
preTextListenerManager.dispatch(new Dispatcher<Document.PreTextListener>() {
@Override
public void dispatch(PreTextListener listener) {
listener.onPreTextChange(Document.this, type, line, lineNumber, column, text);
}
});
}
void setFirstLine(Line line) {
assert line != null : "Line cannot be null";
firstLine = line;
}
void setLastLine(Line line) {
assert line != null : "Line cannot be null";
lastLine = line;
}
}