package org.eclipse.ltk.core.refactoring;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditCopier;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.text.edits.TextEditProcessor;
import org.eclipse.text.edits.UndoEdit;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.ltk.internal.core.refactoring.BufferValidationState;
import org.eclipse.ltk.internal.core.refactoring.Changes;
import org.eclipse.ltk.internal.core.refactoring.ContentStamps;
import org.eclipse.ltk.internal.core.refactoring.Lock;
import org.eclipse.ltk.internal.core.refactoring.MultiStateUndoChange;
import org.eclipse.ltk.internal.core.refactoring.NonDeletingPositionUpdater;
import org.eclipse.ltk.internal.core.refactoring.RefactoringCorePlugin;
public class MultiStateTextFileChange extends TextEditBasedChange {
private static final class ComposableBufferChange {
private TextEdit fEdit;
private List<TextEditBasedChangeGroup> fGroups;
private final TextEdit getEdit() {
return fEdit;
}
private final List<TextEditBasedChangeGroup> getGroups() {
return fGroups;
}
private final void setEdit(final TextEdit edit) {
Assert.isNotNull(edit);
fEdit= edit;
}
private final void setGroups(final List<TextEditBasedChangeGroup> groups) {
Assert.isNotNull(groups);
fGroups= groups;
}
}
private static final class ComposableBufferChangeGroup extends TextEditBasedChangeGroup {
private final Set<TextEdit> fEdits= new HashSet<>();
private ComposableBufferChangeGroup(final MultiStateTextFileChange change, final TextEditGroup group) {
super(change, group);
final TextEdit[] edits= group.getTextEdits();
for (TextEdit edit : edits) {
cacheEdit(edit);
}
}
private final void cacheEdit(final TextEdit edit) {
fEdits.add(edit);
final TextEdit[] edits= edit.getChildren();
for (TextEdit e : edits) {
cacheEdit(e);
}
}
private final boolean containsEdit(final TextEdit edit) {
return fEdits.contains(edit);
}
private final Set<TextEdit> getCachedEdits() {
return fEdits;
}
}
private static final class ComposableEditPosition extends Position {
private String fText;
private final String getText() {
return fText;
}
private final void setText(final String text) {
Assert.isNotNull(text);
fText= text;
}
}
private static final class ComposableUndoEdit {
private ComposableBufferChangeGroup fGroup;
private TextEdit fOriginal;
private ReplaceEdit fUndo;
private final ComposableBufferChangeGroup getGroup() {
return fGroup;
}
private final TextEdit getOriginal() {
return fOriginal;
}
private final String getOriginalText() {
if (fOriginal instanceof ReplaceEdit) {
return ((ReplaceEdit) getOriginal()).getText();
} else if (fOriginal instanceof InsertEdit) {
return ((InsertEdit) getOriginal()).getText();
}
return "";
}
private final ReplaceEdit getUndo() {
return fUndo;
}
private final void setGroup(final ComposableBufferChangeGroup group) {
Assert.isNotNull(group);
fGroup= group;
}
private final void setOriginal(final TextEdit edit) {
fOriginal= edit;
}
private final void setUndo(final ReplaceEdit undo) {
Assert.isNotNull(undo);
fUndo= undo;
}
}
private static final String COMPOSABLE_POSITION_CATEGORY= "ComposableEditPositionCategory_" + System.currentTimeMillis();
private static final String MARKER_POSITION_CATEGORY= "MarkerPositionCategory_" + System.currentTimeMillis();
private ITextFileBuffer fBuffer;
private String fCachedString;
private final ArrayList<ComposableBufferChange> fChanges= new ArrayList<>(4);
private ContentStamp fContentStamp;
private TextEditCopier fCopier;
private int fCount;
private boolean fDirty;
private IFile fFile;
private int fSaveMode= TextFileChange.KEEP_SAVE_STATE;
private BufferValidationState fValidationState;
public MultiStateTextFileChange(final String name, final IFile file) {
super(name);
Assert.isNotNull(file);
fFile= file;
setTextType("txt");
}
private IDocument acquireDocument(final IProgressMonitor monitor) throws CoreException {
if (fCount > 0)
return fBuffer.getDocument();
final ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
final IPath path= fFile.getFullPath();
manager.connect(path, LocationKind.IFILE, monitor);
fCount++;
fBuffer= manager.getTextFileBuffer(path, LocationKind.IFILE);
final IDocument document= fBuffer.getDocument();
fContentStamp= ContentStamps.get(fFile, document);
return document;
}
public final void addChange(final TextChange change) {
Assert.isNotNull(change);
final ComposableBufferChange result= new ComposableBufferChange();
result.setEdit(change.getEdit());
final TextEditBasedChangeGroup[] groups= change.getChangeGroups();
final List<TextEditBasedChangeGroup> list= new ArrayList<>(groups.length);
for (TextEditBasedChangeGroup g : groups) {
final TextEditBasedChangeGroup group= new ComposableBufferChangeGroup(this, g.getTextEditGroup());
list.add(group);
addChangeGroup(group);
}
result.setGroups(list);
fChanges.add(result);
}
private TextEditProcessor createTextEditProcessor(ComposableBufferChange change, IDocument document, int flags, boolean preview) {
List<TextEdit> excludes= new ArrayList<>(0);
for (final Iterator<TextEditBasedChangeGroup> iterator= change.getGroups().iterator(); iterator.hasNext();) {
TextEditBasedChangeGroup group= iterator.next();
if (!group.isEnabled())
excludes.addAll(Arrays.asList(group.getTextEdits()));
}
if (preview) {
fCopier= new TextEditCopier(change.getEdit());
TextEdit copiedEdit= fCopier.perform();
boolean keep= getKeepPreviewEdits();
if (keep)
flags= flags | TextEdit.UPDATE_REGIONS;
LocalTextEditProcessor result= new LocalTextEditProcessor(document, copiedEdit, flags);
result.setExcludes(mapEdits(excludes.toArray(new TextEdit[excludes.size()]), fCopier));
if (!keep)
fCopier= null;
return result;
} else {
LocalTextEditProcessor result= new LocalTextEditProcessor(document, change.getEdit(), flags | TextEdit.UPDATE_REGIONS);
result.setExcludes(excludes.toArray(new TextEdit[excludes.size()]));
return result;
}
}
private ReplaceEdit createUndoEdit(final IDocument document, final int offset, final int length, final String text) {
String currentText= null;
try {
currentText= document.get(offset, length);
} catch (BadLocationException cannotHappen) {
}
if (fCachedString != null && fCachedString.equals(currentText))
currentText= fCachedString;
else
fCachedString= currentText;
return new ReplaceEdit(offset, text != null ? text.length() : 0, currentText);
}
@Override
public final void dispose() {
if (fValidationState != null) {
fValidationState.dispose();
}
}
@Override
public final String getCurrentContent(final IProgressMonitor monitor) throws CoreException {
return getCurrentDocument(monitor).get();
}
@Override
public final String getCurrentContent(final IRegion region, final boolean expand, final int surround, final IProgressMonitor monitor) throws CoreException {
Assert.isNotNull(region);
Assert.isTrue(surround >= 0);
final IDocument document= getCurrentDocument(monitor);
Assert.isTrue(document.getLength() >= region.getOffset() + region.getLength());
return getContent(document, region, expand, surround);
}
public final IDocument getCurrentDocument(IProgressMonitor monitor) throws CoreException {
if (monitor == null)
monitor= new NullProgressMonitor();
IDocument result= null;
monitor.beginTask("", 2);
try {
result= acquireDocument(new SubProgressMonitor(monitor, 1));
} finally {
if (result != null)
releaseDocument(result, new SubProgressMonitor(monitor, 1));
}
monitor.done();
if (result == null)
result= new Document();
return result;
}
@Override
public final Object getModifiedElement() {
return fFile;
}
@Override
public final String getPreviewContent(final TextEditBasedChangeGroup[] groups, final IRegion region, final boolean expand, final int surround, final IProgressMonitor monitor) throws CoreException {
final Set<TextEditBasedChangeGroup> cachedGroups= new HashSet<>(Arrays.asList(groups));
final IDocument document= new Document(getCurrentDocument(monitor).get());
final Position range= new Position(region.getOffset(), region.getLength());
try {
ComposableBufferChange change= null;
final TextEditBasedChangeGroup[] changedGroups= getChangeGroups();
LinkedList<LinkedList<ComposableUndoEdit>> compositeUndo= new LinkedList<>();
for (int index= 0; index < fChanges.size(); index++) {
change= fChanges.get(index);
TextEdit copy= null;
try {
fCopier= new TextEditCopier(change.getEdit());
copy= fCopier.perform();
final Map<TextEdit, TextEdit> originalMap= new HashMap<>();
for (final Iterator<TextEditBasedChangeGroup> outer= change.getGroups().iterator(); outer.hasNext();) {
final ComposableBufferChangeGroup group= (ComposableBufferChangeGroup) outer.next();
for (final Iterator<TextEdit> inner= group.getCachedEdits().iterator(); inner.hasNext();) {
final TextEdit originalEdit= inner.next();
final TextEdit copiedEdit= fCopier.getCopy(originalEdit);
if (copiedEdit != null)
originalMap.put(copiedEdit, originalEdit);
else
RefactoringCorePlugin.logErrorMessage("Could not find a copy for the indexed text edit " + originalEdit.toString());
}
}
final ComposableBufferChangeGroup[] currentGroup= { null};
final TextEdit[] currentEdit= { null};
final TextEditProcessor processor= new TextEditProcessor(document, copy, TextEdit.NONE) {
@Override
protected final boolean considerEdit(final TextEdit edit) {
final TextEdit originalEdit= originalMap.get(edit);
if (originalEdit != null) {
currentEdit[0]= originalEdit;
boolean found= false;
for (int offset= 0; offset < changedGroups.length && !found; offset++) {
final ComposableBufferChangeGroup group= (ComposableBufferChangeGroup) changedGroups[offset];
if (group.containsEdit(originalEdit)) {
currentGroup[0]= group;
found= true;
}
}
if (!found)
currentGroup[0]= null;
} else if (!(edit instanceof MultiTextEdit)) {
RefactoringCorePlugin.logErrorMessage("Could not find the original of the copied text edit " + edit.toString());
}
return true;
}
};
final LinkedList<ComposableUndoEdit> eventUndos= new LinkedList<>();
final IDocumentListener listener= new IDocumentListener() {
@Override
public final void documentAboutToBeChanged(final DocumentEvent event) {
final ComposableUndoEdit edit= new ComposableUndoEdit();
edit.setGroup(currentGroup[0]);
edit.setOriginal(currentEdit[0]);
edit.setUndo(createUndoEdit(document, event.getOffset(), event.getLength(), event.getText()));
eventUndos.addFirst(edit);
}
@Override
public final void documentChanged(final DocumentEvent event) {
}
};
try {
document.addDocumentListener(listener);
processor.performEdits();
} finally {
document.removeDocumentListener(listener);
}
compositeUndo.addFirst(eventUndos);
} finally {
fCopier= null;
}
}
final IPositionUpdater positionUpdater= new IPositionUpdater() {
@Override
public final void update(final DocumentEvent event) {
final int eventOffset= event.getOffset();
final int eventLength= event.getLength();
final int eventOldEndOffset= eventOffset + eventLength;
final String eventText= event.getText();
final int eventNewLength= eventText == null ? 0 : eventText.length();
final int eventNewEndOffset= eventOffset + eventNewLength;
final int deltaLength= eventNewLength - eventLength;
try {
final Position[] positions= event.getDocument().getPositions(COMPOSABLE_POSITION_CATEGORY);
for (Position position : positions) {
if (position.isDeleted())
continue;
final int offset= position.getOffset();
final int length= position.getLength();
final int end= offset + length;
if (offset > eventOldEndOffset) {
position.setOffset(offset + deltaLength);
} else if (end < eventOffset) {
} else if (offset == eventOffset) {
} else if (offset <= eventOffset && end >= eventOldEndOffset) {
position.setLength(length + deltaLength);
} else if (offset < eventOffset) {
position.setLength(eventNewEndOffset - offset);
} else if (end > eventOldEndOffset) {
position.setOffset(eventOffset);
int deleted= eventOldEndOffset - offset;
position.setLength(length - deleted + eventNewLength);
} else {
int newOffset= Math.min(offset, eventNewEndOffset);
int newEndOffset= Math.min(end, eventNewEndOffset);
position.setOffset(newOffset);
position.setLength(newEndOffset - newOffset);
}
}
} catch (BadPositionCategoryException exception) {
}
}
};
try {
document.addPositionCategory(COMPOSABLE_POSITION_CATEGORY);
document.addPositionUpdater(positionUpdater);
final LinkedList<ComposableEditPosition> undoQueue= new LinkedList<>();
for (final Iterator<LinkedList<ComposableUndoEdit>> outer= compositeUndo.iterator(); outer.hasNext();) {
for (final Iterator<ComposableUndoEdit> inner= outer.next().iterator(); inner.hasNext();) {
final ComposableUndoEdit edit= inner.next();
final ReplaceEdit undo= edit.getUndo();
final int offset= undo.getOffset();
final int length= undo.getLength();
final String text= undo.getText();
ComposableEditPosition position= new ComposableEditPosition();
if (cachedGroups.contains(edit.getGroup())) {
if (text == null || text.isEmpty()) {
position.offset= offset;
if (length != 0) {
position.length= 0;
position.setText(edit.getOriginalText());
} else
RefactoringCorePlugin.logErrorMessage("Dubious undo edit found: " + undo.toString());
} else if (length == 0) {
position.offset= offset;
position.setText("");
position.length= text.length();
} else {
position.offset= offset;
position.length= length;
position.setText(edit.getOriginalText());
}
document.addPosition(COMPOSABLE_POSITION_CATEGORY, position);
}
position= new ComposableEditPosition();
position.offset= undo.getOffset();
position.length= undo.getLength();
position.setText(undo.getText());
undoQueue.add(position);
}
for (final Iterator<ComposableEditPosition> iterator= undoQueue.iterator(); iterator.hasNext();) {
final ComposableEditPosition position= iterator.next();
document.replace(position.offset, position.length, position.getText());
iterator.remove();
}
}
final IPositionUpdater markerUpdater= new NonDeletingPositionUpdater(MARKER_POSITION_CATEGORY);
try {
final Position[] positions= document.getPositions(COMPOSABLE_POSITION_CATEGORY);
document.addPositionCategory(MARKER_POSITION_CATEGORY);
document.addPositionUpdater(markerUpdater);
document.addPosition(MARKER_POSITION_CATEGORY, range);
for (Position p : positions) {
final ComposableEditPosition position= (ComposableEditPosition) p;
document.replace(position.offset, position.length, position.getText() != null ? position.getText() : "");
}
} catch (BadPositionCategoryException exception) {
RefactoringCorePlugin.log(exception);
} finally {
document.removePositionUpdater(markerUpdater);
try {
document.removePosition(MARKER_POSITION_CATEGORY, range);
document.removePositionCategory(MARKER_POSITION_CATEGORY);
} catch (BadPositionCategoryException exception) {
}
}
} catch (BadPositionCategoryException exception) {
RefactoringCorePlugin.log(exception);
} finally {
document.removePositionUpdater(positionUpdater);
try {
document.removePositionCategory(COMPOSABLE_POSITION_CATEGORY);
} catch (BadPositionCategoryException exception) {
RefactoringCorePlugin.log(exception);
}
}
return getContent(document, new Region(range.offset, range.length), expand, surround);
} catch (MalformedTreeException | BadLocationException exception) {
RefactoringCorePlugin.log(exception);
}
return getPreviewDocument(monitor).get();
}
@Override
public final String getPreviewContent(final IProgressMonitor monitor) throws CoreException {
return getPreviewDocument(monitor).get();
}
public final IDocument getPreviewDocument(IProgressMonitor monitor) throws CoreException {
if (monitor == null)
monitor= new NullProgressMonitor();
IDocument result= null;
IDocument document= null;
try {
document= acquireDocument(new SubProgressMonitor(monitor, 1));
if (document != null) {
result= new Document(document.get());
performChanges(result, null, true);
}
} catch (BadLocationException exception) {
throw Changes.asCoreException(exception);
} finally {
if (document != null) {
releaseDocument(document, new SubProgressMonitor(monitor, 1));
}
monitor.done();
}
if (result == null)
result= new Document();
return result;
}
public final int getSaveMode() {
return fSaveMode;
}
@Override
public final void initializeValidationData(IProgressMonitor monitor) {
if (monitor == null)
monitor= new NullProgressMonitor();
monitor.beginTask("", 1);
try {
fValidationState= BufferValidationState.create(fFile);
} finally {
monitor.worked(1);
}
}
@Override
public final RefactoringStatus isValid(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
if (monitor == null)
monitor= new NullProgressMonitor();
monitor.beginTask("", 1);
try {
if (fValidationState == null)
throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), "MultiStateTextFileChange has not been initialialized"));
final ITextFileBuffer buffer= FileBuffers.getTextFileBufferManager().getTextFileBuffer(fFile.getFullPath(), LocationKind.IFILE);
fDirty= buffer != null && buffer.isDirty();
final RefactoringStatus status= fValidationState.isValid(needsSaving());
if (needsSaving()) {
status.merge(Changes.validateModifiesFiles(new IFile[] { fFile }));
} else {
status.merge(Changes.checkInSync(new IFile[] { fFile }));
}
return status;
} finally {
monitor.done();
}
}
public final boolean needsSaving() {
return (fSaveMode & TextFileChange.FORCE_SAVE) != 0 || !fDirty && (fSaveMode & TextFileChange.KEEP_SAVE_STATE) != 0;
}
@Override
public final Change perform(final IProgressMonitor monitor) throws CoreException {
monitor.beginTask("", 3);
IDocument document= null;
try {
document= acquireDocument(new SubProgressMonitor(monitor, 1));
final LinkedList<UndoEdit> undoList= new LinkedList<>();
performChanges(document, undoList, false);
if (needsSaving())
fBuffer.commit(new SubProgressMonitor(monitor, 1), false);
return new MultiStateUndoChange(getName(), fFile, undoList.toArray(new UndoEdit[undoList.size()]), fContentStamp, fSaveMode);
} catch (BadLocationException exception) {
throw Changes.asCoreException(exception);
} finally {
if (document != null) {
releaseDocument(document, new SubProgressMonitor(monitor, 1));
}
monitor.done();
}
}
private void performChanges(final IDocument document, final LinkedList<UndoEdit> undoList, final boolean preview) throws BadLocationException {
if (! fBuffer.isSynchronizationContextRequested()) {
performChangesInSynchronizationContext(document, undoList, preview);
return;
}
ITextFileBufferManager fileBufferManager= FileBuffers.getTextFileBufferManager();
final Lock completionLock= new Lock();
final BadLocationException[] exception= new BadLocationException[1];
Runnable runnable= new Runnable() {
@Override
public void run() {
synchronized (completionLock) {
try {
performChangesInSynchronizationContext(document, undoList, preview);
} catch (BadLocationException e) {
exception[0]= e;
} finally {
completionLock.fDone= true;
completionLock.notifyAll();
}
}
}
};
synchronized (completionLock) {
fileBufferManager.execute(runnable);
while (! completionLock.fDone) {
try {
completionLock.wait(500);
} catch (InterruptedException x) {
}
}
}
if (exception[0] != null) {
throw exception[0];
}
}
private void performChangesInSynchronizationContext(final IDocument document, final LinkedList<UndoEdit> undoList, final boolean preview) throws BadLocationException {
DocumentRewriteSession session= null;
try {
if (document instanceof IDocumentExtension4)
session= ((IDocumentExtension4) document).startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED);
for (final Iterator<ComposableBufferChange> iterator= fChanges.iterator(); iterator.hasNext();) {
final ComposableBufferChange change= iterator.next();
final UndoEdit edit= createTextEditProcessor(change, document, undoList != null ? TextEdit.CREATE_UNDO : TextEdit.NONE, preview).performEdits();
if (undoList != null)
undoList.addFirst(edit);
}
} finally {
if (session != null)
((IDocumentExtension4) document).stopRewriteSession(session);
}
}
private void releaseDocument(final IDocument document, final IProgressMonitor monitor) throws CoreException {
Assert.isTrue(fCount > 0);
if (fCount == 1)
FileBuffers.getTextFileBufferManager().disconnect(fFile.getFullPath(), LocationKind.IFILE, monitor);
fCount--;
}
@Override
public final void setKeepPreviewEdits(final boolean keep) {
super.setKeepPreviewEdits(keep);
if (!keep)
fCopier= null;
}
public final void setSaveMode(final int mode) {
fSaveMode= mode;
}
}