blob: e1359d3e799f200aa8953a1bedb0982ddab99f01 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2006 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ltk.core.refactoring;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
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.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
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.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.ltk.internal.core.refactoring.Changes;
/**
* A text change is a special change object that applies a {@link TextEdit
* text edit tree} to a document. The text change manages the text edit tree.
* Access to the document must be provided by concrete subclasses via the method
* {@link #acquireDocument(IProgressMonitor) aquireDocument},
* {@link #commit(IDocument document, IProgressMonitor pm) commitDocument}, and
* {@link #releaseDocument(IDocument, IProgressMonitor) releaseDocument}.
* <p>
* A text change offers the ability to access the original content of
* the document as well as creating a preview of the change. The edit
* tree gets copied when creating any king of preview. Therefore no region
* updating on the original edit tree takes place when requesting a preview
* (for more information on region updating see class {@link TextEdit TextEdit}.
* If region tracking is required for a preview it can be enabled via a call
* to the method {@link #setKeepPreviewEdits(boolean) setKeepPreviewEdits}.
* If enabled the text change keeps the copied edit tree executed for the
* preview allowing clients to map an original edit to an executed edit. The
* executed edit can then be used to determine its position in the preview.
* </p>
* <p>
* Note: this class is not intended to be subclassed outside the refactoring
* framework.
* </p>
*
* @since 3.0
*/
public abstract class TextChange extends TextEditBasedChange {
private TextEdit fEdit;
private TextEditCopier fCopier;
/**
* Creates a new text change with the specified name. The name is a
* human-readable value that is displayed to users. The name does not
* need to be unique, but it must not be <code>null</code>.
* <p>
* The text type of this text change is set to <code>txt</code>.
* </p>
*
* @param name the name of the text change
*
* @see #setTextType(String)
*/
protected TextChange(String name) {
super(name);
}
//---- Edit management -----------------------------------------------
/**
* Sets the root text edit that should be applied to the
* document represented by this text change.
*
* @param edit the root text edit. The root text edit
* can only be set once.
*/
public void setEdit(TextEdit edit) {
Assert.isTrue(fEdit == null, "Root edit can only be set once"); //$NON-NLS-1$
Assert.isTrue(edit != null);
fEdit= edit;
}
/**
* Returns the root text edit.
*
* @return the root text edit or <code>null</code> if no root edit has been
* set
*/
public TextEdit getEdit() {
return fEdit;
}
/**
* Adds a {@link TextEditGroup text edit group}. This method is a convenient
* method for calling <code>change.addTextEditChangeGroup(new
* TextEditChangeGroup(change, group));</code>.
*
* @param group the text edit group to add
*/
public void addTextEditGroup(TextEditGroup group) {
addTextEditChangeGroup(new TextEditChangeGroup(this, group));
}
/**
* Adds a {@link TextEditChangeGroup text edit change group}. Calling the methods
* requires that a root edit has been set via the method {@link #setEdit(TextEdit)
* setEdit}. The edits managed by the given text edit change group must be part of
* the change's root edit.
*
* @param group the text edit change group to add
*/
public void addTextEditChangeGroup(TextEditChangeGroup group) {
Assert.isTrue(fEdit != null, "Can only add a description if a root edit exists"); //$NON-NLS-1$
addChangeGroup(group);
}
/**
* Returns the {@link TextEditChangeGroup text edit change groups} managed by this
* text change.
*
* @return the text edit change groups
*/
public TextEditChangeGroup[] getTextEditChangeGroups() {
final TextEditBasedChangeGroup[] groups= getChangeGroups();
final TextEditChangeGroup[] result= new TextEditChangeGroup[groups.length];
System.arraycopy(groups, 0, result, 0, groups.length);
return result;
}
/**
* Adds the given edit to the edit tree. The edit is added as a top
* level edit.
*
* @param edit the text edit to add
*
* @throws MalformedTreeException if the edit can't be added. Reason
* is that is overlaps with an already existing edit
*
* @since 3.1
*/
public void addEdit(TextEdit edit) throws MalformedTreeException {
Assert.isTrue(fEdit != null, "root must exist to add an edit"); //$NON-NLS-1$
fEdit.addChild(edit);
}
//---- Document management -----------------------------------------------
/**
* Acquires a reference to the document to be changed by this text
* change. A document acquired by this call <em>MUST</em> be released
* via a call to {@link #releaseDocument(IDocument, IProgressMonitor)}.
* <p>
* The method <code>releaseDocument</code> must be call as many times as
* <code>aquireDocument</code> has been called.
* </p>
*
* @param pm a progress monitor
*
* @return a reference to the document to be changed
*
* @throws CoreException if the document can't be acquired
*/
protected abstract IDocument acquireDocument(IProgressMonitor pm) throws CoreException;
/**
* Commits the document acquired via a call to {@link #acquireDocument(IProgressMonitor)
* aquireDocument}. It is up to the implementors of this method to decide what committing
* a document means. Typically, the content of the document is written back to the file
* system.
*
* @param document the document to commit
* @param pm a progress monitor
*
* @throws CoreException if the document can't be committed
*/
protected abstract void commit(IDocument document, IProgressMonitor pm) throws CoreException;
/**
* Releases the document acquired via a call to {@link #acquireDocument(IProgressMonitor)
* aquireDocument}.
*
* @param document the document to release
* @param pm a progress monitor
*
* @throws CoreException if the document can't be released
*/
protected abstract void releaseDocument(IDocument document, IProgressMonitor pm) throws CoreException;
/**
* Hook to create an undo change for the given undo edit. This hook
* gets called while performing the change to construct the corresponding
* undo change object.
*
* @param edit the {@link UndoEdit} to create an undo change for
*
* @return the undo change or <code>null</code> if no undo change can
* be created. Returning <code>null</code> results in the fact that
* the whole change tree can't be undone. So returning <code>null</code>
* is only recommended if an exception occurred during creating the
* undo change.
*/
protected abstract Change createUndoChange(UndoEdit edit);
/**
* {@inheritDoc}
*/
public Change perform(IProgressMonitor pm) throws CoreException {
pm.beginTask("", 3); //$NON-NLS-1$
IDocument document= null;
DocumentRewriteSession session= null;
try {
document= acquireDocument(new SubProgressMonitor(pm, 1));
if (document instanceof IDocumentExtension4) {
session= ((IDocumentExtension4)document).startRewriteSession(
DocumentRewriteSessionType.UNRESTRICTED);
}
TextEditProcessor processor= createTextEditProcessor(document, TextEdit.CREATE_UNDO, false);
UndoEdit undo= processor.performEdits();
commit(document, new SubProgressMonitor(pm, 1));
return createUndoChange(undo);
} catch (BadLocationException e) {
throw Changes.asCoreException(e);
} finally {
if (document != null) {
try {
if (session != null) {
((IDocumentExtension4)document).stopRewriteSession(session);
}
} finally {
releaseDocument(document, new SubProgressMonitor(pm, 1));
}
}
pm.done();
}
}
//---- Method to access the current content of the text change ---------
/**
* Returns the document this text change is associated to. The
* document returned is computed at the point in time when this
* method is called. So calling this method multiple times may
* return different document instances.
* <p>
* The returned document must not be modified by the client. Doing
* so will result in an unexpected behavior when the change is
* performed.
* </p>
*
* @param pm a progress monitor to report progress or <code>null</code>
* if no progress reporting is desired
* @return the document this change is working on
*
* @throws CoreException if the document can't be acquired
*/
public IDocument getCurrentDocument(IProgressMonitor pm) throws CoreException {
if (pm == null)
pm= new NullProgressMonitor();
IDocument result= null;
pm.beginTask("", 2); //$NON-NLS-1$
try{
result= acquireDocument(new SubProgressMonitor(pm, 1));
} finally {
if (result != null)
releaseDocument(result, new SubProgressMonitor(pm, 1));
}
pm.done();
return result;
}
/**
* {@inheritDoc}
*/
public String getCurrentContent(IProgressMonitor pm) throws CoreException {
return getCurrentDocument(pm).get();
}
/**
* {@inheritDoc}
*/
public String getCurrentContent(IRegion region, boolean expandRegionToFullLine, int surroundingLines, IProgressMonitor pm) throws CoreException {
Assert.isNotNull(region);
Assert.isTrue(surroundingLines >= 0);
IDocument document= getCurrentDocument(pm);
Assert.isTrue(document.getLength() >= region.getOffset() + region.getLength());
return getContent(document, region, expandRegionToFullLine, surroundingLines);
}
//---- Method to access the preview content of the text change ---------
/**
* Returns the edit that got executed during preview generation
* instead of the given original. The method requires that <code>
* setKeepPreviewEdits</code> is set to <code>true</code> and that
* a preview has been requested via one of the <code>getPreview*
* </code> methods.
* <p>
* The method returns <code>null</code> if the original isn't managed
* by this text change.
* </p>
*
* @param original the original edit managed by this text change
*
* @return the edit executed during preview generation
*/
public TextEdit getPreviewEdit(TextEdit original) {
Assert.isTrue(getKeepPreviewEdits() && fCopier != null && original != null);
return fCopier.getCopy(original);
}
/**
* Returns the edits that were executed during preview generation
* instead of the given array of original edits. The method requires
* that <code>setKeepPreviewEdits</code> is set to <code>true</code>
* and that a preview has been requested via one of the <code>
* getPreview*</code> methods.
* <p>
* The method returns an empty array if none of the original edits
* is managed by this text change.
* </p>
*
* @param originals an array of original edits managed by this text
* change
*
* @return an array of edits containing the corresponding edits
* executed during preview generation
*/
public TextEdit[] getPreviewEdits(TextEdit[] originals) {
Assert.isTrue(getKeepPreviewEdits() && fCopier != null && originals != null);
if (originals.length == 0)
return new TextEdit[0];
List result= new ArrayList(originals.length);
for (int i= 0; i < originals.length; i++) {
TextEdit copy= fCopier.getCopy(originals[i]);
if (copy != null)
result.add(copy);
}
return (TextEdit[]) result.toArray(new TextEdit[result.size()]);
}
/**
* Returns a document containing a preview of the text change. The
* preview is computed by executing the all managed text edits. The
* method considers the active state of the added {@link TextEditChangeGroup
* text edit change groups}.
*
* @param pm a progress monitor to report progress or <code>null</code>
* if no progress reporting is desired
* @return a document containing the preview of the text change
*
* @throws CoreException if the preview can't be created
*/
public IDocument getPreviewDocument(IProgressMonitor pm) throws CoreException {
PreviewAndRegion result= getPreviewDocument(ALL_EDITS, pm);
return result.document;
}
/**
* {@inheritDoc}
*/
public String getPreviewContent(IProgressMonitor pm) throws CoreException {
return getPreviewDocument(pm).get();
}
/**
* Returns a preview of the text change clipped to a specific region.
* The preview is created by applying the text edits managed by the
* given array of {@link TextEditChangeGroup text edit change groups}.
* The region is determined as follows:
* <ul>
* <li>if <code>expandRegionToFullLine</code> is <code>false</code>
* then the parameter <code>region</code> determines the clipping.
* </li>
* <li>if <code>expandRegionToFullLine</code> is <code>true</code>
* then the region determined by the parameter <code>region</code>
* is extended to cover full lines.
* </li>
* <li>if <code>surroundingLines</code> &gt; 0 then the given number
* of surrounding lines is added. The value of <code>surroundingLines
* </code> is only considered if <code>expandRegionToFullLine</code>
* is <code>true</code>
* </li>
* </ul>
*
* @param changeGroups a set of change groups for which a preview is to be
* generated
* @param region the starting region for the clipping
* @param expandRegionToFullLine if <code>true</code> is passed the region
* is extended to cover full lines
* @param surroundingLines the number of surrounding lines to be added to
* the clipping region. Is only considered if <code>expandRegionToFullLine
* </code> is <code>true</code>
* @param pm a progress monitor to report progress or <code>null</code>
* if no progress reporting is desired
*
* @return the current content of the text change clipped to a region
* determined by the given parameters.
*
* @throws CoreException if an exception occurs while generating the preview
*
* @see #getCurrentContent(IRegion, boolean, int, IProgressMonitor)
*/
public String getPreviewContent(TextEditChangeGroup[] changeGroups, IRegion region, boolean expandRegionToFullLine, int surroundingLines, IProgressMonitor pm) throws CoreException {
return getPreviewContent((TextEditBasedChangeGroup[])changeGroups, region, expandRegionToFullLine, surroundingLines, pm);
}
/**
* Returns a preview of the text change clipped to a specific region.
* The preview is created by applying the text edits managed by the
* given array of {@link TextEditChangeGroup text edit change groups}.
* The region is determined as follows:
* <ul>
* <li>if <code>expandRegionToFullLine</code> is <code>false</code>
* then the parameter <code>region</code> determines the clipping.
* </li>
* <li>if <code>expandRegionToFullLine</code> is <code>true</code>
* then the region determined by the parameter <code>region</code>
* is extended to cover full lines.
* </li>
* <li>if <code>surroundingLines</code> &gt; 0 then the given number
* of surrounding lines is added. The value of <code>surroundingLines
* </code> is only considered if <code>expandRegionToFullLine</code>
* is <code>true</code>
* </li>
* </ul>
*
* @param changeGroups a set of change groups for which a preview is to be
* generated
* @param region the starting region for the clipping
* @param expandRegionToFullLine if <code>true</code> is passed the region
* is extended to cover full lines
* @param surroundingLines the number of surrounding lines to be added to
* the clipping region. Is only considered if <code>expandRegionToFullLine
* </code> is <code>true</code>
* @param pm a progress monitor to report progress or <code>null</code>
* if no progress reporting is desired
*
* @return the current content of the text change clipped to a region
* determined by the given parameters.
*
* @throws CoreException if an exception occurs while generating the preview
*
* @see #getCurrentContent(IRegion, boolean, int, IProgressMonitor)
*
* @since 3.2
*/
public String getPreviewContent(TextEditBasedChangeGroup[] changeGroups, IRegion region, boolean expandRegionToFullLine, int surroundingLines, IProgressMonitor pm) throws CoreException {
IRegion currentRegion= getRegion(changeGroups);
Assert.isTrue(region.getOffset() <= currentRegion.getOffset() &&
currentRegion.getOffset() + currentRegion.getLength() <= region.getOffset() + region.getLength());
// Make sure that all edits in the change groups are rooted under the edit the text change stand for.
TextEdit root= getEdit();
Assert.isNotNull(root, "No root edit"); //$NON-NLS-1$
for (int c= 0; c < changeGroups.length; c++) {
TextEditBasedChangeGroup group= changeGroups[c];
TextEdit[] edits= group.getTextEdits();
for (int e= 0; e < edits.length; e++) {
// TODO: enable once following bug is fixed
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=130909
// Assert.isTrue(root == edits[e].getRoot(), "Wrong root edit"); //$NON-NLS-1$
}
}
PreviewAndRegion result= getPreviewDocument(changeGroups, pm);
int delta;
if (result.region == null) { // all edits were delete edits so no new region
delta= -currentRegion.getLength();
} else {
delta= result.region.getLength() - currentRegion.getLength();
}
return getContent(result.document, new Region(region.getOffset(), region.getLength() + delta), expandRegionToFullLine, surroundingLines);
}
//---- private helper methods --------------------------------------------------
private PreviewAndRegion getPreviewDocument(TextEditBasedChangeGroup[] changes, IProgressMonitor pm) throws CoreException {
IDocument document= new Document(getCurrentDocument(pm).get());
boolean trackChanges= getKeepPreviewEdits();
setKeepPreviewEdits(true);
TextEditProcessor processor= changes == ALL_EDITS
? createTextEditProcessor(document, TextEdit.NONE, true)
: createTextEditProcessor(document, TextEdit.NONE, changes);
try {
processor.performEdits();
return new PreviewAndRegion(document, getNewRegion(changes));
} catch (BadLocationException e) {
throw Changes.asCoreException(e);
} finally {
setKeepPreviewEdits(trackChanges);
}
}
private TextEditProcessor createTextEditProcessor(IDocument document, int flags, boolean preview) {
if (fEdit == null)
return new TextEditProcessor(document, new MultiTextEdit(0,0), flags);
List excludes= new ArrayList(0);
TextEditBasedChangeGroup[] groups= getChangeGroups();
for (int index= 0; index < groups.length; index++) {
TextEditBasedChangeGroup edit= groups[index];
if (!edit.isEnabled()) {
excludes.addAll(Arrays.asList(edit.getTextEditGroup().getTextEdits()));
}
}
if (preview) {
fCopier= new TextEditCopier(fEdit);
TextEdit copiedEdit= fCopier.perform();
boolean keep= getKeepPreviewEdits();
if (keep)
flags= flags | TextEdit.UPDATE_REGIONS;
LocalTextEditProcessor result= new LocalTextEditProcessor(document, copiedEdit, flags);
result.setExcludes(mapEdits(
(TextEdit[])excludes.toArray(new TextEdit[excludes.size()]),
fCopier));
if (!keep)
fCopier= null;
return result;
} else {
LocalTextEditProcessor result= new LocalTextEditProcessor(document, fEdit, flags | TextEdit.UPDATE_REGIONS);
result.setExcludes((TextEdit[])excludes.toArray(new TextEdit[excludes.size()]));
return result;
}
}
private TextEditProcessor createTextEditProcessor(IDocument document, int flags, TextEditBasedChangeGroup[] changes) {
if (fEdit == null)
return new TextEditProcessor(document, new MultiTextEdit(0,0), flags);
List includes= new ArrayList(0);
for (int c= 0; c < changes.length; c++) {
TextEditBasedChangeGroup change= changes[c];
Assert.isTrue(change.getTextEditChange() == this);
if (change.isEnabled()) {
includes.addAll(Arrays.asList(change.getTextEditGroup().getTextEdits()));
}
}
fCopier= new TextEditCopier(fEdit);
TextEdit copiedEdit= fCopier.perform();
boolean keep= getKeepPreviewEdits();
if (keep)
flags= flags | TextEdit.UPDATE_REGIONS;
LocalTextEditProcessor result= new LocalTextEditProcessor(document, copiedEdit, flags);
result.setIncludes(mapEdits(
(TextEdit[])includes.toArray(new TextEdit[includes.size()]),
fCopier));
if (!keep)
fCopier= null;
return result;
}
private IRegion getRegion(TextEditBasedChangeGroup[] changes) {
if (changes == ALL_EDITS) {
if (fEdit == null)
return null;
return fEdit.getRegion();
} else {
List edits= new ArrayList();
for (int i= 0; i < changes.length; i++) {
edits.addAll(Arrays.asList(changes[i].getTextEditGroup().getTextEdits()));
}
if (edits.size() == 0)
return null;
return TextEdit.getCoverage((TextEdit[]) edits.toArray(new TextEdit[edits.size()]));
}
}
private IRegion getNewRegion(TextEditBasedChangeGroup[] changes) {
if (changes == ALL_EDITS) {
if (fEdit == null)
return null;
return fCopier.getCopy(fEdit).getRegion();
} else {
List result= new ArrayList();
for (int c= 0; c < changes.length; c++) {
TextEdit[] edits= changes[c].getTextEditGroup().getTextEdits();
for (int e= 0; e < edits.length; e++) {
TextEdit copy= fCopier.getCopy(edits[e]);
if (copy != null)
result.add(copy);
}
}
if (result.size() == 0)
return null;
return TextEdit.getCoverage((TextEdit[]) result.toArray(new TextEdit[result.size()]));
}
}
/**
* {@inheritDoc}
*/
public void setKeepPreviewEdits(boolean keep) {
super.setKeepPreviewEdits(keep);
if (!keep)
fCopier= null;
}
}