/******************************************************************************* * Copyright (c) 2005, 2008 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 * Ed Swartz <ed.swartz@nokia.com> - * (bug 157203: [ltk] [patch] TextEditBasedChange/TextChange provides incorrect diff when one side is empty) *******************************************************************************/ package org.eclipse.che.ide.ext.java.jdt.refactoring; import org.eclipse.che.ide.ext.java.jdt.text.Document; import org.eclipse.che.ide.ext.java.jdt.text.edits.TextEdit; import org.eclipse.che.ide.ext.java.jdt.text.edits.TextEditCopier; import org.eclipse.che.ide.ext.java.jdt.text.edits.TextEditGroup; import org.eclipse.che.ide.ext.java.jdt.text.edits.TextEditProcessor; import org.eclipse.che.ide.runtime.Assert; import org.eclipse.che.ide.runtime.CoreException; import org.eclipse.che.ide.runtime.IStatus; import org.eclipse.che.ide.runtime.Status; import org.eclipse.che.ide.api.text.BadLocationException; import org.eclipse.che.ide.api.text.Region; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * An abstract base implementation of a change which is based on text edits. * * @since 3.2 */ public abstract class TextEditBasedChange extends Change { /** Text edit processor which has the ability to selectively include or exclude single text edits. */ static final class LocalTextEditProcessor extends TextEditProcessor { public static final int EXCLUDE = 1; public static final int INCLUDE = 2; private TextEdit[] fExcludes; private TextEdit[] fIncludes; protected LocalTextEditProcessor(Document document, TextEdit root, int flags) { super(document, root, flags); } public void setIncludes(TextEdit[] includes) { Assert.isNotNull(includes); Assert.isTrue(fExcludes == null); fIncludes = flatten(includes); } public void setExcludes(TextEdit[] excludes) { Assert.isNotNull(excludes); Assert.isTrue(fIncludes == null); fExcludes = flatten(excludes); } protected boolean considerEdit(TextEdit edit) { if (fExcludes != null) { for (int i = 0; i < fExcludes.length; i++) { if (edit.equals(fExcludes[i])) return false; } return true; } if (fIncludes != null) { for (int i = 0; i < fIncludes.length; i++) { if (edit.equals(fIncludes[i])) return true; } return false; } return true; } private TextEdit[] flatten(TextEdit[] edits) { List result = new ArrayList(5); for (int i = 0; i < edits.length; i++) { flatten(result, edits[i]); } return (TextEdit[])result.toArray(new TextEdit[result.size()]); } private void flatten(List result, TextEdit edit) { result.add(edit); TextEdit[] children = edit.getChildren(); for (int i = 0; i < children.length; i++) { flatten(result, children[i]); } } } /** Value objects encapsulating a document with an associated region. */ static final class PreviewAndRegion { public Document document; public Region region; public PreviewAndRegion(Document d, Region r) { document = d; region = r; } } /** * A special object denoting all edits managed by the change. This even * includes those edits not managed by a {@link TextEditBasedChangeGroup}. */ static final TextEditBasedChangeGroup[] ALL_EDITS = new TextEditBasedChangeGroup[0]; /** The list of change groups */ private List fChangeGroups; private GroupCategorySet fCombiedGroupCategories; /** The name of the change */ private String fName; /** The text type */ private String fTextType; /** Should the positions of edits be tracked during change generation? */ private boolean fTrackEdits; /** * Creates a new abstract text edit 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 edit change is set to <code>txt</code>. * </p> * * @param name * the name of the text edit change * @see #setTextType(String) */ protected TextEditBasedChange(String name) { Assert.isNotNull(name, "Name must not be null"); //$NON-NLS-1$ fChangeGroups = new ArrayList(5); fName = name; fTextType = "txt"; //$NON-NLS-1$ } /** * Adds a {@link TextEditBasedChangeGroup text edit change group}. * 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 addChangeGroup(TextEditBasedChangeGroup group) { Assert.isTrue(group != null); fChangeGroups.add(group); if (fCombiedGroupCategories != null) { fCombiedGroupCategories = GroupCategorySet.union(fCombiedGroupCategories, group.getGroupCategorySet()); } } /** * Adds a {@link TextEditGroup text edit group}. This method is a convenience * method for calling <code>change.addChangeGroup(new * TextEditBasedChangeGroup(change, group));</code>. * * @param group * the text edit group to add */ public void addTextEditGroup(TextEditGroup group) { addChangeGroup(new TextEditBasedChangeGroup(this, group)); } /** * Returns <code>true</code> if the change has one of the given group * categories. Otherwise <code>false</code> is returned. * * @param groupCategories * the group categories to check * @return whether the change has one of the given group * categories * @since 3.2 */ public boolean hasOneGroupCategory(List groupCategories) { if (fCombiedGroupCategories == null) { fCombiedGroupCategories = GroupCategorySet.NONE; for (Iterator iter = fChangeGroups.iterator(); iter.hasNext(); ) { TextEditBasedChangeGroup group = (TextEditBasedChangeGroup)iter.next(); fCombiedGroupCategories = GroupCategorySet.union(fCombiedGroupCategories, group.getGroupCategorySet()); } } return fCombiedGroupCategories.containsOneCategory(groupCategories); } /** * Returns the {@link TextEditBasedChangeGroup text edit change groups} managed by this * buffer change. * * @return the text edit change groups */ public final TextEditBasedChangeGroup[] getChangeGroups() { return (TextEditBasedChangeGroup[])fChangeGroups.toArray(new TextEditBasedChangeGroup[fChangeGroups.size()]); } String getContent(Document document, Region region, boolean expandRegionToFullLine, int surroundingLines) throws CoreException { try { if (expandRegionToFullLine) { int startLine = Math.max(document.getLineOfOffset(region.getOffset()) - surroundingLines, 0); int endLine; if (region.getLength() == 0) { // no lines are in the region, so remove one from the context, // or else spurious changes show up that look like deletes from the source if (surroundingLines == 0) { // empty: show nothing return ""; //$NON-NLS-1$ } endLine = Math.min(document.getLineOfOffset(region.getOffset()) + surroundingLines - 1, document.getNumberOfLines() - 1); } else { endLine = Math.min(document.getLineOfOffset(region.getOffset() + region.getLength() - 1) + surroundingLines, document.getNumberOfLines() - 1); } int offset = document.getLineInformation(startLine).getOffset(); Region endLineRegion = document.getLineInformation(endLine); int length = endLineRegion.getOffset() + endLineRegion.getLength() - offset; return document.get(offset, length); } else { return document.get(region.getOffset(), region.getLength()); } } catch (BadLocationException e) { // Log.error(getClass(), e); throw new CoreException(new Status(IStatus.ERROR, "", e.getMessage())); //throw Changes.asCoreException(e); } } /** * Returns the current content of the document this text * change is associated with. * * @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 edit change * @throws CoreException * if the content can't be accessed */ public abstract String getCurrentContent() throws CoreException; /** * Returns the current content of the text edit change clipped to a specific * region. 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> > 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 region * the starting region for the text to be returned * @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 edit change clipped to a region * determined by the given parameters. * @throws CoreException * if an exception occurs while accessing the current content */ public abstract String getCurrentContent(Region region, boolean expandRegionToFullLine, int surroundingLines) throws CoreException; /** * Returns whether preview edits are remembered for further region * tracking or not. * * @return <code>true</code> if executed text edits are remembered * during preview generation; otherwise <code>false</code> */ public boolean getKeepPreviewEdits() { return fTrackEdits; } /** {@inheritDoc} */ public String getName() { return fName; } /** * Returns a preview of the text edit change clipped to a specific region. * The preview is created by applying the text edits managed by the * given array of {@link TextEditBasedChangeGroup 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> > 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 abstract String getPreviewContent(TextEditBasedChangeGroup[] changeGroups, Region region, boolean expandRegionToFullLine, int surroundingLines) throws CoreException; /** * Returns the preview content as a string. * * @param pm * a progress monitor to report progress or <code>null</code> * if no progress reporting is desired * @return the preview * @throws CoreException * if the preview can't be created */ public abstract String getPreviewContent() throws CoreException; /** * Returns the text edit change's text type. * * @return the text edit change's text type */ public String getTextType() { return fTextType; } TextEdit[] mapEdits(TextEdit[] edits, TextEditCopier copier) { if (edits == null) return null; final List result = new ArrayList(edits.length); for (int i = 0; i < edits.length; i++) { TextEdit edit = copier.getCopy(edits[i]); if (edit != null) result.add(edit); } return (TextEdit[])result.toArray(new TextEdit[result.size()]); } /** {@inheritDoc} */ public void setEnabled(boolean enabled) { super.setEnabled(enabled); for (Iterator iter = fChangeGroups.iterator(); iter.hasNext(); ) { TextEditBasedChangeGroup element = (TextEditBasedChangeGroup)iter.next(); element.setEnabled(enabled); } } /** * Controls whether the text edit change should keep executed edits during * preview generation. * * @param keep * if <code>true</code> executed preview edits are kept */ public void setKeepPreviewEdits(boolean keep) { fTrackEdits = keep; } /** * Sets the text type. The text type is used to determine the content * merge viewer used to present the difference between the original * and the preview content in the user interface. Content merge viewers * are defined via the extension point <code>org.eclipse.compare.contentMergeViewers</code>. * <p> * The default text type is <code>txt</code>. * </p> * * @param type * the text type. If <code>null</code> is passed the text type is * reseted to the default text type <code>txt</code>. */ public void setTextType(String type) { if (type == null) type = "txt"; //$NON-NLS-1$ fTextType = type; } }