/*
* UndoManager.java - Buffer undo manager
* :tabSize=4:indentSize=4:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 2001, 2005 Slava Pestov
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.gjt.sp.jedit.buffer;
//{{{ Imports
import org.gjt.sp.util.IntegerArray;
import org.gjt.sp.util.Log;
import org.gjt.sp.jedit.textarea.Selection;
//}}}
/**
* A class internal to jEdit's document model. You should not use it
* directly. To improve performance, none of the methods in this class
* check for out of bounds access, nor are they thread-safe. The
* <code>Buffer</code> class, through which these methods must be
* called through, implements such protection.
*
* @author Slava Pestov
* @version $Id: UndoManager.java 22171 2012-09-06 15:12:05Z ezust $
* @since jEdit 4.0pre1
*/
public class UndoManager
{
//{{{ UndoManager constructor
public UndoManager(JEditBuffer buffer)
{
this.buffer = buffer;
} //}}}
//{{{ setLimit() method
public void setLimit(int limit)
{
this.limit = limit;
} //}}}
//{{{ clear() method
public void clear()
{
undosFirst = undosLast = redosFirst = null;
undoCount = 0;
} //}}}
//{{{ canUndo() method
public boolean canUndo()
{
return (undosLast != null);
} //}}}
//{{{ undo() method
public Selection[] undo()
{
if(insideCompoundEdit())
throw new InternalError("Unbalanced begin/endCompoundEdit()");
if(undosLast == null)
return null;
else
{
reviseUndoId();
undoCount--;
Selection s[] = undosLast.undo(this);
redosFirst = undosLast;
undosLast = undosLast.prev;
if(undosLast == null)
undosFirst = null;
return s;
}
} //}}}
//{{{ canRedo() method
public boolean canRedo()
{
return (redosFirst != null);
} //}}}
//{{{ redo() method
public Selection[] redo()
{
if(insideCompoundEdit())
throw new InternalError("Unbalanced begin/endCompoundEdit()");
if(redosFirst == null)
return null;
else
{
reviseUndoId();
undoCount++;
Selection[] s = redosFirst.redo(this);
undosLast = redosFirst;
if(undosFirst == null)
undosFirst = undosLast;
redosFirst = redosFirst.next;
return s;
}
} //}}}
//{{{ beginCompoundEdit() method
public void beginCompoundEdit()
{
if(compoundEditCount == 0)
{
compoundEdit = new CompoundEdit();
reviseUndoId();
}
compoundEditCount++;
} //}}}
//{{{ endCompoundEdit() method
public void endCompoundEdit()
{
if(compoundEditCount == 0)
{
Log.log(Log.WARNING,this,new Exception("Unbalanced begin/endCompoundEdit()"));
return;
}
else if(compoundEditCount == 1)
{
if(compoundEdit.first == null)
/* nothing done between begin/end calls */;
else if(compoundEdit.first == compoundEdit.last)
addEdit(compoundEdit.first);
else
addEdit(compoundEdit);
compoundEdit = null;
}
compoundEditCount--;
} //}}}
//{{{ insideCompoundEdit() method
public boolean insideCompoundEdit()
{
return compoundEditCount != 0;
} //}}}
//{{{ getUndoId() method
public Object getUndoId()
{
return undoId;
} //}}}
//{{{ contentInserted() method
public void contentInserted(int offset, int length, String text, boolean clearDirty)
{
Edit toMerge = getMergeEdit();
if(!clearDirty && toMerge instanceof Insert
&& redosFirst == null)
{
Insert ins = (Insert)toMerge;
if(ins.offset == offset)
{
ins.str = text.concat(ins.str);
return;
}
else if(ins.offset + ins.str.length() == offset)
{
ins.str = ins.str.concat(text);
return;
}
}
Insert ins = new Insert(offset,text);
if(clearDirty)
{
redoClearDirty = getLastEdit();
undoClearDirty = ins;
}
if(compoundEdit != null)
compoundEdit.add(this, ins);
else
{
reviseUndoId();
addEdit(ins);
}
} //}}}
//{{{ contentRemoved() method
public void contentRemoved(int offset, int length, String text, boolean clearDirty)
{
Edit toMerge = getMergeEdit();
if(!clearDirty && toMerge instanceof Remove
&& redosFirst == null)
{
Remove rem = (Remove)toMerge;
if(rem.offset == offset)
{
String newStr = rem.str.concat(text);
KillRing.getInstance().changed(rem.str, newStr);
rem.str = newStr;
return;
}
else if(offset + length == rem.offset)
{
String newStr = text.concat(rem.str);
KillRing.getInstance().changed(rem.str, newStr);
rem.offset = offset;
rem.str = newStr;
return;
}
}
// use String.intern() here as new Strings are created in
// JEditBuffer.remove() via undoMgr.contentRemoved(... getText() ...);
Remove rem = new Remove(offset,text.intern());
if(clearDirty)
{
redoClearDirty = getLastEdit();
undoClearDirty = rem;
}
if(compoundEdit != null)
compoundEdit.add(this, rem);
else
{
reviseUndoId();
addEdit(rem);
}
KillRing.getInstance().add(rem.str);
} //}}}
//{{{ resetClearDirty method
public void resetClearDirty()
{
redoClearDirty = getLastEdit();
if(redosFirst instanceof CompoundEdit)
undoClearDirty = ((CompoundEdit)redosFirst).first;
else
undoClearDirty = redosFirst;
} //}}}
//{{{ Private members
//{{{ Instance variables
private JEditBuffer buffer;
// queue of undos. last is most recent, first is oldest
private Edit undosFirst;
private Edit undosLast;
// queue of redos. first is most recent, last is oldest
private Edit redosFirst;
private int limit;
private int undoCount;
private int compoundEditCount;
private CompoundEdit compoundEdit;
private Edit undoClearDirty, redoClearDirty;
private Object undoId;
//}}}
//{{{ addEdit() method
private void addEdit(Edit edit)
{
if(undosFirst == null)
undosFirst = undosLast = edit;
else
{
undosLast.next = edit;
edit.prev = undosLast;
undosLast = edit;
}
redosFirst = null;
undoCount++;
while(undoCount > limit)
{
undoCount--;
if(undosFirst == undosLast)
undosFirst = undosLast = null;
else
{
undosFirst.next.prev = null;
undosFirst = undosFirst.next;
}
}
} //}}}
//{{{ getMergeEdit() method
private Edit getMergeEdit()
{
return (compoundEdit != null ? compoundEdit.last : getLastEdit());
} //}}}
//{{{ getLastEdit() method
private Edit getLastEdit()
{
if(undosLast instanceof CompoundEdit)
return ((CompoundEdit)undosLast).last;
else
return undosLast;
} //}}}
//{{{ reviseUndoId()
/*
* Revises a unique undoId for a the undo operation that is being
* created as a result of a buffer content change, or that is being
* used for undo/redo. Content changes that belong to the same undo
* operation will have the same undoId.
*
* This method should be called whenever:
* - a buffer content change causes a new undo operation to be created;
* i.e. whenever a content change is not included in the same undo
* operation as the previous.
* - an undo/redo is performed.
*/
private void reviseUndoId()
{
undoId = new Object();
} //}}}
//{{{ getReplaceFromRemoveInsert() method
// a Replace Edit is a Remove Edit and then an Insert Edit
private Replace getReplaceFromRemoveInsert(Edit lastElement, Edit newElement)
{
if(lastElement instanceof Remove && newElement instanceof Insert)
{
// don't fold a undoClearDirty Remove Edit, because
// it's the identity is significant.
if(lastElement == undoClearDirty || newElement == undoClearDirty)
return null;
/* newElement is guaranteed to be an Compound-Insert Edit, redoClearDirty will be an Normal-Insert, Normal-Remove,
* Compound-Remove-Insert-Edit or Compound-Replace-Edit (all possible edit operations)
* redoClearDirty cannot become equal to newElement because:
* - redoClearDirty will be set after the file has been saved and the first new change is made, which
* could be an Normal-Insert, Normal-Remove, Compound-Replace-Edit, Compound-Remove-Insert-Edit,
* or null, if this is the first change in the file at all.
* For Compound-Edit case it will be the last element of the Compound edit.
* - As the first Remove&Insert sequence of a Compound-Edit is never compacted by above if statement,
* redoClearDirty can never be any of the following Remove&Insert elements, as the user as no option to save the
* file after the first Remove&Insert sequence, because the GUI is blocked by the search&replace all operation.
*/
assert newElement != redoClearDirty;
assert lastElement != redoClearDirty;
Remove rem = (Remove) lastElement;
Insert ins = (Insert) newElement;
if(rem.offset == ins.offset)
{
return new Replace(rem.offset, rem.str, ins.str);
}
}
return null;
} //}}}
//{{{ getCompressedReplaceFromReplaceReplace() method
// a CompressedReplace Edit is one to many Replace Edit compressed via offsets
private CompressedReplace getCompressedReplaceFromReplaceReplace(Edit lastElement, Edit newElement)
{
if(newElement instanceof Replace)
{
CompressedReplace rep = null;
// try to pack the next Replace into the CompressedReplace
if(lastElement instanceof CompressedReplace)
{
rep = (CompressedReplace) lastElement;
return rep.add((Replace) newElement);
}
// try to create a compressed Replace
if(lastElement instanceof Replace)
{
rep = new CompressedReplace((Replace)lastElement);
return rep.add((Replace) newElement);
}
}
return null;
} //}}}
//{{{ Inner classes
//{{{ Edit class
private abstract static class Edit
{
Edit prev, next;
//{{{ undo() method
/**
* Returns the selection that should be active after performing
* the operation. If no selection should be active, a 0 length
* selection should be returned, pointing the caret location
* to set after the operation.
* <p>Implementation note: undo manager does not receive the actual
* selection, when it records the operations. That's because
* the operations are recorded by <code>Buffer</code>
* class, and this class has no selections,
* which are kept by <code>TextArea</code> class instances.
* So the <code>Selection[]</code>s returned are simply guessed,
* contain the inserted text.
*/
abstract Selection[] undo(UndoManager mgr);
//}}}
//{{{ redo() method
/**
* @return See {@link #undo}.
* <p>Implementation note: redo always returns caret location only,
* because the actual selection is unknown and we guess it from
* the remove/insert operations. Usually after an action
* the selection becomes empty, so such is the guess.</p>
*/
abstract Selection[] redo(UndoManager mgr);
//}}}
} //}}}
//{{{ Insert class
private static class Insert extends Edit
{
//{{{ Insert constructor
Insert(int offset, String str)
{
this.offset = offset;
this.str = str;
} //}}}
//{{{ undo() method
@Override
Selection[] undo(UndoManager mgr)
{
mgr.buffer.remove(offset,str.length());
if(mgr.undoClearDirty == this)
mgr.buffer.setDirty(false);
return new Selection[] { new Selection.Range(offset, offset) };
} //}}}
//{{{ redo() method
@Override
Selection[] redo(UndoManager mgr)
{
mgr.buffer.insert(offset,str);
if(mgr.redoClearDirty == this)
mgr.buffer.setDirty(false);
int caret = offset + str.length();
return new Selection[] { new Selection.Range(caret, caret) };
} //}}}
int offset;
String str;
} //}}}
//{{{ Remove class
private static class Remove extends Edit
{
//{{{ Remove constructor
Remove(int offset, String str)
{
this.offset = offset;
this.str = str;
} //}}}
//{{{ undo() method
@Override
Selection[] undo(UndoManager mgr)
{
mgr.buffer.insert(offset,str);
if(mgr.undoClearDirty == this)
mgr.buffer.setDirty(false);
return new Selection[] {
new Selection.Range(offset, offset + str.length())
};
} //}}}
//{{{ redo() method
@Override
Selection[] redo(UndoManager mgr)
{
mgr.buffer.remove(offset,str.length());
if(mgr.redoClearDirty == this)
mgr.buffer.setDirty(false);
return new Selection[] { new Selection.Range(offset, offset) };
} //}}}
int offset;
String str;
} //}}}
//{{{ Replace class
private static class Replace extends Edit
{
//{{{ Replace constructor
Replace(int offset, String strRemove, String strInsert)
{
this.offset = offset;
this.strRemove = strRemove;
this.strInsert = strInsert;
} //}}}
//{{{ undo() method
@Override
Selection[] undo(UndoManager mgr)
{
mgr.buffer.remove(offset,strInsert.length());
mgr.buffer.insert(offset,strRemove);
assert mgr.undoClearDirty != this;
return new Selection[] {
new Selection.Range(offset, offset + strRemove.length())
};
} //}}}
//{{{ redo() method
@Override
Selection[] redo(UndoManager mgr)
{
mgr.buffer.remove(offset,strRemove.length());
mgr.buffer.insert(offset,strInsert);
if(mgr.redoClearDirty == this)
mgr.buffer.setDirty(false);
int caret = offset + strInsert.length();
return new Selection[] { new Selection.Range(caret, caret) };
} //}}}
int offset;
String strRemove, strInsert;
} //}}}
//{{{ CompressedReplace class
private static class CompressedReplace extends Replace
{
//{{{ CompressedReplace constructor
CompressedReplace(Replace r1)
{
super(r1.offset, r1.strRemove, r1.strInsert);
offsets = new IntegerArray(4);
offsets.add(r1.offset);
} //}}}
//{{{ add() method
CompressedReplace add(Replace rep)
{
if(this.strInsert.equals(rep.strInsert) && this.strRemove.equals(rep.strRemove))
{
offsets.add(rep.offset);
return this;
}
return null;
} //}}}
//{{{ undo() method
@Override
Selection[] undo(UndoManager mgr)
{
Selection[] s = null;
for(int i = offsets.getSize() - 1; i >= 0; i--)
{
offset = offsets.get(i);
s = super.undo(mgr);
}
return s;
} //}}}
//{{{ redo() method
@Override
Selection[] redo(UndoManager mgr)
{
Selection[] s = null;
for(int i = 0; i < offsets.getSize(); i++)
{
offset = offsets.get(i);
s = super.redo(mgr);
}
return s;
} //}}}
IntegerArray offsets;
} //}}}
//{{{ CompoundEdit class
private static class CompoundEdit extends Edit
{
//{{{ undo() method
@Override
public Selection[] undo(UndoManager mgr)
{
Selection[] retVal = null;
Edit edit = last;
while(edit != null)
{
retVal = edit.undo(mgr);
edit = edit.prev;
}
return retVal;
} //}}}
//{{{ redo() method
@Override
public Selection[] redo(UndoManager mgr)
{
Selection[] retVal = null;
Edit edit = first;
while(edit != null)
{
retVal = edit.redo(mgr);
edit = edit.next;
}
return retVal;
} //}}}
//{{{ _add() method
private void _add(Edit edit)
{
if(first == null)
first = last = edit;
else
{
edit.prev = last;
last.next = edit;
last = edit;
}
} //}}}
//{{{ add() method
public void add(UndoManager mgr, Edit edit)
{
_add(edit);
// try to compact a sequence of Remove and Insert into a Replace
// Edit to save memory for large search&replace operations
if(last.prev != null)
{
Edit rep = mgr.getReplaceFromRemoveInsert(last.prev, last);
if(rep != null)
exchangeLastElement(rep);
}
// try to compress a sequence of Replace and Replace into a "CompressedReplace"
if(last.prev != null)
{
Edit rep = mgr.getCompressedReplaceFromReplaceReplace(last.prev, last);
if(rep != null)
exchangeLastElement(rep);
}
// try to compress a sequence of CompressedReplace and Replace into a "CompressedReplace"
if(last.prev != null)
{
Edit rep = mgr.getCompressedReplaceFromReplaceReplace(last.prev, last);
if(rep != null)
exchangeLastElement(rep);
}
} //}}}
//{{{ exchangeLastElement() method
private void exchangeLastElement(Edit edit)
{
// remove last
if(first == last)
first = last = null;
else
{
last.prev.next = null;
last = last.prev;
}
// exchange current last
if(first == null || first == last)
first = last = edit;
else
{
edit.prev = last.prev;
last.prev.next = edit;
last = edit;
}
} //}}}
Edit first, last;
} //}}}
//}}}
//}}}
}