/*******************************************************************************
* Copyright (c) 2015 Pivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.editor.support.completions;
import java.util.ArrayList;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.swt.graphics.Point;
import org.eclipse.text.edits.TextEdit;
/**
* Helper to make it easier to create composite modifications to IDocument.
* <p>
* It allows building up a sequence of edits which are all expressed in terms of
* offsets in the unmodified document. (So, when computing edits based on a
* some kind of AST its is not necessary to recompute the AST or update its
* position information after each small modification).
* <p>
* Similar functionality to Eclipse's {@link TextEdit} but unlike {@link TextEdit}
* it is not as finicky with respect to overlapping edits. We consider the
* order in which edits are created meaningful and give a logical semantics
* to edits that 'overlap'.
* <p>
* Also, each edit affects the cursor position placing it at the end of
* that edit. This will mostly do what you would want it to, provided that
* you save the edit where you want the cursor to end-up at for last.
*
* @author Kris De Volder
*/
public class DocumentEdits implements ProposalApplier {
// Note: for small number of edits this implementation is okay.
// for large number of edits it is potentially slow because of the
// way it transforms edit coordinates (a growing chain of
// OffsetTransformer is created so every extra edit added
// will take O(n) to preform the transform on its coordinates.
// So applying 'n' edits is O(n^2).
//
// A smarter way of doing this is possible. Here's a possible idea:
//
// For simplicity sake assume that all edits are 'independent' (i.e.
// changing their executing order doesn't matter.
//
// It is advantageous to sort the edits by position and execute them
// high to low because... we can then guarantee that the transform
// function that will apply to each edit does nothing on the coordinates
// that it cares about (since all prior edits only affect higher offets)
//
// Unfortunately the simplifying assumption does not allways hold.
// There are two problems:
//
// 1) updating the selection is order dependent.
// => this can be solved by observing that only the last
// edit operation need update the selection since it cancels
// all prior selections.
// => Mark the last operation with a 'flag' 'setSelection=true'
// and do not update the selection in any other operations.
//
// 2) some edits may not be independent
// => When the edits are sorted in descending order based on their 'end'
// coordinate them 'conflicting' edits should be adjacent and we can
// 'group them' together into 'cluster' where we can preserve their
// relative execution order.
// => While executing a 'cluster' we shall keep track of the offset
// transform function just like the current implementation does.
// => When the cluster of 'conflicting' operations has been dealt with
// the offset transform function no longer matters for the
// remaining edits who's offesets are all strictly 'smaller'.
// Thus the trasnform function can be discarded.
//
// Assuming most edits are independent and only a few of them conflict, then
// this algorithm can provide equivalent functinonality to the current one
// but for an 'average' performance which is O(n*log(n))
// Of course worst-case is still O(n^2) but we wouldn't expect to hit that case
// assuming we mostly have lots of small edits to disjoint sections of the document.
//
// So... edit operations could be sorted based on their position
// and executed in decreasing order of their 'start'.
//
// The tricky part would be to preserve the order-dependent semantics.
private class Insertion extends Edit {
private int offset;
private String text;
public Insertion(int offset, String insert) {
this.offset = offset;
this.text = insert;
}
@Override
void apply(DocumentState doc) throws BadLocationException {
doc.insert(offset, text);
}
@Override
public String toString() {
return "ins("+text+"@"+offset+")";
}
}
private abstract class Edit {
abstract void apply(DocumentState doc) throws BadLocationException;
public abstract String toString();
}
private class Deletion extends Edit {
private int start;
private int end;
public Deletion(int start, int end) {
this.start = start;
this.end = end;
}
@Override
void apply(DocumentState doc) throws BadLocationException {
doc.delete(start, end);
}
@Override
public String toString() {
return "del("+start+"->"+end+")";
}
}
private interface OffsetTransformer {
int trasform(int offset);
}
private static final OffsetTransformer NULL_TRANSFORM = new OffsetTransformer() {
public int trasform(int offset) {
return offset;
}
};
/**
* DocumentState provides methods to modify a document, its methods accept
* offsets expressed relative to the original document contents and keeps track
* of a OffsetTransformer that maps them to offsets in the current document.
*/
private static class DocumentState {
private IDocument doc; //may be null, in which case no actual modifications are performed
private OffsetTransformer org2new = NULL_TRANSFORM;
private int selection = -1; //-1 Means no edits where applied that change selection so
// the current selection is unknown
public DocumentState(IDocument doc) {
this.doc = doc;
}
public void insert(int start, final String text) throws BadLocationException {
final int tStart = org2new.trasform(start);
if (!text.isEmpty()) {
if (doc!=null) {
doc.replace(tStart, 0, text);
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
public int trasform(int org) {
int tOffset = parent.trasform(org);
if (tOffset<tStart) {
return tOffset;
} else {
return tOffset + text.length();
}
}
};
}
selection = tStart+text.length();
}
public void delete(final int start, final int end) throws BadLocationException {
final int tStart = org2new.trasform(start);
if (end>start) { // skip work for 'delete nothing' op
final int tEnd = org2new.trasform(end);
if (tEnd>tStart) { // skip work for 'delete nothing' op
if (doc!=null) {
doc.replace(tStart, tEnd-tStart, "");
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
public int trasform(int org) {
int tOffset = parent.trasform(org);
if (tOffset<=tStart) {
return tOffset;
} else if (tOffset>=tEnd) {
return tOffset - tEnd + tStart;
} else {
return start;
}
}
};
}
}
selection = tStart;
}
@Override
public String toString() {
if (doc==null) {
return super.toString();
}
StringBuilder buf = new StringBuilder();
buf.append("DocumentState(\n");
buf.append(doc.get()+"\n");
buf.append(")\n");
return buf.toString();
}
}
private ArrayList<Edit> edits = new ArrayList<Edit>();
private IDocument doc;
public DocumentEdits(IDocument doc) {
this.doc = doc;
}
public void delete(int start, int end) {
Assert.isLegal(start<=end);
edits.add(new Deletion(start, end));
}
public void delete(int offset, String text) {
delete(offset, offset+text.length());
}
public void insert(int offset, String insert) {
edits.add(new Insertion(offset, insert));
}
@Override
public Point getSelection(IDocument doc) throws Exception {
DocumentState selectionState = new DocumentState(null);
for (Edit edit : edits) {
edit.apply(selectionState);
}
if (selectionState.selection>=0) {
return new Point(selectionState.selection, 0);
}
return null;
}
@Override
public void apply(IDocument _doc) throws Exception {
DocumentState doc = new DocumentState(_doc);
for (Edit edit : edits) {
edit.apply(doc);
}
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("DocumentModifier(\n");
for (Edit edit : edits) {
buf.append(" "+edit);
}
buf.append(")\n");
return buf.toString();
}
public void moveCursorTo(int newCursor) {
insert(newCursor, "");
}
public void deleteLineBackwardAtOffset(int offset) throws Exception {
int line = doc.getLineOfOffset(offset);
deleteLineBackward(line);
}
/**
* Deletes the line of text with given line number, including either the following or
* preceding newline. If there is a choice between the preceding or following newline,
* the preceding newline is deleted. This will leave the cursor at the end of
* the preceding line.
* <p>
* Note: a similar operation 'deleteLineForward' could be implemented prefering to
* delete the following newline. This would be equivalent except that it will leave the
* cursor at the start of the following line.
*/
public void deleteLineBackward(int lineNumber) throws BadLocationException {
IRegion line = doc.getLineInformation(lineNumber);
int startOfDeletion;
int endOfDeletion;
if (lineNumber>0) {
IRegion previousLine = doc.getLineInformation(lineNumber-1);
startOfDeletion = endOf(previousLine);
endOfDeletion = endOf(line);
} else if (lineNumber<doc.getNumberOfLines()-1) {
IRegion nextLine = doc.getLineInformation(lineNumber+1);
startOfDeletion = line.getOffset();
endOfDeletion = nextLine.getOffset();
} else {
startOfDeletion = line.getOffset();
endOfDeletion = endOf(line);
}
delete(startOfDeletion, endOfDeletion);
}
private int endOf(IRegion line) {
return line.getOffset()+line.getLength();
}
public void replace(int start, int end, String newText) {
delete(start, end);
insert(start, newText);
}
}