/*
 * Copyright (c) Eclipse contributors and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 */
package org.eclipse.emf.edit.ui.util;


import java.lang.reflect.Field;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.CommonPlugin;
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.IItemLabelProvider;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.provider.ItemPropertyDescriptorDecorator;
import org.eclipse.emf.edit.ui.EMFEditUIPlugin;
import org.eclipse.emf.edit.ui.action.EditingDomainActionBarContributor;
import org.eclipse.emf.edit.ui.action.FindAction;
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.emf.edit.ui.view.ExtendedPropertySheetPage;
import org.eclipse.emf.edit.ui.view.ExtendedPropertySheetPage.ExtendedPropertySheetEntry;
import org.eclipse.jface.action.IAction;
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.CellLabelProvider;
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.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
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.Font;
import org.eclipse.swt.graphics.FontData;
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.part.WorkbenchPart;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
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.PropertySheetPage;
import org.eclipse.ui.views.properties.PropertySheetSorter;


/**
 * An implementation of a find and replace target specialized to work with EMF Editors and EMF's {@link ExtendedPropertySheetPage}.
 * Because platform internals are used, which are subject to arbitrary change, this class is final and the constructor is private.
 * The only public entry point for creating an instance is {@link #getAdapter(Class, IWorkbenchPart, AbstractUIPlugin)}.
 * <p>
 * This find and replace target expects to be used by the platform's {@code org.eclipse.ui.texteditor.FindReplaceDialog},
 * i.e., it reuses the same find and replace dialog that is used by the platform's text editors,
 * specializing that dialog with addition controls, i.e., support for a {@link SearchType search type} control.
 * </p>
 * <p>
 * To exploit this implementation, {@link FindAction create} a find action the constructor of a derived {@link EditingDomainActionBarContributor}
 * and specialize the {@link WorkbenchPart#getAdapter(Class)} method in the editor implementation class to call the {@link #getAdapter(Class, IWorkbenchPart, AbstractUIPlugin) getAdapter} method of this class.
 * This will result in the {@code Find/Replace...} appearing in the context menu and {@code Edit} menu, bound to the search hot key, generally {@code Ctrl-F},
 * and will result in an instance of this class being created, bound to the viewers in your editor.
 * But take note, this implementation's behavior is <b>invasive</b>, i.e., it replaces the label provider of the structure viewer with a wrapper label provider
 * that is able to show boxes around the matching search text.
 * <p>
 * <p>
 * If your content trees are potentially infinite, or you otherwise wish to control which parts of the tree are searched, you can specialize each structured viewer's {@link StructuredViewerTreeIterator}.
 * If you have specialized property sheet's {@link PropertySheetPage#setSorter(PropertySheetSorter) sorter}, you must {@link #setCollator(Collator) set the collator} used by this implementation to sort properties and property categories.
 * </p>
 *
 * @since 2.14
 */
public final class FindAndReplaceTarget implements IFindReplaceTarget, IFindReplaceTargetExtension, IFindReplaceTargetExtension3
{
  /**
   * A map for managing the association between a workbench part and it associated find and replace target.
   */
  private static final Map<IWorkbenchPart, FindAndReplaceTarget> FIND_AND_REPLACE_TARGETS = new WeakHashMap<IWorkbenchPart, FindAndReplaceTarget>();

  /**
   * A styler for drawing a box around matching text.
   */
  private static final Styler MATCH_STYLER = new Styler()
    {
      @Override
      public void applyStyles(TextStyle textStyle)
      {
        textStyle.borderStyle = SWT.BORDER_SOLID;
      }
    };

  /**
   * Provides access to the {@code fIsRegExCheckBox} of a {@code org.eclipse.ui.texteditor.FindReplaceDialog}.
   */
  private static final Field F_IS_REGEX_CHECK_BOX_FIELD;

  /**
   * Provides access to the {@code fSelectedRangeRadioButton} of a {@code org.eclipse.ui.texteditor.FindReplaceDialog}.
   */
  private static final Field F_SELECTED_RANGE_RADIO_BUTTON_FIELD;

