com.google.dart.tools.internal.search.ui.SearchMatchPage.java Source code

Java tutorial

Introduction

Here is the source code for com.google.dart.tools.internal.search.ui.SearchMatchPage.java

Source

/*
 * Copyright (c) 2013, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.dart.tools.internal.search.ui;

import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.dart.engine.element.Element;
import com.google.dart.engine.element.ExecutableElement;
import com.google.dart.engine.search.MatchKind;
import com.google.dart.engine.search.SearchEngine;
import com.google.dart.engine.search.SearchMatch;
import com.google.dart.engine.source.Source;
import com.google.dart.engine.source.UriKind;
import com.google.dart.engine.utilities.source.SourceRange;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.internal.corext.refactoring.util.ExecutionUtils;
import com.google.dart.tools.internal.corext.refactoring.util.RunnableEx;
import com.google.dart.tools.ui.DartPluginImages;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.DartUI;
import com.google.dart.tools.ui.internal.text.editor.DartEditor;
import com.google.dart.tools.ui.internal.text.editor.EditorUtility;
import com.google.dart.tools.ui.internal.text.editor.NewDartElementLabelProvider;
import com.google.dart.tools.ui.internal.util.ExceptionHandler;
import com.google.dart.tools.ui.internal.util.SWTUtil;

import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.progress.UIJob;

import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Abstract {@link SearchPage} for displaying {@link SearchMatch}s.
 * 
 * @coverage dart.editor.ui.search
 */
public abstract class SearchMatchPage extends SearchPage {
    /**
     * Item for an element in search results tree.
     */
    private static class ElementItem {
        private final Element element;
        private final List<ElementItem> children = Lists.newArrayList();
        private final List<LineItem> lines = Lists.newArrayList();
        private ElementItem parent;
        private ElementItem prev;
        private ElementItem next;
        private int numMatches;

        public ElementItem(Element element) {
            this.element = element;
        }

