org.eclipse.oomph.internal.ui.FindAndReplaceTarget.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.oomph.internal.ui.FindAndReplaceTarget.java

Source

/*
 * Copyright (c) 2015 Ed Merks(Berlin, Germany) 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:
 *    Ed Merks - initial API and implementation
 */
package org.eclipse.oomph.internal.ui;

import org.eclipse.oomph.ui.UIUtil;
import org.eclipse.oomph.util.ReflectUtil;

import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.ui.viewer.IStyledLabelDecorator;
import org.eclipse.emf.common.ui.viewer.IViewerProvider;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.ui.provider.DecoratingColumLabelProvider;
import org.eclipse.emf.edit.ui.provider.DelegatingStyledCellLabelProvider;
import org.eclipse.emf.edit.ui.provider.PropertyDescriptor;
import org.eclipse.emf.edit.ui.provider.PropertySource;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;
import org.eclipse.jface.text.IFindReplaceTargetExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.IPage;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.properties.IPropertySourceProvider;
import org.eclipse.ui.views.properties.PropertySheet;
import org.eclipse.ui.views.properties.PropertySheetEntry;
import org.eclipse.ui.views.properties.PropertySheetPage;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Ed Merks
 */
public class FindAndReplaceTarget
        implements IFindReplaceTarget, IFindReplaceTargetExtension, IFindReplaceTargetExtension3 {
    private static final Map<IWorkbenchPart, FindAndReplaceTarget> FIND_AND_REPLACE_TARGETS = new WeakHashMap<IWorkbenchPart, FindAndReplaceTarget>();

    private static final Field FILTER_ACTION_FIELD = ReflectUtil.getField(PropertySheetPage.class, "filterAction");

    private static final Method GET_DESCRIPTOR_METHOD = ReflectUtil.getMethod(PropertySheetEntry.class,
            "getDescriptor");

    private static final Field OBJECT_FIELD = ReflectUtil.getField(PropertyDescriptor.class, "object");

    private static final Field ITEM_PROPERTY_DESCRIPTOR_FIELD = ReflectUtil.getField(PropertyDescriptor.class,
            "itemPropertyDescriptor");

    private static final Field ITEM_PROPERTY_SOURCE_FIELD = ReflectUtil.getField(PropertySource.class,
            "itemPropertySource");

    private static final Styler MATCH_STYLER = new Styler() {
        @Override
        public void applyStyles(TextStyle textStyle) {
            textStyle.borderStyle = SWT.BORDER_SOLID;
        }
    };

    private IWorkbenchPart workbenchPart;

    private Runnable propertiesCleanup;

    private Runnable sessionCleanup;

    private List<?> selectionScope;

    private Set<Object> selectionScopeObjects;

    private String selectionText;

    private Data.Item selectedItem;

    private int selectedItemStart;

    private Pattern selectedItemPattern;

    private CompoundCommand replaceAllCommand;

    private int pendingReplacements = -1;

    private boolean findReplaceable;

    private FindAndReplaceTarget.SearchType searchType;

    private TreeItem specialTreeItem;

    private int specialStart;

    private boolean suspendScopeChanges;

    public FindAndReplaceTarget(IWorkbenchPart workbenchPart) {
        this.workbenchPart = workbenchPart;
    }

    /**
     * Extracts the viewer from the workbench part.
     */
    protected StructuredViewer getViewer() {
        if (workbenchPart instanceof IViewerProvider) {
            IViewerProvider viewerProvider = (IViewerProvider) workbenchPart;
            Viewer viewer = viewerProvider.getViewer();
            if (viewer instanceof StructuredViewer) {
                return (StructuredViewer) viewer;
            }
        }

        return null;
    }

    /**
     * Returns the property sheet page of the workbench page's active property sheet.
     */
    protected PropertySheetPage getActivePropertySheetPage() {
        IWorkbenchPart activePart = workbenchPart.getSite().getPage().getActivePart();
        if (activePart instanceof PropertySheet) {
            PropertySheet propertySheet = (PropertySheet) activePart;
            IPage currentPage = propertySheet.getCurrentPage();
            if (currentPage != null) {
                if (currentPage instanceof PropertySheetPage) {
                    PropertySheetPage propertySheetPage = (PropertySheetPage) currentPage;
                    return propertySheetPage;
                }
            }
        }

        return null;
    }

    /**
     * Returns the tree of the active property sheet page.
     */
    protected Tree getActivePropertySheetTree() {
        PropertySheetPage propertySheetPage = getActivePropertySheetPage();
        if (propertySheetPage != null) {
            Control control = propertySheetPage.getControl();
            if (control instanceof Tree) {
                Tree tree = (Tree) control;
                return tree;
            }
        }

        return null;
    }

    public boolean isEditable() {
        // Editing is always supported.
        // Replace is selectively disabled when a particular selection is not editable.
        return true;
    }

    public boolean canPerformFind() {
        // As long as there is a viewer is appropriate label and content providers, we can support find.
        StructuredViewer viewer = getViewer();
        return viewer != null && viewer.getLabelProvider() instanceof ILabelProvider
                && viewer.getContentProvider() instanceof IStructuredContentProvider;
    }

    public void initialize(IWorkbenchPart workbenchPart) {
        // This method is called by the find and replace action before it opens the find and replace dialog.
        // It has the opportunity to see the state before the find and replace dialog takes focus away.
        // In particular, it can see the selected text in an active cell editor in the properties view.
        StructuredViewer viewer = getViewer();
        Tree propertySheetTree = getActivePropertySheetTree();
        if (propertySheetTree != null) {
            // If there is an active property sheet with a tree, iterate over the selected tree items.
            for (TreeItem treeItem : propertySheetTree.getSelection()) {
                // Determine if there is an EMF property descriptor associated with it.
                PropertyDescriptor propertyDescriptor = getPropertyDescriptor(treeItem);
                if (propertyDescriptor != null) {
                    // If so, extract the object and look for it in the data.
                    Object object = getObject(propertyDescriptor);
                    for (FindAndReplaceTarget.Data data : new TextData(viewer)) {
                        if (data.object == object) {
                            // Look for the feature in the data items.
                            Object feature = getFeature(propertyDescriptor);
                            for (Data.Item item : data.items) {
                                Object itemFeature = item.getFeature();
                                if (itemFeature == feature) {
                                    // If there is a focus text control, it must be the cell editor of this property.
                                    Control control = workbenchPart.getSite().getShell().getDisplay()
                                            .getFocusControl();
                                    if (control instanceof Text) {
                                        // Extract the selected text, if any...
                                        Text text = (Text) control;
                                        selectionText = text.getSelectionText();
                                        if (selectionText.length() > 0) {
                                            // Use this item's selected text as our initial selection.
                                            Point selection = text.getSelection();
                                            setSelection(true, viewer, item, selection.x,
                                                    Pattern.compile(Pattern.quote(selectionText)));

                                            // Then we're done.
                                            return;
                                        }
                                    }

                                    // Use this overall item as our initial selection.
                                    setSelection(true, viewer, item, 0, Pattern.compile(Pattern.quote(item.value)));
                                    return;
                                }
                            }

                            // Once we've passed the object of interest, there is nothing left to do.
                            break;
                        }
                    }
                }
            }
        } else {
            // Otherwise, use the first item selected in the viewer as our initial selection.
            List<?> list = viewer.getStructuredSelection().toList();
            for (FindAndReplaceTarget.Data data : new TextData(viewer)) {
                if (list.contains(data.object)) {
                    Data.Item item = data.items.get(0);
                    setSelection(true, viewer, item, 0, Pattern.compile(Pattern.quote(item.value)));
                    return;
                }
            }
        }
    }

    public void beginSession() {
        // When the session start, we add our search-type control.
        addSearchTypeControl();
    }

    protected void addSearchTypeControl() {
        // Look through all child shells...
        Shell workbenchShell = workbenchPart.getSite().getShell();
        Shell[] shells = workbenchShell.getShells();
        for (final Shell shell : shells) {
            // If this is a shell for the find and replace dialog...
            Object data = shell.getData();
            if (data instanceof Dialog
                    && "org.eclipse.ui.texteditor.FindReplaceDialog".equals(data.getClass().getName())) {
                // Find the last checkbox in the dialog.
                Dialog dialog = (Dialog) data;
                Object checkBox = ReflectUtil.getValue("fIsRegExCheckBox", dialog);
                if (checkBox instanceof Button) {
                    // It should have grid data.
                    Button checkBoxButton = (Button) checkBox;
                    Object layoutData = checkBoxButton.getLayoutData();
                    if (layoutData instanceof GridData) {
                        // Change it's span and alignment to make room for our additional control.
                        final GridData checkBoxGridData = (GridData) layoutData;
                        if (checkBoxGridData.horizontalSpan == 2) {
                            checkBoxGridData.verticalAlignment = SWT.TOP;
                            checkBoxGridData.horizontalSpan = 1;
                        }

                        // Keep state in a section of our dialog settings.
                        final IDialogSettings dialogSettings = UIPlugin.INSTANCE
                                .getDialogSettings("org.eclipse.ui.texteditor.FindReplaceDialog");

                        // Create a search-type combo in the same group as the checkbox.
                        final Composite group = checkBoxButton.getParent();
                        final Combo combo = new Combo(group, SWT.READ_ONLY | SWT.DROP_DOWN);
                        combo.setItems(SearchType.getLabels());
                        GridData gridData = new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1);
                        combo.setLayoutData(gridData);

                        // The initial choice is remembered from the dialog settings.
                        searchType = SearchType.getSearchType(dialogSettings.get("search-type"));
                        combo.select(searchType.ordinal());

                        // Listen for changes in the choice.
                        combo.addSelectionListener(new SelectionAdapter() {
                            @Override
                            public void widgetSelected(SelectionEvent e) {
                                // Not only remember the choice, but record it in the dialog settings.
                                searchType = SearchType.values()[combo.getSelectionIndex()];
                                dialogSettings.put("search-type", searchType.key());
                            }
                        });

                        String oldLabel = null;
                        Button selectedRangeButton = null;
                        Object selectedRange = ReflectUtil.getValue("fSelectedRangeRadioButton", dialog);
                        if (selectedRange instanceof Button) {
                            selectedRangeButton = (Button) selectedRange;
                            String label = selectedRangeButton.getText();
                            if (label.endsWith("lines")) {
                                oldLabel = label;
                                selectedRangeButton.setText(label.substring(0, label.length() - 5) + " elements");
                            }
                        }

                        // Relayout the shell.
                        shell.layout(true, true);

                        // If we've never checked that the default size of the dialog is big enough to fit our controls,
                        // do it this time.
                        if (!dialogSettings.getBoolean("resized")) {
                            // Determine the bottom right corner location of the combo in absolute coordinates.
                            Rectangle comboBounds = combo.getBounds();
                            Point comboBottomRightLocation = group.toDisplay(comboBounds.x + comboBounds.width,
                                    comboBounds.y + comboBounds.height);

                            // Determine the bottom right corner location of the containing group in absolute coordinates.
                            Rectangle groupBounds = group.getBounds();
                            Point groupBottomRightLocation = group.getParent().toDisplay(
                                    groupBounds.x + groupBounds.width, groupBounds.y + groupBounds.height);

                            // Determine how much too small each dimension might be.
                            // If there is less that 8 pixes of padding in either direction...
                            int widthDelta = groupBottomRightLocation.x - comboBottomRightLocation.x;
                            int heightDelta = groupBottomRightLocation.y - comboBottomRightLocation.y;
                            if (widthDelta < 8 || heightDelta < 8) {
                                // Increase the shell size so that the search-type combo fits nicely.
                                Point shellSize = shell.getSize();

                                if (widthDelta < 8) {
                                    shellSize.x -= widthDelta - 8;
                                }

                                if (heightDelta < 8) {
                                    shellSize.y -= heightDelta - 8;
                                }

                                shell.setSize(shellSize);
                            }

                            // Only do this once in this workspace.
                            dialogSettings.put("resized", true);
                        }

                        // Setup the task that needs to be done to undo what we've done here.
                        final String finalOldLabel = oldLabel;
                        final Button finalSelectRangeButton = selectedRangeButton;
                        sessionCleanup = new Runnable() {
                            public void run() {
                                // Restore the grid data and dispose our control.
                                checkBoxGridData.horizontalSpan = 2;
                                checkBoxGridData.verticalAlignment = SWT.CENTER;
                                combo.dispose();

                                // Clean up the label change that we did.
                                if (finalOldLabel != null) {
                                    finalSelectRangeButton.setText(finalOldLabel);
                                }

                                UIUtil.asyncExec(group, new Runnable() {
                                    public void run() {
                                        // Defer the layout so that when the editor is switched to another EMF editor that supports find and replace,
                                        // we don't see a lot of flickering.
                                        shell.layout(true, true);
                                    }
                                });
                            }
                        };
                    }
                }
            }
        }
    }

    public void endSession() {
        // When the session ends, we clean up or search-type control.
        if (sessionCleanup != null) {
            sessionCleanup.run();
        }

        // Also do the clean that's done when the find and replace dialog loses focus.
        setScope(null);
    }

    public Point getLineSelection() {
        // This method is used only to compute region to pass to setScope.
        // So instead of computing something useless we use this opportunity to remember the viewer's selection.
        StructuredViewer viewer = getViewer();
        selectionScope = viewer.getStructuredSelection().toList();
        return new Point(0, 0);
    }

    public IRegion getScope() {
        // This method is kind of useless, we remember our scope when the getLineSelection is called.
        return new Region(0, 0);
    }

    public void setScope(IRegion scope) {
        // When the properties view is activated, it's sometimes given focus.
        // In that case we restore the focus to the dialog, but we need to given the scope changes that are caused by the transient focus changes.
        if (suspendScopeChanges) {
            return;
        }

        StructuredViewer viewer = getViewer();
        if (scope == null) {
            // Remember the objects that need label updating.
            Object[] selectionScopeObjectsToUpdate = selectionScopeObjects == null ? null
                    : selectionScopeObjects.toArray();
            Object selectionToUpdate = selectedItem != null && selectedItem.itemPropertyDescriptor == null
                    ? selectedItem.data.object
                    : null;

            // The scope is set to null when the dialog loses focus.
            // We should forget about most of our state at this point.
            selectionText = null;
            selectedItem = null;
            findReplaceable = false;
            selectionScopeObjects = null;

            propertiesCleanup();

            // Update update the selection scope objects.
            if (selectionScopeObjectsToUpdate != null) {
                viewer.update(selectionScopeObjectsToUpdate, null);
            }

            // Update the selection object.
            if (selectionToUpdate != null) {
                viewer.update(selectionToUpdate, null);
            }
        } else {
            // Record the objects in the selection scope.
            // These will be painted in a special way in the first to highlight them.
            selectionScopeObjects = new HashSet<Object>();

            int depth = -1;
            for (FindAndReplaceTarget.Data data : new TextData(viewer)) {
                // If we hit the next object at the same depend, reset the depth.
                if (data.depth == depth) {
                    depth = -1;
                }

                // If the object is directly in scope.
                if (selectionScope.contains(data.object)) {
                    // Remember the depth if we haven't remember one already.
                    if (depth == -1) {
                        depth = data.depth;
                    }
                }

                // If the object is a selected object or nested under a selected object, include it.
                if (depth != -1) {
                    selectionScopeObjects.add(data.object);
                }
            }

            // Hook up our decorating label provider for the current viewer.
            hookLabelProvider();

            // Eliminate the selection.
            viewer.setSelection(StructuredSelection.EMPTY);
        }
    }

    protected void propertiesCleanup() {
        // Clean up the stuff we did to the properties view.
        if (propertiesCleanup != null) {
            propertiesCleanup.run();
            propertiesCleanup = null;
        }
    }

    public void setScopeHighlightColor(Color color) {
        // This is never called by the find and replace dialog.
    }

    public String getSelectionText() {
        // This method is called for three different reasons.
        // 1 - For the initial text in the find field.
        // 2 - For enabling the replace button.
        // 3 - For recording a "selection" history; goodness knows what that's for though!
        //
        // If we're updating the button state, the selection text is used to enable/disable the replace button state.
        // We want it enabled only if we've selected an editable property.
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        if ("updateButtonState".equals(stackTrace[2].getMethodName())
                && (selectedItem == null || selectedItem.itemPropertyDescriptor == null
                        || !selectedItem.itemPropertyDescriptor.canSetProperty(selectedItem.data.object))) {
            return "";
        }

        // We use this only to initialize the text selection from a cell editor.
        // Once we've done a find, we clean this field.
        if (selectionText != null) {
            return selectionText;
        }

        // If there is a selected item, return its value.
        if (selectedItem != null) {
            return selectedItem.value;
        }

        // Otherwise we have no selection; we must return a non-null value.
        return "";
    }

    public Point getSelection() {
        if (pendingReplacements >= 0) {
            return new Point(pendingReplacements, 0);
        }

        // This method is super important for determining the point from which the next find processing will proceed.
        // If there is a selected item, we should proceed from the end of the current match.
        if (selectedItem != null) {
            Matcher matcher = selectedItemPattern.matcher(selectedItem.value);
            int size = 0;
            if (selectedItem.value.length() >= selectedItemStart && matcher.find(selectedItemStart)) {
                size = matcher.group().length();
            }

            Point point = new Point(selectedItem.index + selectedItemStart, size);
            return point;
        }

        // If there is an active property sheet tree.
        StructuredViewer viewer = getViewer();
        Tree propertySheetTree = getActivePropertySheetTree();
        if (propertySheetTree != null) {
            // Look through the selection.
            for (final TreeItem treeItem : propertySheetTree.getSelection()) {
                // If it has and EMF property descriptor
                PropertyDescriptor propertyDescriptor = getPropertyDescriptor(treeItem);
                if (propertyDescriptor != null) {
                    // Determine it's wrapped object and look for it in the induced text data.
                    Object object = getObject(propertyDescriptor);
                    for (FindAndReplaceTarget.Data data : new TextData(viewer)) {
                        // If we find it...
                        if (data.object == object) {
                            // Determine which feature is the selected feature.
                            Object feature = getFeature(propertyDescriptor);

                            // Collection all features before the selected feature.
                            List<Object> features = new ArrayList<Object>();
                            for (final TreeItem otherTreeIem : propertySheetTree.getItems()) {
                                PropertyDescriptor otherPropertyDescriptor = getPropertyDescriptor(otherTreeIem);
                                if (otherPropertyDescriptor != null) {
                                    Object otherFeature = getFeature(otherPropertyDescriptor);
                                    features.add(otherFeature);
                                    if (otherFeature == feature) {
                                        break;
                                    }
                                }
                            }

                            // Consider all the items.
                            Data.Item candidate = null;
                            for (Data.Item item : data.items) {
                                // If we find an exact match, return the information for it immediately, otherwise consider it a candidate.
                                Object itemFeature = item.getFeature();
                                if (itemFeature == feature) {
                                    return new Point(item.index, 0);
                                } else if (features.contains(itemFeature)) {
                                    candidate = item;
                                }
                            }

                            // If there is a candidate, return the information for it.
                            if (candidate != null) {
                                return new Point(candidate.index, 0);
                            }

                            // If we find nothing, there's no point in looking anywhere else.
                            break;
                        }
                    }
                }
            }
        }

        // Otherwise find the first item of an object in the selection.
        List<?> list = viewer.getStructuredSelection().toList();
        for (FindAndReplaceTarget.Data data : new TextData(viewer)) {
            if (list.contains(data.object)) {
                return new Point(data.items.get(0).index, 0);
            }
        }

        // Start at the beginning.
        return new Point(0, 0);
    }

    public void setSelection(int offset, int length) {
        // This method is always called right before setScope.
        // The information it provides is not useful and can be ignored.
    }

    public int findAndSelect(int offset, String findString, boolean searchForward, boolean caseSensitive,
            boolean wholeWord) {
        // This is never called, but we forward it sensibly anyway.
        return findAndSelect(offset, findString, searchForward, caseSensitive, wholeWord, false);
    }

    public int findAndSelect(int offset, String findString, boolean searchForward, boolean caseSensitive,
            boolean wholeWord, boolean regExSearch) {
        // Clear out the text used to populate the search text field once we've done a search.
        selectionText = null;

        // Compile the raw pattern early so it can throw an exception if it's not well formed.
        // The information in that exception is displayed to the user.
        if (regExSearch) {
            Pattern.compile(findString);
        }

        // A pattern will be constructed depending on the search parameters.
        String impliedPattern = findString;

        // If we're not doing a regular expression search, quote the pattern.
        if (!regExSearch) {
            impliedPattern = Pattern.quote(impliedPattern);
        }

        // If we want case-insensitive matching, encode that in the pattern.
        if (!caseSensitive) {
            impliedPattern = "(?i)" + impliedPattern;
        }

        // If we want whole word matching, add the word break delimiters to the pattern.
        if (wholeWord) {
            impliedPattern = "\\b" + impliedPattern + "\\b";
        }

        // This should always compile correctly.
        Pattern pattern = Pattern.compile(impliedPattern);

        // If there are pending replacements, don't bother searching.
        if (pendingReplacements >= 0) {
            selectedItemPattern = pattern;
            return --pendingReplacements;
        }

        StructuredViewer viewer = getViewer();

        // Iterate over the induced text, keeping track of a candidates for the case of backward searching.
        Data.Item candidate = null;
        int candidateStart = -1;
        LOOP: for (FindAndReplaceTarget.Data data : new TextData(viewer)) {
            // If we have no restricted scope or the we and the object is in that scope...
            if (selectionScopeObjects == null || selectionScopeObjects.contains(data.object)) {
                // Iterate over the items.
                for (Data.Item item : data.items) {
                    // If we're searching forward and the item is at or above the offset or the end of the string is above the offset...
                    // Otherwise if were's searching backward and the offset is -1, because we're wrapped, or the item is before the offset...
                    if (searchForward ? item.index >= offset || item.index + item.value.length() > offset
                            : offset == -1 || item.index < offset) {
                        // If we've just done a replace, we make sure that the next find will find something that can be replaced, i.e., a modifiable attribute.
                        // Otherwise we make sure that the item is included by the search type.
                        if (findReplaceable ? !SearchType.MODIFIABLE_ATTRIBUTE.isIncluded(item)
                                : searchType != null && !searchType.isIncluded(item)) {
                            continue;
                        }

                        // When searching forward, we need to make sure that we begin the pattern match where it skips the stuff before the current offset.
                        int begin = 0;
                        if (searchForward && item.index < offset) {
                            begin = offset - item.index;
                        }

                        // Look for a match from the starting point.
                        Matcher matcher = pattern.matcher(item.value);
                        if (matcher.find(begin)) {
                            // Determine the offset of the match.
                            int start = matcher.start();

                            if (searchForward) {
                                // If we're searching forward, we're so record and mark this selection point.
                                setSelection(false, viewer, item, start, pattern);

                                // Mark the fact that we've done a find but not a replace yet.
                                findReplaceable = false;

                                // Return the appropriate absolute index where we matched.
                                return item.index + start;
                            }
                            // Otherwise, if we're searching backward all the way to the end, or the match is not past the target offset...
                            else if (offset == -1 || item.index + start <= offset) {
                                // This is definitely a candidate.
                                candidate = item;
                                candidateStart = start;

                                // But keep searching for a better candidate later in the string.
                                while (matcher.find()) {
                                    // Repeat the same logic.
                                    start = matcher.start();
                                    if (offset == -1 || item.index + start <= offset) {
                                        candidate = item;
                                        candidateStart = start;
                                    }
                                }
                            }
                        }
                    }
                    // If we're searching backward and we've gone as far as we need to go...
                    else if (!searchForward && item.index > offset) {
                        // Break from the loop.
                        break LOOP;
                    }
                }
            }
        }

        // If there is a candidate (which is only possible if we're searching backwards...
        if (candidate != null) {
            // Record and mark this selection point.
            setSelection(false, viewer, candidate, candidateStart, pattern);

            // Mark the fact that we've done a find but not a replace yet.
            findReplaceable = false;

            // Return the appropriate absolute index where we matched.
            return candidate.index + candidateStart;
        }

        // There is no match.
        return -1;
    }

    /**
     * This records a match either initially or as a result of a find.
     */
    protected void setSelection(boolean preserve, StructuredViewer viewer, Data.Item item, final int start,
            Pattern pattern) {
        Object selectedObjectToUpdate = selectedItem != null && selectedItem.itemPropertyDescriptor == null
                ? selectedItem.data.object
                : null;

        // Remember the information about the item, pattern, and offset within the item of the match.
        selectedItem = item;
        selectedItemPattern = pattern;
        selectedItemStart = start;

        if (selectedObjectToUpdate != null) {
            viewer.update(selectedObjectToUpdate, null);
        }

        // There is no special tree item anymore.
        specialTreeItem = null;

        // If we haven't already done so, hook up the special label provider for providing selection feedback.
        hookLabelProvider();

        // Clean any previous stuff we did to decorate the properties view.
        propertiesCleanup();

        // In replace all mode, we don't want to provide any further feedback.
        if (replaceAllCommand == null) {
            // Select the item in the viewer, unless we're preserving the selection, i.e., during the initial feedback.
            StructuredSelection selection = new StructuredSelection(new TreePath(item.data.getPath()));
            if (!preserve) {
                viewer.setSelection(selection, true);
            }

            // If there is an active property page, update it's selection immediately.
            PropertySheetPage activePropertySheetPage = getActivePropertySheetPage();
            if (activePropertySheetPage != null) {
                activePropertySheetPage.selectionChanged(workbenchPart, selection);
            }

            // Make the properties view visible, creating it if necessary.
            IWorkbenchPartSite site = workbenchPart.getSite();
            IWorkbenchPage page = site.getPage();
            IViewPart viewPart = page.findView("org.eclipse.ui.views.PropertySheet");

            // Sometimes showing the properties view gives it focus, e.g., when the editor is maximized.
            Display display = site.getShell().getDisplay();
            Control oldFocusControl = display.getFocusControl();
            try {
                // Ignore scope changes while showing the properties view.
                suspendScopeChanges = true;
                if (item.itemPropertyDescriptor != null) {
                    viewPart = page.showView("org.eclipse.ui.views.PropertySheet", null,
                            IWorkbenchPage.VIEW_VISIBLE);
                    if (viewPart == null) {
                        viewPart = page.showView("org.eclipse.ui.views.PropertySheet", null,
                                IWorkbenchPage.VIEW_CREATE);
                    }
                }

                // Restore the focus.
                Control newFocusControl = display.getFocusControl();
                if (oldFocusControl != newFocusControl) {
                    oldFocusControl.setFocus();
                }
            } catch (PartInitException ex) {
                UIPlugin.INSTANCE.log(ex);
            } finally {
                suspendScopeChanges = false;
            }

            // If it is a property sheet, as expected...
            if (viewPart instanceof PropertySheet) {
                // And the current page is a property sheet page as expected...
                final PropertySheet propertySheet = (PropertySheet) viewPart;
                IPage currentPage = propertySheet.getCurrentPage();
                if (currentPage instanceof PropertySheetPage) {
                    // And the control is a tree...
                    Control control = currentPage.getControl();
                    if (control instanceof Tree) {
                        final Tree tree = (Tree) control;

                        if (item.itemPropertyDescriptor != null) {
                            // Remember the filter action that we needed to check it to be able to show an advanced property.
                            Action filterAction = null;

                            // If the property has filter flags...
                            String[] filterFlags = item.itemPropertyDescriptor.getFilterFlags(item.data.object);
                            if (filterFlags != null) {
                                for (String filterFlag : filterFlags) {
                                    // If the filter is one for expert property...
                                    if ("org.eclipse.ui.views.properties.expert".equals(filterFlag)) {
                                        Action action = ReflectUtil.getValue(FILTER_ACTION_FIELD, currentPage);
                                        if (!action.isChecked()) {
                                            // Run the action to show advanced properties, and remember that.
                                            action.setChecked(true);
                                            action.run();
                                            filterAction = action;
                                        }
                                    }
                                }
                            }

                            // Walk the tree items.
                            for (final TreeItem treeItem : tree.getItems()) {
                                // If there is an EMF property descriptor with a feature for the selected item...
                                PropertyDescriptor propertyDescriptor = getPropertyDescriptor(treeItem);
                                if (propertyDescriptor != null
                                        && propertyDescriptor.getFeature() == item.getFeature()) {
                                    // Consider the label shown in the tree verses the value of the selected item...
                                    String treeItemText = treeItem.getText(1);
                                    String itemValue = item.value;

                                    // We might need to replace the tree item's text with a special representation...
                                    specialStart = -1;

                                    // If they are're identical....
                                    if (!treeItemText.equals(itemValue)) {
                                        // Find the match, which really must be there, do we can determine the length of the match.
                                        Matcher matcher = pattern.matcher(itemValue);
                                        if (matcher.find(start)) {
                                            // Remember this special item, because we'll want to update it after we do a replace to show the replaced text.
                                            specialTreeItem = treeItem;

                                            // If the end of the match is after the end of the tree item's text, or the strings up until the end of the match are not
                                            // identical...
                                            int end = matcher.end();
                                            if (treeItemText.length() < end || !treeItemText.substring(0, end)
                                                    .equals(itemValue.substring(0, end))) {
                                                // Consider the starting point of the match, and work our way backward for 20 characters or until the preceding control
                                                // character.
                                                int begin = matcher.start();
                                                specialStart = 2;
                                                while (begin >= 0 && specialStart < 20
                                                        && !Character.isISOControl(itemValue.charAt(begin))) {
                                                    ++specialStart;
                                                    --begin;
                                                }

                                                // Work our way forward until the end of the string or until we hit a control character.
                                                int itemValueLength = itemValue.length();
                                                while (end < itemValueLength
                                                        && !Character.isISOControl(itemValue.charAt(end))) {
                                                    ++end;
                                                }

                                                // Create a special string with ellipses at both ends.
                                                String specialText = "..." + itemValue.substring(begin + 1, end)
                                                        + "...";

                                                // But that back into the item.
                                                treeItem.setText(1, specialText);

                                                // Get the tree to redraw itself.
                                                tree.redraw();
                                            }
                                        }
                                    }

                                    // Create a paint listener to select the match.
                                    final Listener paintItemListener = new Listener() {
                                        private void paintItem(Event event, TreeItem item, int matchStart) {
                                            String text = item.getText(1);
                                            Matcher matcher = selectedItemPattern.matcher(text);
                                            if (matchStart < text.length() && matcher.find(matchStart)) {
                                                // Compute the offset of the start of the matching, relative to the start of the text.
                                                int start = matcher.start();
                                                int x = event.gc.textExtent(text.substring(0, start)).x
                                                        + item.getTextBounds(1).x - treeItem.getBounds(1).x;

                                                // Compute the offset at the end of the match, taking into account the width of the matching text.
                                                int width = event.gc.textExtent(matcher.group()).x;
                                                event.gc.drawRectangle(event.x + x + 1, event.y, width + 1,
                                                        event.height - 1);
                                            } else if (text.endsWith("...")) {
                                                int x = event.gc.textExtent(text.substring(0, text.length() - 3)).x
                                                        + treeItem.getTextBounds(1).x - treeItem.getBounds(1).x;
                                                int width = event.gc.textExtent("...").x;
                                                event.gc.drawRectangle(event.x + x + 1, event.y, width + 1,
                                                        event.height - 1);
                                            }
                                        }

                                        public void handleEvent(Event event) {
                                            // If we're painting or special item...
                                            TreeItem item = (TreeItem) event.item;
                                            if (item == treeItem && event.index == 1) {
                                                paintItem(event, item, specialStart == -1 ? start : specialStart);
                                            }
                                        }
                                    };

                                    // Add the listener.
                                    tree.addListener(SWT.PaintItem, paintItemListener);

                                    // Set up the runnable to clean up what we've done here.
                                    final PropertySheetPage propertySheetPage = (PropertySheetPage) currentPage;
                                    final Action finalFilterAction = filterAction;
                                    propertiesCleanup = new Runnable() {
                                        public void run() {
                                            // Remove the listener.
                                            tree.removeListener(SWT.PaintItem, paintItemListener);

                                            // If there is a filter action we toggled...
                                            if (finalFilterAction != null) {
                                                // Toggle it back, which will refresh the view.
                                                finalFilterAction.setChecked(false);
                                                finalFilterAction.run();
                                            } else {
                                                // Otherwise refresh the view.
                                                propertySheetPage.refresh();
                                            }
                                        }
                                    };

                                    // Select the item, and force a repaint.
                                    tree.setSelection(treeItem);
                                    tree.redraw();

                                    // We're done.
                                    return;
                                }
                            }
                        }

                        // If we didn't find it at all, clear out the selection.
                        tree.setSelection(new TreeItem[0]);
                    }
                }
            }
        }
    }

    /**
     * This sets up a special label provider in the viewer to be able to highlight the scope and show the selected match.
     */
    protected void hookLabelProvider() {
        final StructuredViewer viewer = getViewer();

        // We use this special class so we can detect if the label provider is already hooked up.
        class DecoratingLabelProvider extends DelegatingStyledCellLabelProvider.FontAndColorProvider
                implements IStyledLabelProvider {
            public DecoratingLabelProvider(IStyledLabelProvider styledLabelProvider) {
                super(styledLabelProvider);
            }

            @Override
            public StyledString getStyledText(Object element) {
                return super.getStyledText(element);
            }
        }

        // If the label provider is already hooked up...
        final ILabelProvider labelProvider = (ILabelProvider) viewer.getLabelProvider();
        if (labelProvider instanceof DecoratingLabelProvider) {
            // Update the selection scope objects.
            if (selectionScopeObjects != null) {
                viewer.update(selectionScopeObjects.toArray(), null);
            }

            // Update the selected object if it's for a label.
            if (selectedItem != null && selectedItem.itemPropertyDescriptor == null) {
                viewer.update(selectedItem.data.object, null);
            }
        } else {
            // Create a styled label provider that can decorate the text.
            IStyledLabelProvider styledProvider = new DecoratingColumLabelProvider.StyledLabelProvider(
                    labelProvider, new IStyledLabelDecorator() {
                        // Use the color from the theme that the editor uses to highlight the scope.
                        final Color color = workbenchPart.getSite().getWorkbenchWindow().getWorkbench()
                                .getThemeManager().getCurrentTheme().getColorRegistry()
                                .get("org.eclipse.ui.editors.findScope");

                        final Styler scopeStyler = new Styler() {
                            @Override
                            public void applyStyles(TextStyle textStyle) {
                                textStyle.background = color;
                            }
                        };

                        public void removeListener(ILabelProviderListener listener) {
                            labelProvider.removeListener(listener);
                        }

                        public boolean isLabelProperty(Object element, String property) {
                            return labelProvider.isLabelProperty(element, property);
                        }

                        public void dispose() {
                            labelProvider.dispose();
                        }

                        public void addListener(ILabelProviderListener listener) {
                            labelProvider.addListener(listener);
                        }

                        public String decorateText(String text, Object element) {
                            if (labelProvider instanceof ILabelDecorator) {
                                ILabelDecorator labelDecorator = (ILabelDecorator) labelProvider;
                                return labelDecorator.decorateText(text, element);
                            }

                            return text;
                        }

                        public Image decorateImage(Image image, Object element) {
                            if (labelProvider instanceof ILabelDecorator) {
                                ILabelDecorator labelDecorator = (ILabelDecorator) labelProvider;
                                return labelDecorator.decorateImage(image, element);
                            }

                            return image;
                        }

                        public StyledString decorateStyledText(StyledString styledString, Object element) {
                            if (labelProvider instanceof IStyledLabelDecorator) {
                                IStyledLabelDecorator styledLabelDecorator = (IStyledLabelDecorator) labelProvider;
                                styledString = styledLabelDecorator.decorateStyledText(styledString, element);
                            }

                            // If we have a selected item, it's the item for the label, and this element is that selected element's object...
                            if (selectedItem != null && selectedItem.itemPropertyDescriptor == null
                                    && element == selectedItem.data.object) {
                                // Convert the styled string to just a string.
                                String string = styledString.getString();

                                // Find the pattern match within that string.
                                Matcher matcher = selectedItemPattern.matcher(string);
                                if (matcher.find(selectedItemStart)) {
                                    // Create a new styles string.
                                    StyledString result = new StyledString();

                                    // Recompose the string with the match styled to show a selection bod.
                                    String group = matcher.group();
                                    int start = matcher.start();
                                    int end = matcher.end();
                                    result.append(string.substring(0, start));
                                    result.append(group, MATCH_STYLER);
                                    result.append(string.substring(end));
                                    return result;
                                }
                            }

                            // If we have scope objects and the element is one of those...
                            if (selectionScopeObjects != null && selectionScopeObjects.contains(element)) {
                                // Mark the entire string with the scope styling.
                                StyledString result = new StyledString();
                                result.append(styledString.getString(), scopeStyler);
                                return result;
                            }

                            // Otherwise just pass through the string.
                            return styledString;
                        }
                    });

            // Hook up the label provider to be the one used by the view.
            ILabelProvider delegatingLabelProvider = new DecoratingLabelProvider(styledProvider);
            viewer.setLabelProvider(delegatingLabelProvider);
        }
    }

    public void replaceSelection(String text) {
        // This is never called, but delegate it appropriately nevertheless.
        replaceSelection(text, false);
    }

    public void replaceSelection(String text, boolean regExReplace) {
        // If we're in replace all mode.
        if (replaceAllCommand != null) {
            // And this is the first call to replace selection...
            if (pendingReplacements == Integer.MAX_VALUE - 1) {
                // Determine all the replacements that are applicable.
                pendingReplacements = replaceSelectionAll(text, regExReplace) - 1;
            }

            // Both find and replace will ignore the next pendingReplacements number of calls.
            return;
        }

        // If the selected item can't be modified...
        if (!SearchType.MODIFIABLE_ATTRIBUTE.isIncluded(selectedItem)) {
            return;
        }

        // If the pattern doesn't match...
        Matcher matcher = selectedItemPattern.matcher(selectedItem.value);
        if (!matcher.find(selectedItemStart)) {
            return;
        }

        // Build up the replacement.
        StringBuffer result = new StringBuffer();

        // Remember the start of the match...
        int start = matcher.start();

        // Escape the $ if we're not doing a regular expression replacement.
        String replacement = regExReplace ? text : text.replace("$", "\\$");

        // Append the replacement
        matcher.appendReplacement(result, replacement);

        // Remember exactly what we've replaced the pattern with.
        String actualReplacement = result.substring(start);

        // Complete the composition.
        matcher.appendTail(result);

        // We must have an editing domain and the selected item's feature must be a modifiable attribute.
        EditingDomain domain = ((IEditingDomainProvider) workbenchPart).getEditingDomain();
        EAttribute eAttribute = (EAttribute) selectedItem.getFeature();

        // Try to convert the value modified string to a value; this can fail.
        Object value;
        try {
            value = EcoreUtil.createFromString(eAttribute.getEAttributeType(), result.toString());
        } catch (RuntimeException exception) {
            return;
        }

        // If there is a special item in the properties view, we want to update the text to show the replacement.
        String replacementSpecialText = null;
        if (specialTreeItem != null && !specialTreeItem.isDisposed()) {
            String specialText = specialTreeItem.getText(1);
            Matcher specialMatcher = selectedItemPattern.matcher(specialText);
            if (specialMatcher.find(specialStart)) {
                StringBuffer specialResult = new StringBuffer();
                specialMatcher.appendReplacement(specialResult, replacement);
                specialMatcher.appendTail(specialResult);
                replacementSpecialText = specialResult.toString();
            }
        }

        // Remember the replacement as the pattern so that views showing the matching selection continue to show the matching replacement.
        selectedItemPattern = Pattern.compile(Pattern.quote(actualReplacement));

        Command setCommand;
        if (eAttribute.isMany()) {
            Object propertyValue = selectedItem.itemPropertyDescriptor.getPropertyValue(selectedItem.data.object);
            if (propertyValue instanceof IItemPropertySource) {
                propertyValue = ((IItemPropertySource) propertyValue).getEditableValue(selectedItem.data.object);
            }

            // Compute the new overall value for the list.
            List<Object> values = new ArrayList<Object>((List<?>) propertyValue);
            values.set(selectedItem.itemIndex, value);

            // Create a command to set the overall list value.
            setCommand = SetCommand.create(domain, selectedItem.data.object, eAttribute, values);
        } else {
            // Create a command to set the value.
            setCommand = SetCommand.create(domain, selectedItem.data.object, eAttribute, value);
        }

        // If this is not in replace all mode, we need to be careful that command execution will cause notification that will try to select the affected
        // objects.
        // This messes up or attempts to track the selection progress in the case of replace/find.
        CompoundCommand wrapper = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL) {
            boolean isFirst = true;

            @Override
            public Collection<?> getAffectedObjects() {
                // The first time this is called, it returns the empty list, so that no selection takes place.
                if (isFirst) {
                    isFirst = false;
                    return Collections.emptyList();
                }

                return super.getAffectedObjects();
            }
        };

        // Put the set command in the wrapper and execute the wrapper.
        wrapper.append(setCommand);
        domain.getCommandStack().execute(wrapper);

        // We need to wait for async executed updates to finish.
        Display display = getViewer().getControl().getShell().getDisplay();
        final boolean[] run = new boolean[] { true };
        display.asyncExec(new Runnable() {
            public void run() {
                run[0] = false;
            }
        });

        // Process the event queue until our runnable has run, at which point other runnables queued before ours will also have been completed.
        while (run[0] && display.readAndDispatch()) {
            display.sleep();
        }

        // If we have a special item.
        if (specialTreeItem != null) {
            // And it's not disposed yet.
            if (!specialTreeItem.isDisposed() && replacementSpecialText != null) {
                // Update it to the replacement text, and force it to repaint to highlight the replacement.
                specialTreeItem.setText(1, replacementSpecialText);
                specialTreeItem.getParent().redraw();
            }

            // We can forget about it now.
            specialTreeItem = null;
        }

        // It's important to relocate our selected item, because the replacement could have updated text that appears earlier in our induced text view.
        // This would mess up the current selected item's index.
        for (FindAndReplaceTarget.Data data : new TextData(getViewer())) {
            if (data.object == selectedItem.data.object) {
                for (Data.Item item : data.items) {
                    if (selectedItem.itemPropertyDescriptor == item.itemPropertyDescriptor
                            && selectedItem.itemIndex == item.itemIndex) {
                        selectedItem = item;
                    }
                }
            }
        }

        // We want the next find (especially for find/replace) to find only modifiable selections.
        findReplaceable = true;
    }

    protected int replaceSelectionAll(String text, boolean regExReplace) {
        // Escape the $ if we're not doing a regular expression replacement.
        String replacement = regExReplace ? text : text.replace("$", "\\$");

        // We must have an editing domain.
        EditingDomain domain = ((IEditingDomainProvider) workbenchPart).getEditingDomain();

        int total = 0;
        for (Data data : new TextData(getViewer())) {
            // We must defer creation of the set command for a multi-valued feature until after all its items are processed.
            EAttribute currentListAttribute = null;
            List<Object> currentListValue = null;

            for (Data.Item item : data.items) {
                // If the selected item can't be modified...
                if (!SearchType.MODIFIABLE_ATTRIBUTE.isIncluded(item)) {
                    continue;
                }

                // If the pattern doesn't match...
                Matcher matcher = selectedItemPattern.matcher(item.value);
                if (!matcher.find()) {
                    continue;
                }

                // Build up the replacement.
                StringBuffer result = new StringBuffer();

                // Keep track of the number of matches.
                int count = 0;
                do {
                    // Append the replacement
                    matcher.appendReplacement(result, replacement);
                    ++count;
                } while (matcher.find());

                // Complete the composition.
                matcher.appendTail(result);

                // The feature must be an attribute.
                EAttribute eAttribute = (EAttribute) item.getFeature();

                // If we've deferred a multi-valued feature change, and this is a different feature
                if (currentListAttribute != null && eAttribute != currentListAttribute) {
                    Command setCommand = SetCommand.create(domain, data.object, currentListAttribute,
                            currentListValue);
                    replaceAllCommand.append(setCommand);

                    currentListAttribute = null;
                    currentListValue = null;
                }

                // Try to convert the modified string to a value; this can fail.
                Object value;
                try {
                    value = EcoreUtil.createFromString(eAttribute.getEAttributeType(), result.toString());
                } catch (RuntimeException exception) {
                    continue;
                }

                total += count;

                if (eAttribute.isMany()) {
                    if (currentListValue == null) {
                        currentListAttribute = eAttribute;

                        Object propertyValue = item.itemPropertyDescriptor.getPropertyValue(data.object);
                        if (propertyValue instanceof IItemPropertySource) {
                            propertyValue = ((IItemPropertySource) propertyValue).getEditableValue(data.object);
                        }

                        // Compute the new overall value for the list.
                        currentListValue = new ArrayList<Object>((List<?>) propertyValue);
                    }

                    currentListValue.set(item.itemIndex, value);
                } else {
                    // Create a command to set the value.
                    Command setCommand = SetCommand.create(domain, data.object, eAttribute, value);
                    replaceAllCommand.append(setCommand);
                }
            }

            // If we've deferred a multi-valued feature change and it's not been processed in the above loop.
            if (currentListAttribute != null) {
                Command setCommand = SetCommand.create(domain, data.object, currentListAttribute, currentListValue);
                replaceAllCommand.append(setCommand);
            }
        }

        return total;
    }

    public void setReplaceAllMode(boolean replaceAll) {
        if (replaceAll) {
            // We want the next find to find only modifiable selections.
            findReplaceable = true;

            // We want all the commands to be batched into a single undoable command.
            replaceAllCommand = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL, "Replace All") {
            };
            pendingReplacements = Integer.MAX_VALUE;
        } else {
            try {
                // We must have an editing domain.
                EditingDomain domain = ((IEditingDomainProvider) workbenchPart).getEditingDomain();
                domain.getCommandStack().execute(replaceAllCommand);
            } finally {
                // We're done now.
                findReplaceable = false;
                replaceAllCommand = null;
                pendingReplacements = -1;
            }
        }
    }

    public static Object getAdapter(Class<?> adapter, IWorkbenchPart workbenchPart) {
        if (adapter == IFindReplaceTarget.class) {
            synchronized (FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS) {
                FindAndReplaceTarget findAndReplaceTarget = FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS
                        .get(workbenchPart);
                if (findAndReplaceTarget == null) {
                    findAndReplaceTarget = new FindAndReplaceTarget(workbenchPart);
                    FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS.put(workbenchPart, findAndReplaceTarget);
                }

                return findAndReplaceTarget;

            }
        }

        return null;
    }

    /**
     * Exacts the textual representation of the attribute-based property descriptor.
     * Returns <code>null</code> if the property is not attribute-based, or if the value is not representable as text.
     */
    protected static List<String> getText(IItemPropertyDescriptor itemPropertyDescriptor, Object object) {
        // If the feature is an attribute...
        Object feature = itemPropertyDescriptor.getFeature(object);
        if (feature instanceof EAttribute) {
            // If the attribute's type is serializeable...
            EAttribute eAttribute = (EAttribute) feature;
            EDataType eDataType = eAttribute.getEAttributeType();
            if (eDataType.isSerializable()) {
                // Extract the property value and unwrap it if necessary.
                Object value = itemPropertyDescriptor.getPropertyValue(object);
                if (value instanceof IItemPropertySource) {
                    value = ((IItemPropertySource) value).getEditableValue(object);
                }

                // If there is a value.
                if (value != null) {
                    // Always create a list.
                    List<String> result = new ArrayList<String>();
                    if (eAttribute.isMany()) {
                        // Add the textual representation of each value.
                        for (Object item : (List<?>) value) {
                            result.add(EcoreUtil.convertToString(eDataType, item));
                        }
                    } else {
                        // Ad the textual representation of the one value.
                        result.add(EcoreUtil.convertToString(eDataType, value));
                    }

                    return result;
                }
            }
        }

        return null;
    }

    /**
     * Returns the EMF property descriptor, if any, of the tree item.
     */
    protected static PropertyDescriptor getPropertyDescriptor(TreeItem treeItem) {
        Object data = treeItem.getData();
        if (data instanceof PropertySheetEntry) {
            PropertySheetEntry propertySheetEntry = (PropertySheetEntry) data;
            Object descriptor = ReflectUtil.invokeMethod(GET_DESCRIPTOR_METHOD, propertySheetEntry);
            if (descriptor instanceof PropertyDescriptor) {
                PropertyDescriptor propertyDescriptor = (PropertyDescriptor) descriptor;
                return propertyDescriptor;
            }
        }

        return null;
    }

    /**
     * Returns the underlying object of the EMF property descriptor.
     */
    protected static Object getObject(PropertyDescriptor propertyDescriptor) {
        Object object = ReflectUtil.getValue(OBJECT_FIELD, propertyDescriptor);
        return object;
    }

    /**
     * Returns the feature of item property descriptor of the EMF property descriptor.
     */
    protected static Object getFeature(PropertyDescriptor propertyDescriptor) {
        Object object = getObject(propertyDescriptor);
        IItemPropertyDescriptor itemPropertyDescriptor = ReflectUtil.getValue(ITEM_PROPERTY_DESCRIPTOR_FIELD,
                propertyDescriptor);
        return itemPropertyDescriptor.getFeature(object);
    }

    /**
     * This represents an element associated with each object in a tree.
     * @see TextData
     *
     * @author Ed Merks
     */
    public static class Data {
        /**
         * The object in the tree.
         */
        public Object object;

        /**
         * The depth of the object in the tree.
         */
        public int depth;

        /**
         * The parent data in the tree.
         */
        public Data parent;

        /**
         * The items associated with the object.
         */
        public List<Data.Item> items;

        public Data(int depth, Object object, List<Data.Item> items) {
            this.depth = depth;
            this.object = object;
            this.items = items;
        }

        public Object[] getPath() {
            List<Object> path = new ArrayList<Object>();
            for (Data data = this; data != null; data = data.parent) {
                path.add(0, data.object);
            }

            return path.toArray();
        }

        /**
         * This represents an item associated with each data item.
         *
         * @author Ed Merks
         */
        public static class Item {
            /**
             * The containing data of this item.
             */
            public FindAndReplaceTarget.Data data;

            /**
             * The index of this item in the overall {@link TextData induced} textual representation.
             */
            public int index;

            /**
             * The property descriptor associated with this item.
             * The <code>null</value> represents the label value of the data object.
             */
            public IItemPropertyDescriptor itemPropertyDescriptor;

            /**
             * Each value in a multi-valued feature is represented as an item.
             * This is the index of that item in its list.
             */
            public int itemIndex;

            /**
             * The textual value of the item.
             */
            public String value;

            public Item(FindAndReplaceTarget.Data data, int index, IItemPropertyDescriptor itemPropertyDescriptor,
                    int itemIndex, String value) {
                this.data = data;
                this.index = index;
                this.itemPropertyDescriptor = itemPropertyDescriptor;
                this.itemIndex = itemIndex;
                this.value = value;
            }

            public Object getFeature() {
                if (itemPropertyDescriptor != null) {
                    return itemPropertyDescriptor.getFeature(data.object);
                }

                return null;
            }
        }
    }

    /**
     * This supports iterating over an induced textual representation of a structured viewer's structure.
     *
     * @author Ed Merks
     */
    public static class TextData implements Iterable<FindAndReplaceTarget.Data> {
        private StructuredViewer viewer;

        private ILabelProvider labelProvider;

        private IPropertySourceProvider propertySourceProvider;

        public TextData(StructuredViewer viewer) {
            this.viewer = viewer;
            labelProvider = (ILabelProvider) viewer.getLabelProvider();
            IContentProvider contentProvider = viewer.getContentProvider();
            if (contentProvider instanceof IPropertySourceProvider) {
                propertySourceProvider = (IPropertySourceProvider) contentProvider;
            }
        }

        public Iterator<FindAndReplaceTarget.Data> iterator() {
            final StructuredViewerTreeIterator structuredViewerTreeIterator;
            IContentProvider contentProvider = viewer.getContentProvider();
            if (contentProvider instanceof StructuredViewerTreeIterator.Provider) {
                structuredViewerTreeIterator = ((StructuredViewerTreeIterator.Provider) contentProvider).create();
            } else {
                structuredViewerTreeIterator = new StructuredViewerTreeIterator(viewer);
            }

            // This is an iterator that delegates to an iterator that walks the structure of the viewer.
            return new Iterator<FindAndReplaceTarget.Data>() {
                private List<Data> parents = new ArrayList<Data>();

                // This keeps track of the textual index as we iterate.
                private int index;

                public boolean hasNext() {
                    return structuredViewerTreeIterator.hasNext();
                }

                public FindAndReplaceTarget.Data next() {
                    // Keep track of the depth before calling next.
                    int depth = structuredViewerTreeIterator.size();
                    Object object = structuredViewerTreeIterator.next();

                    // Create a list of items for this object and use that to create a new data representation.
                    List<Data.Item> items = new ArrayList<Data.Item>();
                    FindAndReplaceTarget.Data data = new Data(depth - 1, object, items);

                    if (parents.size() < depth) {
                        parents.add(data);
                    } else {
                        parents.set(depth - 1, data);
                    }

                    if (depth > 1) {
                        data.parent = parents.get(depth - 2);
                    }

                    // Add an item for the label.
                    String label = labelProvider.getText(object);
                    items.add(new Data.Item(data, index, null, 0, label));
                    index += label.length();

                    // If we have a source provider...
                    if (propertySourceProvider != null) {
                        // And we have an EMF property source for the object...
                        IPropertySource propertySource = propertySourceProvider.getPropertySource(object);
                        if (propertySource instanceof PropertySource) {
                            // Extract the EMF property source so we can iterate directly over the EMF property descriptors.
                            PropertySource emfPropertySource = (PropertySource) propertySource;
                            IItemPropertySource itemPropertySource = ReflectUtil
                                    .getValue(ITEM_PROPERTY_SOURCE_FIELD, emfPropertySource);
                            for (IItemPropertyDescriptor itemPropertyDescriptor : itemPropertySource
                                    .getPropertyDescriptors(object)) {
                                // Extract the textual values of the property, if there are any.
                                List<String> text = getText(itemPropertyDescriptor, object);
                                if (text != null) {
                                    // Create an item for each value.
                                    for (int i = 0, size = text.size(); i < size; ++i) {
                                        String value = text.get(i);
                                        items.add(new Data.Item(data, index, itemPropertyDescriptor, i, value));
                                        index += value.length();
                                    }
                                }
                            }
                        }
                    }

                    return data;
                }

                public void remove() {
                    throw new UnsupportedOperationException("remove");
                }
            };
        }
    }

    /**
     * This enumerates the types of searches that are possible for an EMF-based structured editor.
     * @author Ed Merks
     */
    private enum SearchType {
        LABEL_AND_ATTRIBUTE() {
            @Override
            public boolean isIncluded(Data.Item item) {
                return item != null;
            }

            @Override
            public String key() {
                return "label+attribute";
            }

            @Override
            public String label() {
                return "Labels and attributes";
            }
        },
        LABEL() {
            @Override
            public boolean isIncluded(Data.Item item) {
                return item != null && item.itemPropertyDescriptor == null;
            }

            @Override
            public String key() {
                return "label";
            }

            @Override
            public String label() {
                return "Labels";
            }
        },
        ATTRIBUTE() {
            @Override
            public boolean isIncluded(Data.Item item) {
                return item != null && item.itemPropertyDescriptor != null;
            }

            @Override
            public String key() {
                return "attribute";
            }

            @Override
            public String label() {
                return "Attributes";
            }
        },
        MODIFIABLE_ATTRIBUTE {
            @Override
            public boolean isIncluded(Data.Item item) {
                return item != null && item.itemPropertyDescriptor != null
                        && item.itemPropertyDescriptor.canSetProperty(item.data.object);
            }

            @Override
            public String key() {
                return "modifiable-attribute";
            }

            @Override
            public String label() {
                return "Modifiable attributes";
            }
        };

        public abstract boolean isIncluded(Data.Item item);

        public abstract String key();

        public abstract String label();

        public static FindAndReplaceTarget.SearchType getSearchType(String key) {
            for (FindAndReplaceTarget.SearchType searchType : SearchType.values()) {
                if (searchType.key().equals(key)) {
                    return searchType;
                }
            }

            return LABEL_AND_ATTRIBUTE;
        }

        public static String[] getLabels() {
            FindAndReplaceTarget.SearchType[] values = SearchType.values();
            String[] labels = new String[values.length];
            for (int i = 0; i < values.length; ++i) {
                labels[i] = values[i].label();
            }

            return labels;
        }
    }
}