  static
  {
    Field fIsRegExCheckBoxField = null;
    Field fSelectedRangeRadioButtonField = null;
    try
    {
      Class<?> findReplaceDialogClass = CommonPlugin.loadClass("org.eclipse.ui.workbench.texteditor", "org.eclipse.ui.texteditor.FindReplaceDialog");
      fIsRegExCheckBoxField = findReplaceDialogClass.getDeclaredField("fIsRegExCheckBox");
      fIsRegExCheckBoxField.setAccessible(true);
      fSelectedRangeRadioButtonField = findReplaceDialogClass.getDeclaredField("fSelectedRangeRadioButton");
    }
    catch (Throwable throwable)
    {
      // Ignore.
    }
    F_IS_REGEX_CHECK_BOX_FIELD = fIsRegExCheckBoxField;
    F_SELECTED_RANGE_RADIO_BUTTON_FIELD = fSelectedRangeRadioButtonField;
  }

  /**
   * We need a tiny font to produce a bit of white space in the labels.
   * @see #hookLabelProvider()
   */
  private static final Map<Font, Font> TINY_FONT = new HashMap<Font, Font>();

  /**
   * The workbench part of this find and replace target.
   */
  private final IWorkbenchPart workbenchPart;

  /**
   * The plugin used for saving and fetching the dialog settings of the find/replace dialog.
   */
  private final AbstractUIPlugin plugin;

  /**
   * The collator used to {@link org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.TextData#sort(List, Object) sort properties}.
   */
  Collator collator = Collator.getInstance();

  /**
   * A cleanup action for removing the paint lister from the properties view and for refreshing its labels.
   *
   * @see FindAndReplaceTarget#propertiesCleanup()
   * @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
   */
  private Runnable propertiesCleanup;

  /**
   * A cleanup action for when the session ends.
   *
   * @see #addSearchTypeControl()
   * @see #endSession()
   */
  private Runnable sessionCleanup;

  /**
   * If an advanced property needs to be displayed for search results and that action is currently not checked,
   * it will be checked and this cleanup action will be set to restore the state when the session ends.
   *
   * @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
   */
  private Runnable filterChangeCleanup;

  /**
   * Properties are always search assuming the are categorized.
   * The properties view state will be changed to match, and if it was changed, this cleanup action will be set to restore the state when the session ends.
   *
   * @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
   */
  private Runnable categoriesChangeCleanup;

  /**
   * Set to the objects in the selection of the viewer by {@link #getLineSelection()} and used by {@link #setScope(IRegion)}.
   */
  private List<?> selectionScope;

  /**
   * Use to control the scope of all objects that will be searched.
   */
  private Set<Object> selectionScopeObjects;

  /**
   * Initialized with the initial text to search based on the text of whatever is select in whatever viewer is active.
   */
  private String selectionText;

  /**
   * The current selected item.
   * @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
   */
  private Data.Item selectedItem;

  /**
   * The current selection start.
   * @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
   */
  private int selectedItemStart;

  /**
   * The current pattern being selected.
   * @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
   */
  private Pattern selectedItemPattern;

  /**
   * The command for doing replace-all.
   * @see #replaceSelectionAll(String, boolean)
   * @see #setReplaceAllMode(boolean)
   */
  private CompoundCommand replaceAllCommand;

  /**
   * Used for processing all replacements.
   */
  private int pendingReplacements = -1;

  /**
   * Used to control whether replace-all is enabled. Of course you can only modify editable properties.
   */
  private boolean findReplaceable;

  /**
   * The types of things to search.
   */
  private FindAndReplaceTarget.SearchType searchType;

  /**
   * A tree item in the properties view that's currently being processed for search/replace.
   */
  private TreeItem specialTreeItem;

  /**
   * If the special tree item can't show the matching text, e.g., because there are control characters, this handles the display of the matching text.
   */
  private int specialStart;

  /**
   * This avoids changes in {@link #setScope(IRegion)} while the properties view is being made visible.
   */
  private boolean suspendScopeChanges;