        /**
         * Adds new {@link SearchMatch}, on the same or new {@link LineItem}.
         */
        public void addMatch(SourceLineProvider lineProvider, SearchMatch match) {
            ReferenceKind referenceKind = ReferenceKind.of(match.getKind());
            Source source = element.getSource();
            SourceLine sourceLine = lineProvider.getLine(source, match.getSourceRange().getOffset());
            // find target LineItem
            LineItem targetLineItem = null;
            for (LineItem lineItem : lines) {
                if (lineItem.line.equals(sourceLine)) {
                    targetLineItem = lineItem;
                    break;
                }
            }
            // new LineItem
            if (targetLineItem == null) {
                boolean potential = FILTER_POTENTIAL.apply(match);
                targetLineItem = new LineItem(this, potential, sourceLine);
                lines.add(targetLineItem);
            }
            // prepare position
            SourceRange matchRange = match.getSourceRange();
            Position position = new Position(matchRange.getOffset(), matchRange.getLength());
            // add new position
            targetLineItem.addPosition(position, referenceKind);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ElementItem)) {
                return false;
            }
            return Objects.equal(((ElementItem) obj).element, element);
        }

        @Override
        public int hashCode() {
            return element != null ? element.hashCode() : 0;
        }

        public void merge(ElementItem item) {
            numMatches += item.numMatches;
            List<LineItem> thisLines = Lists.newArrayList(lines);
            for (LineItem otherLine : item.lines) {
                // try to merge into existing line
                boolean merged = false;
                for (LineItem thisLine : thisLines) {
                    merged |= thisLine.merge(otherLine);
                }
                // add line
                if (!merged) {
                    lines.add(otherLine);
                    otherLine.item = this;
                }
            }
        }

        void addChild(ElementItem child) {
            if (child.parent == null) {
                child.parent = this;
                children.add(child);
            }
        }
    }

    /**
     * Helper for navigating {@link ElementItem} and {@link LineItem} hierarchy.
     */
    private static class ItemCursor {
        ElementItem item;
        int lineIndex;

        ItemCursor(ElementItem item) {
            this(item, -1);
        }

        ItemCursor(ElementItem item, int positionIndex) {
            this.item = item;
            this.lineIndex = positionIndex;
        }

        Position getPosition() {
            if (item == null) {
                return null;
            }
            if (lineIndex < 0 || lineIndex > item.lines.size() - 1) {
                return null;
            }
            LineItem lineItem = item.lines.get(lineIndex);
            LinePosition linePosition = lineItem.positions.get(0);
            return linePosition.position;
        }

        /**
         * Moves this {@link ItemCursor} to the next {@link LineItem} in the same or next
         * {@link ElementItem}.
         * 
         * @return {@code true} if was moved, or {@code false} if cursor is at the last line.
         */
        boolean next() {
            ElementItem _item = item;
            int _lineIndex = lineIndex;
            // try to go to next
            if (_next()) {
                return true;
            }
            // rollback
            item = _item;
            lineIndex = _lineIndex;
            return false;
        }

        /**
         * Moves this {@link ItemCursor} to the previous {@link LineItem} in the same or previous
         * {@link ElementItem}.
         * 
         * @return {@code true} if was moved, or {@code false} if cursor is at the first line.
         */
        boolean prev() {
            ElementItem _item = item;
            int _lineIndex = lineIndex;
            // try to go to previous
            if (_prev()) {
                return true;
            }
            // rollback
            item = _item;
            lineIndex = _lineIndex;
            return false;
        }

        private boolean _next() {
            if (item == null) {
                return false;
            }
            // in the same leaf
            if (lineIndex < item.lines.size() - 1) {
                lineIndex++;
                return true;
            }
            // next leaf
            while (true) {
                item = item.next;
                if (item == null) {
                    return false;
                }
                if (!item.lines.isEmpty()) {
                    lineIndex = 0;
                    break;
                }
            }
            return true;
        }

        private boolean _prev() {
            if (item == null) {
                return false;
            }
            // in the same leaf
            if (lineIndex > 0) {
                lineIndex--;
                return true;
            }
            // previous leaf
            while (true) {
                item = item.prev;
                if (item == null) {
                    return false;
                }
                if (!item.lines.isEmpty()) {
                    lineIndex = item.lines.size() - 1;
                    break;
                }
            }
            return true;
        }
    }

    /**
     * Item for a line with one or more matches.
     */
    private static class LineItem {
        private ElementItem item;
        private boolean potential;
        private final SourceLine line;
        private final List<LinePosition> positions = Lists.newArrayList();

        public LineItem(ElementItem item, boolean potential, SourceLine line) {
            this.item = item;
            this.potential = potential;
            this.line = line;
        }

        /**
         * Adds new the {@link LinePosition} with given parameters.
         */
        public void addPosition(Position position, ReferenceKind kind) {
            positions.add(new LinePosition(position, kind));
        }

        /**
         * Attempts to merge the given {@link LineItem} into this.
         * 
         * @return {@code true} if merge was done, {@code false} if not the same line.
         */
        public boolean merge(LineItem other) {
            if (!other.line.equals(line)) {
                return false;
            }
            potential |= other.potential;
            positions.addAll(other.positions);
            return true;
        }
    }

    /**
     * Value object with {@link Position} and its {@link ReferenceKind}.
     */
    private static class LinePosition {
        private final Position position;
        private final Position positionSrc;
        private final ReferenceKind kind;

        public LinePosition(Position position, ReferenceKind kind) {
            this.position = position;
            this.positionSrc = new Position(position.offset, position.length);
            this.kind = kind;
        }
    }

    private static class PreferenceBackgroundStyler extends StyledString.Styler {
        private final IPreferenceStore store;
        private final String key;

        public PreferenceBackgroundStyler(IPreferenceStore store, String key) {
            this.store = store;
            this.key = key;
        }

        @Override
        public void applyStyles(TextStyle textStyle) {
            RGB rgb = PreferenceConverter.getColor(store, key);
            if (rgb != null) {
                textStyle.background = DartUI.getColorManager().getColor(rgb);
            }
        }
    }

    /**
     * Coarse-grained kind of the reference. We don't need all details of {@link MatchKind}.
     */
    private static enum ReferenceKind {
        REFERENCE, READ, WRITE;
        public static ReferenceKind of(MatchKind kind) {
            if (kind == MatchKind.FIELD_READ || kind == MatchKind.VARIABLE_READ) {
                return READ;
            }
            if (kind == MatchKind.FIELD_WRITE || kind == MatchKind.VARIABLE_WRITE
                    || kind == MatchKind.VARIABLE_READ_WRITE) {
                return WRITE;
            }
            return REFERENCE;
        }
    }

    /**
     * {@link ITreeContentProvider} for {@link ElementItem}s.
     */
    private static class SearchContentProvider implements ITreeContentProvider {
        @Override
        public void dispose() {
        }

        @Override
        public Object[] getChildren(Object parentElement) {
            // prepare item
            if (!(parentElement instanceof ElementItem)) {
                return ArrayUtils.EMPTY_OBJECT_ARRAY;
            }
            ElementItem item = (ElementItem) parentElement;
            // sub-items
            List<ElementItem> children = item.children;
            if (!children.isEmpty()) {
                return children.toArray(new ElementItem[children.size()]);
            }
            // lines
            List<LineItem> lines = item.lines;
            return lines.toArray(new LineItem[lines.size()]);
        }

        @Override
        public Object[] getElements(Object inputElement) {
            return getChildren(inputElement);
        }

        @Override
        public Object getParent(Object element) {
            if (element instanceof ElementItem) {
                return ((ElementItem) element).parent;
            }
            if (element instanceof LineItem) {
                return ((LineItem) element).item;
            }
            return null;
        }

        @Override
        public boolean hasChildren(Object element) {
            if (element instanceof ElementItem) {
                ElementItem item = (ElementItem) element;
                return !item.children.isEmpty() || !item.lines.isEmpty();
            }
            return false;
        }

        @Override
        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        }
    }

    /**
     * {@link ILabelProvider} for {@link ElementItem}s.
     */
    private static class SearchLabelProvider extends NewDartElementLabelProvider {
        @Override
        public Image getImage(Object elem) {
            // element
            if (elem instanceof ElementItem) {
                ElementItem item = (ElementItem) elem;
                return super.getImage(item.element);
            }
            // line
            if (elem instanceof LineItem) {
                LineItem item = (LineItem) elem;
                // has any write?
                for (LinePosition position : item.positions) {
                    if (position.kind == ReferenceKind.WRITE) {
                        return DartPluginImages.get(DartPluginImages.IMG_OBJS_SEARCH_WRITEACCESS);
                    }
                }
                // has any read?
                for (LinePosition position : item.positions) {
                    if (position.kind == ReferenceKind.READ) {
                        return DartPluginImages.get(DartPluginImages.IMG_OBJS_SEARCH_READACCESS);
                    }
                }
                // just some reference
                return DartPluginImages.get(DartPluginImages.IMG_OBJS_SEARCH_OCCURRENCE);
            }
            // unknown
            return null;
        }

        @Override
        public StyledString getStyledText(Object elem) {
            if (elem instanceof ElementItem) {
                ElementItem item = (ElementItem) elem;
                StyledString styledText = super.getStyledText(item.element);
                if (item.numMatches == 1) {
                    styledText.append(" (1 match)", StyledString.COUNTER_STYLER);
                } else if (item.numMatches > 1) {
                    styledText.append(" (" + item.numMatches + " matches)", StyledString.COUNTER_STYLER);
                }
                return styledText;
            } else {
                LineItem item = (LineItem) elem;
                StyledString styledText = new StyledString(item.line.content);
                for (LinePosition linePosition : item.positions) {
                    StyledString.Styler style = linePosition.kind == ReferenceKind.WRITE ? HIGHLIGHT_WRITE_STYLE
                            : HIGHLIGHT_STYLE;
                    int styleOffset = linePosition.positionSrc.offset - item.line.start;
                    int styleLength = linePosition.positionSrc.length;
                    try {
                        styledText.setStyle(styleOffset, styleLength, style);
                    } catch (Throwable e) {
                        // https://code.google.com/p/dart/issues/detail?id=18576
                        // Cannot reproduce, just ignore.
                    }
                }
                // may be potential match
                if (item.potential) {
                    styledText.append(" ");
                    styledText.append(" (potential match)", StyledString.DECORATIONS_STYLER);
                }
                // done
                return styledText;
            }
        }
    }

    /**
     * Information about a single line in some {@link Source}.
     */
    private static class SourceLine {
        final Source source;
        final int start;
        final String content;

        public SourceLine(Source source, int start, String content) {
            this.source = source;
            this.start = start;
            this.content = content;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof SourceLine)) {
                return false;
            }
            SourceLine other = (SourceLine) obj;
            return other.source == source && other.start == start;
        }
    }

    /**
     * Helper for transforming offsets in the some {@link Source} into {@link SourceLine} objects.
     */
    private static class SourceLineProvider {
        private final Map<Source, String> sourceContentMap = Maps.newHashMap();

        /**
         * @return the {@link SourceLine} for the given {@link Source} and offset; may be {@code null}.
         */
        public SourceLine getLine(Source source, int offset) {
            String content = getContent(source);
            // find start of line
            int start = offset;
            while (start > 0 && content.charAt(start - 1) != '\n') {
                start--;
            }
            // find end of line
            int end = offset;
            while (end < content.length() && content.charAt(end) != '\r' && content.charAt(end) != '\n') {
                end++;
            }
            // done
            String text = content.substring(start, end);
            return new SourceLine(source, start, text);
        }

        private String getContent(Source source) {
            String content = sourceContentMap.get(source);
            if (content == null) {
                try {
                    IResource resource = DartCore.getProjectManager().getResource(source);
                    if (resource instanceof IFile) {
                        // IFile in workspace, can be open and modified - get contents using FileBuffers.
                        IFile file = (IFile) resource;
                        ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
                        IPath path = file.getFullPath();
                        manager.connect(path, LocationKind.IFILE, null);
                        try {
                            ITextFileBuffer buffer = manager.getTextFileBuffer(path, LocationKind.IFILE);
                            IDocument document = buffer.getDocument();
                            content = document.get();
                            sourceContentMap.put(source, content);
                        } finally {
                            manager.disconnect(path, LocationKind.IFILE, null);
                        }
                    } else {
                        // It must be an external file, cannot be modified - request its contents directly.
                        content = source.getContents().getData().toString();
                        sourceContentMap.put(source, content);
                    }
                } catch (Throwable e) {
                    return null;
                }
            }
            return content;
        }
    }

    @SuppressWarnings("restriction")
    private static final StyledString.Styler HIGHLIGHT_WRITE_STYLE = new PreferenceBackgroundStyler(
            org.eclipse.ui.internal.editors.text.EditorsPlugin.getDefault().getPreferenceStore(),
            "writeOccurrenceIndicationColor");

    @SuppressWarnings("restriction")
    private static final StyledString.Styler HIGHLIGHT_STYLE = new PreferenceBackgroundStyler(
            org.eclipse.ui.internal.editors.text.EditorsPlugin.getDefault().getPreferenceStore(),
            "searchResultIndicationColor");

    private static final String SETTINGS_ID = "SearchMatchPage";
    private static final String FILTER_SDK_ID = "filter_SDK";
    private static final String FILTER_POTENTIAL_ID = "filter_potential";
    private static final String FILTER_PROJECT_ID = "filter_project";

    private static final ITreeContentProvider CONTENT_PROVIDER = new SearchContentProvider();

    /**
     * Adds new {@link ElementItem} to the tree.
     */
    private static ElementItem addElementItem(Map<Element, ElementItem> itemMap, ElementItem child) {
        // put child
        Element childElement = child.element;
        {
            ElementItem existingChild = itemMap.get(childElement);
            if (existingChild == null) {
                itemMap.put(childElement, child);
            } else {
                existingChild.merge(child);
                child = existingChild;
            }
        }
        // bind child to parent
        if (childElement != null) {
            Element parentElement = childElement.getEnclosingElement();
            ElementItem parent = new ElementItem(parentElement);
            parent = addElementItem(itemMap, parent);
            parent.addChild(child);
        }
        // done
        return child;
    }

    /**
     * Builds {@link ElementItem} tree out of the given {@link SearchMatch}s.
     */
    private static ElementItem buildElementItemTree(List<SearchMatch> matches) {
        SourceLineProvider sourceLineProvider = new SourceLineProvider();
        ElementItem rootItem = new ElementItem(null);
        Map<Element, ElementItem> itemMap = Maps.newHashMap();
        itemMap.put(null, rootItem);
        for (SearchMatch match : matches) {
            Element element = getExecutableElement(match);
            ElementItem elementItem = new ElementItem(element);
            elementItem.addMatch(sourceLineProvider, match);
            addElementItem(itemMap, elementItem);
        }
        calculateNumMatches(rootItem);
        sortLines(rootItem);
        linkLeaves(rootItem, null);
        return rootItem;
    }

    /**
     * Recursively calculates {@link ElementItem#numMatches} fields.
     */
    private static int calculateNumMatches(ElementItem item) {
        int result = 0;
        for (LineItem lineItem : item.lines) {
            result += lineItem.positions.size();
        }
        for (ElementItem child : item.children) {
            result += calculateNumMatches(child);
        }
        item.numMatches = result;
        return result;
    }

    private static IDialogSettings getDialogSettings() {
        return DartToolsPlugin.getDefault().getDialogSettingsSection(SETTINGS_ID);
    }

    /**
     * @return the {@link Element} to use as enclosing in {@link ElementItem} tree.
     */
    private static Element getExecutableElement(SearchMatch match) {
        Element element = match.getElement();
        while (element != null) {
            Element executable = element.getAncestor(ExecutableElement.class);
            if (executable == null) {
                break;
            }
            element = executable;
        }
        return element;
    }

    /**
     * Recursively visits {@link ElementItem} and links leaves.
     * 
     * @return the last {@link ElementItem} leaf in the sub-tree.
     */
    private static ElementItem linkLeaves(ElementItem item, ElementItem prev) {
        // leaf
        if (item.children.isEmpty()) {
            if (prev != null) {
                prev.next = item;
            }
            item.prev = prev;
            prev = item;
            return item;
        }
        // container
        ElementItem lastLeaf = prev;
        item.next = item.children.get(0);
        for (ElementItem child : item.children) {
            lastLeaf = linkLeaves(child, lastLeaf);
        }
        return lastLeaf;
    }

    /**
     * Opens the {@link Position} in the {@link Element}s editor.
     */
    private static void openPositionInElement(Element element, Position position) {
        try {
            IEditorPart editor = DartUI.openInEditor(element);
            revealInEditor(editor, position);
        } catch (Throwable e) {
            ExceptionHandler.handle(e, "Search", "Exception during open.");
        }
    }

    /**
     * Reveals the given {@link Position} in the {@link IEditorPart}.
     */
    private static void revealInEditor(IEditorPart editor, Position position) {
        SourceRange sourceRange = new SourceRange(position.offset, position.length);
        EditorUtility.revealInEditor(editor, sourceRange);
    }

    /**
     * Recursively sorts {@link ElementItem}s and {@link LineItem}s.
     */
    private static void sortLines(ElementItem item) {
        // process lines
        Collections.sort(item.lines, new Comparator<LineItem>() {
            @Override
            public int compare(LineItem o1, LineItem o2) {
                return o1.line.start - o2.line.start;
            }
        });
        for (LineItem lineItem : item.lines) {
            Collections.sort(lineItem.positions, new Comparator<LinePosition>() {
                @Override
                public int compare(LinePosition o1, LinePosition o2) {
                    return o1.position.offset - o2.position.offset;
                }
            });
        }
        // process children
        Collections.sort(item.children, new Comparator<ElementItem>() {
            @Override
            public int compare(ElementItem o1, ElementItem o2) {
                return o1.element.getNameOffset() - o2.element.getNameOffset();
            }
        });
        for (ElementItem child : item.children) {
            sortLines(child);
        }
    }

    private IAction removeAction = new Action() {
        {
            setToolTipText("Remove Selected Matches");
            DartPluginImages.setLocalImageDescriptors(this, "search_rem.gif");
        }

        @Override
        public void run() {
            IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
            for (Iterator<?> iter = selection.toList().iterator(); iter.hasNext();) {
                Object obj = iter.next();
                // LineItem
                if (obj instanceof LineItem) {
                    LineItem lineItem = (LineItem) obj;
                    ElementItem parentItem = lineItem.item;
                    List<LineItem> parentLines = parentItem.lines;
                    // remove this line
                    parentLines.remove(lineItem);
                    // it no more lines, remove parent item too
                    if (parentLines.isEmpty()) {
                        obj = parentItem;
                    }
                }
                // ResultItem
                if (obj instanceof ElementItem) {
                    ElementItem item = (ElementItem) obj;
                    while (item != null && item.element != null) {
                        ElementItem parent = item.parent;
                        parent.children.remove(item);
                        if (!parent.children.isEmpty()) {
                            break;
                        }
                        item = parent;
                    }
                }
            }
            calculateNumMatches(rootItem);
            // update viewer
            viewer.refresh();
            // update markers
            addMarkers();
        }
    };

    private IAction removeAllAction = new Action() {
        {
            setToolTipText("Remove All Matches");
            DartPluginImages.setLocalImageDescriptors(this, "search_remall.gif");
        }

        @Override
        public void run() {
            close();
        }
    };

    private IAction refreshAction = new Action() {
        {
            setToolTipText("Refresh the Current Search");
            DartPluginImages.setLocalImageDescriptors(this, "refresh.gif");
        }

        @Override
        public void run() {
            refresh();
        }
    };

    private IAction expandAllAction = new Action() {
        {
            setToolTipText("Expand All");
            DartPluginImages.setLocalImageDescriptors(this, "expandall.gif");
        }

        @Override
        public void run() {
            viewer.expandAll();
        }
    };

    private IAction collapseAllAction = new Action() {
        {
            setToolTipText("Collapse All");
            DartPluginImages.setLocalImageDescriptors(this, "collapseall.gif");
        }

        @Override
        public void run() {
            viewer.collapseAll();
        }
    };

    private IAction nextAction = new Action() {
        {
            setToolTipText("Show Next Match");
            DartPluginImages.setLocalImageDescriptors(this, "search_next.gif");
        }

        @Override
        public void run() {
            openItemNext();
        }
    };

    private IAction prevAction = new Action() {
        {
            setToolTipText("Show Previous Match");
            DartPluginImages.setLocalImageDescriptors(this, "search_prev.gif");
        }

        @Override
        public void run() {
            openItemPrev();
        }
    };

    private IAction filterSdkAction = new Action(null, IAction.AS_CHECK_BOX) {
        {
            setToolTipText("Hide SDK and package matches");
            DartPluginImages.setLocalImageDescriptors(this, "search_filter_sdk.png");
        }

        @Override
        public void run() {
            filterEnabledSdk = isChecked();
            getDialogSettings().put(FILTER_SDK_ID, filterEnabledSdk);
            refresh();
        }
    };

    private IAction filterPotentialAction = new Action(null, IAction.AS_CHECK_BOX) {
        {
            setToolTipText("Hide potential matches");
            DartPluginImages.setLocalImageDescriptors(this, "search_filter_potential.png");
        }

        @Override
        public void run() {
            filterEnabledPotential = isChecked();
            getDialogSettings().put(FILTER_POTENTIAL_ID, filterEnabledPotential);
            refresh();
        }
    };

    private IAction filterProjectAction = new Action(null, IAction.AS_CHECK_BOX) {
        {
            setToolTipText("Show only current project actions");
            DartPluginImages.setLocalImageDescriptors(this, "search_filter_project.gif");
        }

        @Override
        public void run() {
            filterEnabledProject = isChecked();
            getDialogSettings().put(FILTER_PROJECT_ID, filterEnabledProject);
            refresh();
        }
    };

    private final SearchView searchView;
    private final String taskName;

    private final Set<IResource> markerResources = Sets.newHashSet();

    private TreeViewer viewer;
    private IPreferenceStore preferences;

    private IPropertyChangeListener propertyChangeListener = new IPropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent event) {
            updateColors();
        }
    };
    private ElementItem rootItem;
    private ItemCursor itemCursor;

    private PositionTracker positionTracker;
    private boolean filterEnabledSdk = false;
    private boolean filterEnabledPotential = false;
    private boolean filterEnabledProject = false;
    private int filteredCountSdk = 0;
    private int filteredCountPotential = 0;
    private int filteredCountProject = 0;

    private static final Predicate<SearchMatch> FILTER_SDK = new Predicate<SearchMatch>() {
        @Override
        public boolean apply(SearchMatch input) {
            Source source = input.getElement().getSource();
            UriKind uriKind = source.getUriKind();
            return uriKind == UriKind.DART_URI || uriKind == UriKind.PACKAGE_URI;
        }
    };

    private static final Predicate<SearchMatch> FILTER_POTENTIAL = new Predicate<SearchMatch>() {
        @Override
        public boolean apply(SearchMatch input) {
            return input.getKind() == MatchKind.NAME_REFERENCE_RESOLVED
                    || input.getKind() == MatchKind.NAME_REFERENCE_UNRESOLVED;
        }
    };

    private final Predicate<SearchMatch> FILTER_PROJECT = new Predicate<SearchMatch>() {
        @Override
        public boolean apply(SearchMatch input) {
            IProject currentProject = getCurrentProject();
            IFile resource = DartUI.getElementFile(input.getElement());
            return resource != null && currentProject != null && currentProject.equals(resource.getProject());
        }
    };

    private long lastQueryStartTime = 0;
    private long lastQueryFinishTime = 0;

    public SearchMatchPage(SearchView searchView, String taskName) {
        this.searchView = searchView;
        this.taskName = taskName;
    }

    @Override
    public void createControl(Composite parent) {
        initFilters();
        viewer = new TreeViewer(parent, SWT.FULL_SELECTION);
        viewer.setContentProvider(CONTENT_PROVIDER);
        // NB(scheglov): don't attempt to share label provider - it is not allowed in JFace
        viewer.setLabelProvider(new DelegatingStyledCellLabelProvider(new SearchLabelProvider()));
        viewer.addDoubleClickListener(new IDoubleClickListener() {
            @Override
            public void doubleClick(DoubleClickEvent event) {
                ISelection selection = event.getSelection();
                openSelectedElement(selection);
            }
        });
        // update colors
        preferences = DartToolsPlugin.getDefault().getCombinedPreferenceStore();
        preferences.addPropertyChangeListener(propertyChangeListener);
        updateColors();
        SWTUtil.bindJFaceResourcesFontToControl(viewer.getControl());
    }

    @Override
    public void dispose() {
        preferences.removePropertyChangeListener(propertyChangeListener);
        removeMarkers();
        disposePositionTracker();
        super.dispose();
    }

    @Override
    public Control getControl() {
        return viewer.getControl();
    }

    @Override
    public long getLastQueryFinishTime() {
        return lastQueryFinishTime;
    }

    @Override
    public long getLastQueryStartTime() {
        return lastQueryStartTime;
    }

    @Override
    public void makeContributions(IMenuManager menuManager, IToolBarManager toolBarManager,
            IStatusLineManager statusLineManager) {
        toolBarManager.add(filterProjectAction);
        toolBarManager.add(filterSdkAction);
        toolBarManager.add(filterPotentialAction);
        toolBarManager.add(new Separator());
        toolBarManager.add(nextAction);
        toolBarManager.add(prevAction);
        toolBarManager.add(new Separator());
        toolBarManager.add(removeAction);
        toolBarManager.add(removeAllAction);
        toolBarManager.add(new Separator());
        toolBarManager.add(expandAllAction);
        toolBarManager.add(collapseAllAction);
        toolBarManager.add(new Separator());
        toolBarManager.add(refreshAction);
    }

    @Override
    public void setFocus() {
        viewer.getControl().setFocus();
    }

    @Override
    public void show() {
        refresh();
    }

    /**
     * This is the first method called before performing refresh.
     */
    protected void beforeRefresh() {
    }

    /**
     * @return {@code true} if potential filter can be used.
     */
    protected boolean canUseFilterPotential() {
        return true;
    }

    /**
     * Clients may implement this method to allow "Only current project" filter.
     */
    protected IProject getCurrentProject() {
        return null;
    }

    /**
     * @return the description of the element we searched for.
     */
    protected abstract String getQueryElementName();

    /**
     * @return the description of the query we executed.
     */
    protected abstract String getQueryKindName();

    /**
     * Runs a {@link SearchEngine} request.
     * 
     * @return the {@link SearchMatch}s to display.
     */
    protected abstract List<SearchMatch> runQuery();

    /**
     * Closes this page and removes all search markers
     */
    void close() {
        searchView.showPage(null);
    }

    /**
     * Adds markers for all {@link ElementItem}s starting from {@link #rootItem}.
     */
    private void addMarkers() {
        try {
            ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
                @Override
                public void run(IProgressMonitor monitor) throws CoreException {
                    removeMarkers();
                    markerResources.clear();
                    addMarkers(rootItem);
                }
            }, null);
        } catch (Throwable e) {
            DartToolsPlugin.log(e);
        }
    }

    /**
     * Adds markers for the given {@link ElementItem} and its children.
     */
    private void addMarkers(ElementItem item) throws CoreException {
        // add marker if leaf
        if (!item.lines.isEmpty()) {
            IFile resource = DartUI.getElementFile(item.element);
            if (resource != null && resource.exists()) {
                markerResources.add(resource);
                try {
                    for (LineItem lineItem : item.lines) {
                        List<LinePosition> positions = lineItem.positions;
                        for (LinePosition linePosition : positions) {
                            Position position = linePosition.position;
                            IMarker marker = resource.createMarker(SearchView.SEARCH_MARKER);
                            marker.setAttribute(IMarker.CHAR_START, position.getOffset());
                            marker.setAttribute(IMarker.CHAR_END, position.getOffset() + position.getLength());
                        }
                    }
                } catch (Throwable e) {
                }
            }
        }
        // process children
        for (ElementItem child : item.children) {
            addMarkers(child);
        }
    }

    private List<SearchMatch> applyFilters(List<SearchMatch> matches) {
        filteredCountSdk = 0;
        filteredCountPotential = 0;
        filteredCountProject = 0;
        IProject currentProject = getCurrentProject();
        List<SearchMatch> filtered = Lists.newArrayList();
        for (SearchMatch match : matches) {
            // SDK filter
            if (FILTER_SDK.apply(match)) {
                filteredCountSdk++;
                if (filterEnabledSdk) {
                    continue;
                }
            }
            // potential filter
            if (canUseFilterPotential()) {
                if (FILTER_POTENTIAL.apply(match)) {
                    filteredCountPotential++;
                    if (filterEnabledPotential) {
                        continue;
                    }
                }
            }
            // project filter
            if (currentProject != null) {
                if (FILTER_PROJECT.apply(match)) {
                    filteredCountProject++;
                } else if (filterEnabledProject) {
                    continue;
                }
            }
            // OK
            filtered.add(match);
        }
        return filtered;
    }

    /**
     * Disposes {@link #positionTracker}.
     */
    private void disposePositionTracker() {
        if (positionTracker == null) {
            return;
        }
        positionTracker.dispose();
        positionTracker = null;
    }

    /**
     * Worker method for {@link #expandTreeItemsTimeBoxed(List, long)}.
     */
    private long expandTreeItemsTimeBoxed(List<ElementItem> items, int childrenLimit, long nanoBudget) {
        for (ElementItem item : items) {
            if (nanoBudget < 0) {
                return -1;
            }
            if (item.children.size() <= childrenLimit) {
                // expand single item
                {
                    long startNano = System.nanoTime();
                    viewer.setExpandedState(item, true);
                    nanoBudget -= System.nanoTime() - startNano;
                }
                // expand children
                nanoBudget = expandTreeItemsTimeBoxed(item.children, childrenLimit, nanoBudget);
                if (nanoBudget < 0) {
                    return -1;
                }
            }
        }
        return nanoBudget;
    }

    /**
     * Analyzes each {@link ElementItem} and expends it in {@link #viewer} only if it has not too much
     * children. So, user will see enough information, but not too much.
     */
    private void expandTreeItemsTimeBoxed(List<ElementItem> items, long nanoBudget) {
        int numIterations = 10;
        int childrenLimit = 10;
        for (int i = 0; i < numIterations; i++) {
            if (nanoBudget < 0) {
                break;
            }
            nanoBudget = expandTreeItemsTimeBoxed(items, childrenLimit, nanoBudget);
            childrenLimit *= 2;
        }
    }

    /**
     * Initializes filters from {@link IDialogSettings}.
     */
    private void initFilters() {
        IDialogSettings settings = getDialogSettings();
        if (settings.getBoolean(FILTER_SDK_ID)) {
            filterEnabledSdk = true;
            filterSdkAction.setChecked(true);
        }
        if (settings.getBoolean(FILTER_POTENTIAL_ID)) {
            filterEnabledPotential = true;
            filterPotentialAction.setChecked(true);
        }
        if (settings.getBoolean(FILTER_PROJECT_ID)) {
            filterEnabledProject = true;
            filterProjectAction.setChecked(true);
        }
        if (!canUseFilterPotential()) {
            filterPotentialAction.setEnabled(false);
            filterPotentialAction.setChecked(false);
        }
        if (getCurrentProject() == null) {
            filterProjectAction.setEnabled(false);
            filterProjectAction.setChecked(false);
        }
    }

    /**
     * Opens {@link DartEditor} with the next {@link Position} in the same of the next
     * {@link ElementItem}.
     */
    private void openItemNext() {
        boolean changed = itemCursor.next();
        if (changed) {
            showCursor();
        }
    }

    /**
     * Opens {@link DartEditor} with the previous {@link Position} in the same of the previous
     * {@link ElementItem}.
     */
    private void openItemPrev() {
        boolean changed = itemCursor.prev();
        if (changed) {
            showCursor();
        }
    }

    /**
     * Opens selected {@link ElementItem} in the editor.
     */
    private void openSelectedElement(ISelection selection) {
        // need IStructuredSelection
        if (!(selection instanceof IStructuredSelection)) {
            return;
        }
        IStructuredSelection structuredSelection = (IStructuredSelection) selection;
        // only single element
        if (structuredSelection.size() != 1) {
            return;
        }
        Object firstElement = structuredSelection.getFirstElement();
        // line item
        if (firstElement instanceof LineItem) {
            LineItem lineItem = (LineItem) firstElement;
            Position position = lineItem.positions.get(0).position;
            openPositionInElement(lineItem.item.element, position);
        }
        // element item
        if (firstElement instanceof ElementItem) {
            ElementItem item = (ElementItem) firstElement;
            // use ResultCursor to find first occurrence in the requested subtree
            itemCursor = new ItemCursor(item);
            boolean found = itemCursor.next();
            if (!found) {
                return;
            }
            Element element = itemCursor.item.element;
            Position position = itemCursor.getPosition();
            // open position
            openPositionInElement(element, position);
        }
    }

    /**
     * Runs background {@link Job} to fetch {@link SearchMatch}s and then displays them in the
     * {@link #viewer}.
     */
    private void refresh() {
        try {
            lastQueryStartTime = System.currentTimeMillis();
            new Job(taskName) {
                @Override
                protected IStatus run(IProgressMonitor monitor) {
                    beforeRefresh();
                    // do query
                    List<SearchMatch> matches = runQuery();
                    int totalCount = matches.size();
                    matches = applyFilters(matches);
                    // set description
                    String filtersDesc;
                    {
                        filtersDesc = "";
                        filtersDesc += ",   SDK: " + filteredCountSdk;
                        if (filterEnabledSdk) {
                            filtersDesc += " (filtered)";
                        }
                        if (canUseFilterPotential()) {
                            filtersDesc += ",   potential: " + filteredCountPotential;
                            if (filterEnabledPotential) {
                                filtersDesc += " (filtered)";
                            }
                        }
                        if (getCurrentProject() != null) {
                            filtersDesc += ",   in project: " + filteredCountProject;
                            if (filterEnabledProject) {
                                filtersDesc += " (only)";
                            }
                        }
                    }
                    setContentDescription("'" + getQueryElementName() + "' - " + matches.size() + " "
                            + getQueryKindName() + ",   total: " + totalCount + filtersDesc);
                    // process query results
                    rootItem = buildElementItemTree(matches);
                    itemCursor = new ItemCursor(rootItem);
                    trackPositions();
                    // add markers
                    addMarkers();
                    // schedule UI update
                    new UIJob("Displaying search results...") {
                        @Override
                        public IStatus runInUIThread(IProgressMonitor monitor) {
                            // may be already disposed (e.g. new search was done)
                            if (viewer.getControl().isDisposed()) {
                                return Status.OK_STATUS;
                            }
                            // set new input
                            Object[] expandedElements = viewer.getExpandedElements();
                            viewer.setInput(rootItem);
                            viewer.setExpandedElements(expandedElements);
                            // expand
                            expandTreeItemsTimeBoxed(rootItem.children, 75L * 1000000L);
                            lastQueryFinishTime = System.currentTimeMillis();
                            return Status.OK_STATUS;
                        }
                    }.schedule();
                    // done
                    return Status.OK_STATUS;
                }
            }.schedule();
        } catch (Throwable e) {
            ExceptionHandler.handle(e, "Search", "Exception during search.");
        }
    }

    /**
     * Removes all search markers from {@link #markerResources}.
     */
    private void removeMarkers() {
        try {
            ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
                @Override
                public void run(IProgressMonitor monitor) throws CoreException {
                    for (IResource resource : markerResources) {
                        if (resource.exists()) {
                            try {
                                resource.deleteMarkers(SearchView.SEARCH_MARKER, false, IResource.DEPTH_ZERO);
                            } catch (Throwable e) {
                            }
                        }
                    }
                }
            }, null);
        } catch (Throwable e) {
            DartToolsPlugin.log(e);
        }
    }

    /**
     * Shows given text as description for {@link SearchView}.
     */
    private void setContentDescription(final String description) {
        ExecutionUtils.runRethrowUI(new RunnableEx() {
            @Override
            public void run() throws Exception {
                searchView.setContentDescription(description);
            }
        });
    }

    /**
     * Shows current {@link #itemCursor} state.
     */
    private void showCursor() {
        try {
            ElementItem elementItem = itemCursor.item;
            LineItem lineItem = elementItem.lines.get(itemCursor.lineIndex);
            viewer.setSelection(new StructuredSelection(lineItem), true);
            // open editor with Element
            Element element = elementItem.element;
            IEditorPart editor = DartUI.openInEditor(element, false, true);
            // show Position
            Position position = itemCursor.getPosition();
            if (position != null) {
                revealInEditor(editor, position);
            }
        } catch (Throwable e) {
            ExceptionHandler.handle(e, "Search", "Exception during open.");
        }
    }

    /**
     * Starts tracking all search result positions in {@link #positionTracker}.
     */
    private void trackPositions() {
        disposePositionTracker();
        positionTracker = new PositionTracker();
        trackPositions(rootItem);
    }

    /**
     * Recursively visits {@link ElementItem} and tracks all {@link Position}s.
     */
    private void trackPositions(ElementItem item) {
        // do track positions
        if (item.element != null) {
            IFile file = DartUI.getElementFile(item.element);
            if (file != null) {
                for (LineItem lineItem : item.lines) {
                    List<LinePosition> positions = lineItem.positions;
                    for (LinePosition linePosition : positions) {
                        Position position = linePosition.position;
                        positionTracker.trackPosition(file, position);
                    }
                }
            }
        }
        // process children
        for (ElementItem child : item.children) {
            trackPositions(child);
        }
    }

    private void updateColors() {
        if (viewer.getTree().isDisposed()) {
            return;
        }
        SWTUtil.runUI(new Runnable() {
            @Override
            public void run() {
                SWTUtil.setColors(viewer.getTree(), preferences);
                viewer.refresh();
            }
        });
    }
}