  /**
   * Adapter the workbench part to an {@link IFindReplaceTarget}.
   * @param adapter the adapter type; only {@code IFindReplaceTarget.class} will return a non-null result.
   * @param workbenchPart the workbench part to adapt.
   * @param plugin the plugin used for {@link AbstractUIPlugin#getDialogSettings() dialog settings}.
   * @return a {@link FindAndReplaceTarget} or {@code null}.
   */
  public static <T> T getAdapter(Class<T> adapter, IWorkbenchPart workbenchPart, AbstractUIPlugin plugin)
  {
    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, plugin);
          FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS.put(workbenchPart, findAndReplaceTarget);
        }
        return adapter.cast(findAndReplaceTarget);
      }
    }
    return null;
  }

  /**
   * Creates an instance for the given workbench part and the plugin used for {@link AbstractUIPlugin#getDialogSettings() dialog settings}.
   * @param workbenchPart the workbench part.
   * @param plugin the plugin used for dialog settings.
   */
  private FindAndReplaceTarget(IWorkbenchPart workbenchPart, AbstractUIPlugin plugin)
  {
    this.workbenchPart = workbenchPart;
    this.plugin = plugin;
  }

  /**
   * Returns the collator used to sort properties and property categories.
   * @return the collator used to sort properties and property categories.
   */
  public Collator getCollator()
  {
    return collator;
  }

  /**
   * Sets the collator used to sort properties and property categories.
   * @param collator the collator used to sort properties and property categories.
   */
  public void setCollator(Collator collator)
  {
    this.collator = collator;
  }

  /**
   * 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 ExtendedPropertySheetPage getActivePropertySheetPage()
  {
    IWorkbenchPart activePart = workbenchPart.getSite().getPage().getActivePart();
    if (activePart instanceof PropertySheet)
    {
      PropertySheet propertySheet = (PropertySheet)activePart;
      IPage currentPage = propertySheet.getCurrentPage();
      if (currentPage instanceof ExtendedPropertySheetPage)
      {
        ExtendedPropertySheetPage propertySheetPage = (ExtendedPropertySheetPage)currentPage;
        return propertySheetPage;
      }
    }

    return null;
  }

  /**
   * Returns the tree of the active property sheet page.
   */
  protected Tree getActivePropertySheetTree()
  {
    ExtendedPropertySheetPage 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 that has appropriate label and content providers, we can support find.
    StructuredViewer viewer = getViewer();
    return viewer != null && viewer.getLabelProvider() instanceof ILabelProvider && viewer.getContentProvider() instanceof IStructuredContentProvider;
  }

  /**
   * Called by {@link #beginSession()} to populate the initial {@link #selectionText}.
   */
  protected 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.
        Collection<? extends PropertyDescriptor> propertyDescriptors = getPropertyDescriptors(treeItem).keySet();
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
        {
          // If so, extract the object and look for it in the data.
          Object object = propertyDescriptor.getObject();
          for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
          {
            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 = ((IStructuredSelection)viewer.getSelection()).toList();
      for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
      {
        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()
  {
    initialize(workbenchPart);

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

  /**
   * Used to access the private fields of {@org.eclipse.ui.texteditor.FindReplaceDialog}.
   */
  private static Object getFieldValue(Dialog dialog, Field field)
  {
    try
    {
      return field.get(dialog);
    }
    catch (Exception exception)
    {
      return null;
    }
  }

  /**
   * Modifies the  {@org.eclipse.ui.texteditor.FindReplaceDialog} to add our {@link SearchType search type controls}.
   */
  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 = getFieldValue(dialog, F_IS_REGEX_CHECK_BOX_FIELD);
        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 plugin's dialog settings.
            IDialogSettings pluginDialogSettings = plugin.getDialogSettings();
            final IDialogSettings dialogSettings = pluginDialogSettings.getSection("org.eclipse.ui.texteditor.FindReplaceDialog") == null
              ? pluginDialogSettings.addNewSection("org.eclipse.ui.texteditor.FindReplaceDialog") : pluginDialogSettings.getSection("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 = getFieldValue(dialog, F_SELECTED_RANGE_RADIO_BUTTON_FIELD);
            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);
                  }

                  group.getDisplay().asyncExec(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.
                        if (!group.isDisposed() && !shell.isDisposed())
                        {
                          shell.layout(true, true);
                        }
                      }
                    });

                  if (filterChangeCleanup != null)
                  {
                    filterChangeCleanup.run();
                  }

                  if (categoriesChangeCleanup != null)
                  {
                    categoriesChangeCleanup.run();
                  }
                }
              };
          }
        }
      }
    }
  }

  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 = ((IStructuredSelection)viewer.getSelection()).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, collator))
      {
        // 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);
    }
  }

  /**
   * Handles the cleanup of the properties view.
   * @see #propertiesCleanup
   */
  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
        Collection<? extends PropertyDescriptor> propertyDescriptors = getPropertyDescriptors(treeItem).keySet();
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
        {
          // Determine it's wrapped object and look for it in the induced text data.
          Object object = propertyDescriptor.getObject();
          for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
          {
            // 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>();
              LOOP: for (final TreeItem otherTreeIem : propertySheetTree.getItems())
              {
                Collection<? extends PropertyDescriptor> otherPropertyDescriptors = getPropertyDescriptors(otherTreeIem).keySet();
                for (PropertyDescriptor otherPropertyDescriptor : otherPropertyDescriptors)
                {
                  Object otherFeature = getFeature(otherPropertyDescriptor);
                  features.add(otherFeature);
                  if (otherFeature == feature)
                  {
                    break LOOP;
                  }
                }
              }

              // 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 = ((IStructuredSelection)viewer.getSelection()).toList();
    for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
    {
      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, collator))
    {
      // 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, 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.
      ISelection selection = new StructuredSelection(new TreePath(item.data.getPath()));
      if (!preserve)
      {
        viewer.setSelection(selection, true);
        selection = viewer.getSelection();

        // If there is an active property page, update its selection immediately.
        ExtendedPropertySheetPage 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 exception)
      {
        plugin.getLog().log(new Status(IStatus.WARNING, plugin.getBundle().getSymbolicName(), 0, exception.getLocalizedMessage(), null));
      }
      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 ExtendedPropertySheetPage)
        {
          // And the control is a tree...
          Control control = currentPage.getControl();
          if (control instanceof Tree)
          {
            final Tree tree = (Tree)control;

            if (item.itemPropertyDescriptor != 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))
                  {
                    final IAction action = ((ExtendedPropertySheetPage)currentPage).getFilterAction();
                    if (!action.isChecked())
                    {
                      // Run the action to show advanced properties, and remember that.
                      action.setChecked(true);
                      action.run();
                      filterChangeCleanup = new Runnable()
                        {
                          public void run()
                          {
                            action.setChecked(false);
                            action.run();
                          }
                        };
                    }
                  }
                }
              }

              // Remember the categories action that we need to ensure is checked because we search in an order that assumes this.
              final IAction categoriesAction = ((ExtendedPropertySheetPage)currentPage).getCategoriesAction();
              if (!categoriesAction.isChecked())
              {
                categoriesAction.setChecked(true);
                categoriesAction.run();
                categoriesChangeCleanup = new Runnable()
                  {
                    public void run()
                    {
                      categoriesAction.setChecked(false);
                      categoriesAction.run();
                    }
                  };
              }

              // Walk the tree items.
              for (TreeItem rootTreeItem : tree.getItems())
              {
                // If there is an EMF property descriptor with a feature for the selected item...
                Map<? extends PropertyDescriptor, TreeItem> propertyDescriptors = getPropertyDescriptors(rootTreeItem);
                for (Map.Entry<? extends PropertyDescriptor, TreeItem> entry : propertyDescriptors.entrySet())
                {
                  PropertyDescriptor propertyDescriptor = entry.getKey();
                  final TreeItem propertyDescriptorTreeItem = entry.getValue();
                  if (propertyDescriptor.getFeature() == item.getFeature())
                  {
                    // Consider the label shown in the tree versus the value of the selected item...
                    String treeItemText = propertyDescriptorTreeItem.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 = propertyDescriptorTreeItem;

                        // 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.
                          propertyDescriptorTreeItem.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 - propertyDescriptorTreeItem.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 + propertyDescriptorTreeItem.getTextBounds(1).x
                              - propertyDescriptorTreeItem.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 == propertyDescriptorTreeItem && 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 ExtendedPropertySheetPage propertySheetPage = (ExtendedPropertySheetPage)currentPage;
                    propertiesCleanup = new Runnable()
                      {
                        public void run()
                        {
                          // Remove the listener.
                          tree.removeListener(SWT.PaintItem, paintItemListener);

                          // Refresh the view.
                          propertySheetPage.refreshLabels();
                        }
                      };

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

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

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

  /**
   * The styled text decorator used to show highlight box around the matching text in structured viewer's label.
   */
  protected final class StyledLabelDecorator implements IStyledLabelDecorator
  {
    private final ILabelProvider labelProvider;

    // 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;
        }
      };

    private final Styler tinyFontStyler;

    private StyledLabelDecorator(ILabelProvider labelProvider, Styler tinyFontStyler)
    {
      this.labelProvider = labelProvider;
      this.tinyFontStyler = tinyFontStyler;
    }

    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 box.
          String group = matcher.group();
          int start = matcher.start();
          int end = matcher.end();
          result.append(string.substring(0, start));
          Styler tinyPadding = new Styler()
            {
              @Override
              public void applyStyles(TextStyle textStyle)
              {
                MATCH_STYLER.applyStyles(textStyle);
                tinyFontStyler.applyStyles(textStyle);
              }
            };
          result.append(" ", tinyPadding);
          result.append(group, MATCH_STYLER);
          result.append(" ", tinyPadding);
          result.append(string.substring(end));
          result.append(" ", tinyFontStyler);
          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);
        result.append(" ", tinyFontStyler);
        return result;
      }

      // Otherwise just pass through the string with a tiny bit of extra white space at the end.
      styledString.append(" ", tinyFontStyler);
      return styledString;
    }
  }

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

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

  /**
   * 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();

    // Create a very tiny font that can be used to make very tiny spaces.
    Font font = viewer.getControl().getFont();
    Font tinyFont = TINY_FONT.get(font);
    if (tinyFont == null)
    {
      FontData[] fontData = font.getFontData();
      for (FontData data : fontData)
      {
        data.setHeight(data.getHeight() / 4);
      }
      tinyFont = new Font(font.getDevice(), fontData);
      TINY_FONT.put(font, tinyFont);
    }

    // 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 styler for creating very small spaces.
      final Font finalTinyFont = tinyFont;
      Styler tinyFontStyler = new Styler()
        {
          @Override
          public void applyStyles(TextStyle textStyle)
          {
            textStyle.font = finalTinyFont;
          }
        };
      // Create a styled label provider that can decorate the text.
      IStyledLabelProvider styledProvider = new DecoratingColumLabelProvider.StyledLabelProvider(labelProvider, new StyledLabelDecorator(labelProvider, tinyFontStyler))
        {
          {
            if (labelProvider instanceof CellLabelProvider)
            {
              cellLabelProvider = (CellLabelProvider)labelProvider;
            }
          }
        };

      // 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(), collator))
    {
      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;
  }

  /**
   * Use to help process all matches to be replaced.
   * @see #replaceSelection(String, boolean)
   */
  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(), collator))
    {
      // 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;
      }
    }
  }

  /**
   * 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 (itemPropertyDescriptor instanceof ItemPropertyDescriptorDecorator)
    {
      ItemPropertyDescriptorDecorator itemPropertyDescriptorDecorator = (ItemPropertyDescriptorDecorator)itemPropertyDescriptor;
      return getText(itemPropertyDescriptorDecorator.getDecoratedItemPropertyDescriptor(), itemPropertyDescriptorDecorator.getDecoratedObject());
    }

    List<String> result = new ArrayList<String>();

    // Extract the property value and unwrap it if necessary.
    Object propertyValue = itemPropertyDescriptor.getPropertyValue(object);
    if (propertyValue instanceof IItemPropertySource)
    {
      propertyValue = ((IItemPropertySource)propertyValue).getEditableValue(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())
      {
        // If there is a value.
        if (propertyValue != null)
        {
          boolean useLabel = false;
          // Always create a list.
          if (eAttribute.isMany())
          {
            // Add the textual representation of each value.
            for (Object item : (List<?>)propertyValue)
            {
              try
              {
                result.add(EcoreUtil.convertToString(eDataType, item));
              }
              catch (Exception exception)
              {
                useLabel = true;
                break;
              }
            }
          }
          else
          {
            // Add the textual representation of the one value.
            try
            {
              result.add(EcoreUtil.convertToString(eDataType, propertyValue));
            }
            catch (RuntimeException exception)
            {
              useLabel = true;
            }
          }

          if (!useLabel)
          {
            // return result;
          }
        }
      }
    }

    // Use the label as displayed in the properties view's value column.
    IItemLabelProvider labelProvider = itemPropertyDescriptor.getLabelProvider(object);
    if (labelProvider != null && propertyValue != null)
    {
      // Make sure it's not duplicated text.
      String text = labelProvider.getText(propertyValue);
      for (String otherText : result)
      {
        if (otherText.contains(text))
        {
          return result;
        }
      }
      result.add(text);
    }

    return result;
  }

  /**
   * Returns the EMF property descriptors, mapped to the corresponding the tree item.
   * It will walked nested properties and property categories.
   */
  protected static Map<? extends PropertyDescriptor, TreeItem> getPropertyDescriptors(TreeItem treeItem)
  {
    Map<PropertyDescriptor, TreeItem> result = new LinkedHashMap<PropertyDescriptor, TreeItem>();
    Object data = treeItem.getData();
    if (data instanceof ExtendedPropertySheetEntry)
    {
      ExtendedPropertySheetEntry propertySheetEntry = (ExtendedPropertySheetEntry)data;
      IPropertyDescriptor descriptor = propertySheetEntry.getDescriptor();
      if (descriptor instanceof PropertyDescriptor)
      {
        PropertyDescriptor propertyDescriptor = (PropertyDescriptor)descriptor;
        result.put(propertyDescriptor, treeItem);
        if (treeItem.getItemCount() != 0)
        {
          // Force the content provider to create the child items without actually expanding the tree.
          Event event = new Event();
          event.item = treeItem;
          treeItem.getParent().notifyListeners(SWT.Expand, event);

          for (TreeItem childItem : treeItem.getItems())
          {
            result.putAll(getPropertyDescriptors(childItem));
          }
        }
      }
    }
    else
    {
      for (TreeItem childTreeItem : treeItem.getItems())
      {
        result.putAll(getPropertyDescriptors(childTreeItem));
      }
    }
    return result;
  }

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

  /**
   * This represents an element associated with each object in a tree.
   * @see TextData
   */
  protected 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.
     */
    protected 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.
   */
  protected static class TextData implements Iterable<FindAndReplaceTarget.Data>
  {
    private final StructuredViewer viewer;

    private final Collator collator;

    private final ILabelProvider labelProvider;

    private final IPropertySourceProvider propertySourceProvider;

    public TextData(StructuredViewer viewer, Collator collator)
    {
      this.viewer = viewer;
      this.collator = collator;
      labelProvider = (ILabelProvider)viewer.getLabelProvider();
      IContentProvider contentProvider = viewer.getContentProvider();
      propertySourceProvider = contentProvider instanceof IPropertySourceProvider ? (IPropertySourceProvider)contentProvider : null;
    }

    public Iterator<FindAndReplaceTarget.Data> iterator()
    {
      // This is an iterator that delegates to an iterator that walks the structure of the viewer.
      final StructuredViewerTreeIterator structuredViewerTreeIterator = StructuredViewerTreeIterator.create(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 = emfPropertySource.getItemPropertySource();
                visit(items, data, itemPropertySource, object);
              }
            }

            return data;
          }

          private void visit(List<Data.Item> items, FindAndReplaceTarget.Data data, IItemPropertySource itemPropertySource, Object object)
          {
            for (IItemPropertyDescriptor itemPropertyDescriptor : sort(itemPropertySource.getPropertyDescriptors(object), 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();
                }
              }

              Object propertyValue = itemPropertyDescriptor.getPropertyValue(object);
              if (propertyValue instanceof IItemPropertySource)
              {
                IItemPropertySource childPropertySource = (IItemPropertySource)propertyValue;
                visit(items, data, childPropertySource, propertyValue);
              }
            }
          }

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

    protected List<? extends IItemPropertyDescriptor> sort(List<IItemPropertyDescriptor> propertyDescriptors, Object object)
    {
      Map<String, Map<String, IItemPropertyDescriptor>> categorizedPropertyDescriptors = collator == null
        ? new LinkedHashMap<String, Map<String, IItemPropertyDescriptor>>() : new TreeMap<String, Map<String, IItemPropertyDescriptor>>(collator);
      for (IItemPropertyDescriptor itemPropertyDescriptor : propertyDescriptors)
      {
        String category = itemPropertyDescriptor.getCategory(object);
        if (category == null)
        {
          category = EMFEditUIPlugin.INSTANCE.getString("_UI_Misc_property_category");
        }
        Map<String, IItemPropertyDescriptor> sortedItemPropertyDescriptors = categorizedPropertyDescriptors.get(category);
        if (sortedItemPropertyDescriptors == null)
        {
          sortedItemPropertyDescriptors = collator == null
            ? new LinkedHashMap<String, IItemPropertyDescriptor>() : new TreeMap<String, IItemPropertyDescriptor>(Collator.getInstance());
          categorizedPropertyDescriptors.put(category, sortedItemPropertyDescriptors);
        }
        sortedItemPropertyDescriptors.put(itemPropertyDescriptor.getDisplayName(object), itemPropertyDescriptor);
      }

      List<IItemPropertyDescriptor> result = new ArrayList<IItemPropertyDescriptor>(propertyDescriptors.size());
      for (Map<String, IItemPropertyDescriptor> sortedItemPropertyDescriptors : categorizedPropertyDescriptors.values())
      {
        result.addAll(sortedItemPropertyDescriptors.values());
      }

      return result;
    }
  }

  /**
   * This enumerates the types of searches that are possible for an EMF-based structured editor.
   * <ul>
   *   <li>Labels and properties - Searches the labels in the structure viewer as well as the values of all properties in the properties view.</li>
   *   <li>Labels - Searches the only the labels in the structure viewer.</li>
   *   <li>Properties - Searches the only the properties in the properties view.</li>
   *   <li>Modifiable attributes - Searches the only the properties in the properties view that correspond to modified attributes of the model.</li>
   * </ul>
   */
  protected enum SearchType
  {
    LABEL_AND_PROPERTY()
    {
      @Override
      public boolean isIncluded(Data.Item item)
      {
        return item != null;
      }

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

      @Override
      public String label()
      {
        return EMFEditUIPlugin.INSTANCE.getString("_UI_LabelAndProperties_label");
      }
    },
    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 EMFEditUIPlugin.INSTANCE.getString("_UI_Labels_label");
      }
    },
    PROPERTY()
    {
      @Override
      public boolean isIncluded(Data.Item item)
      {
        return item != null && item.itemPropertyDescriptor != null;
      }

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

      @Override
      public String label()
      {
        return EMFEditUIPlugin.INSTANCE.getString("_UI_Properties_label");
      }
    },
    MODIFIABLE_ATTRIBUTE
    {
      @Override
      public boolean isIncluded(Data.Item item)
      {
        return item != null && item.itemPropertyDescriptor != null && item.getFeature() instanceof EAttribute && item.itemPropertyDescriptor.canSetProperty(item.data.object);
      }

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

      @Override
      public String label()
      {
        return EMFEditUIPlugin.INSTANCE.getString("_UI_ModifiableAttributes_label");
      }
    };

    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_PROPERTY;
    }

    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;
    }
  }
}
