blob: 59dba24aca12a163fb141af2bb3676558b515efa [file] [log] [blame]
/*
* Copyright (c) 2014, 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.Charsets;
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.common.io.Files;
import com.google.dart.engine.search.SearchMatch;
import com.google.dart.engine.utilities.source.SourceRange;
import com.google.dart.server.generated.types.Element;
import com.google.dart.server.generated.types.ElementKind;
import com.google.dart.server.generated.types.Location;
import com.google.dart.server.generated.types.SearchResult;
import com.google.dart.server.generated.types.SearchResultKind;
import com.google.dart.tools.core.internal.util.ResourceUtil;
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.ElementLabelProvider_NEW;
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.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
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.LabelProvider;
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.io.File;
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 SearchResult}s.
*
* @coverage dart.editor.ui.search
*/
public abstract class SearchResultPage_NEW 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 SearchResult}, on the same or new {@link LineItem}.
*/
public void addMatch(FileLineProvider lineProvider, SearchResult match) {
ReferenceKind referenceKind = ReferenceKind.of(match.getKind());
Location location = match.getLocation();
String filePath = location.getFile();
FileLine sourceLine = lineProvider.getLine(filePath, location.getOffset());
if (sourceLine == null) {
return;
}
// 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, filePath, potential, sourceLine);
lines.add(targetLineItem);
}
// prepare position
Position position = new Position(location.getOffset(), location.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;
}
void addChild(ElementItem child) {
if (child.parent == null) {
child.parent = this;
children.add(child);
}
}
}
/**
* Information about a single line in some file.
*/
private static class FileLine {
final String file;
final int start;
final String content;
public FileLine(String file, int start, String content) {
this.file = file;
this.start = start;
this.content = content;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof FileLine)) {
return false;
}
FileLine other = (FileLine) obj;
return other.file.equals(file) && other.start == start;
}
}
/**
* Helper for transforming offsets in some file into {@link FileLine} objects.
*/
private static class FileLineProvider {
private final Map<String, String> fileContentMap = Maps.newHashMap();
/**
* @return the {@link FileLine} for the given file and offset; may be {@code null}.
*/
public FileLine getLine(String filePath, int offset) {
String content = getContent(filePath);
if (content == null) {
return null;
}
if (offset < 0 || offset >= content.length()) {
return null;
}
// 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 FileLine(filePath, start, text);
}
private String getContent(String filePath) {
String content = fileContentMap.get(filePath);
if (content == null) {
try {
File javaFile = new File(filePath);
IFile resource = ResourceUtil.getFile(javaFile);
if (resource instanceof IFile) {
// IFile in workspace, can be open and modified - get contents using FileBuffers.
IFile file = 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();
fileContentMap.put(filePath, content);
} finally {
manager.disconnect(path, LocationKind.IFILE, null);
}
} else {
// It must be an external file, cannot be modified - request its contents directly.
content = Files.toString(javaFile, Charsets.UTF_8);
fileContentMap.put(filePath, content);
}
} catch (Throwable e) {
return null;
}
}
return content;
}
}
/**
* 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;
}
LineItem getLine() {
if (item == null) {
return null;
}
if (lineIndex < 0 || lineIndex > item.lines.size() - 1) {
return null;
}
return item.lines.get(lineIndex);
}
Position getPosition() {
LineItem lineItem = getLine();
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 final String filePath;
private boolean potential;
private final FileLine line;
private final List<LinePosition> positions = Lists.newArrayList();
public LineItem(ElementItem item, String filePath, boolean potential, FileLine line) {
this.item = item;
this.filePath = filePath;
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));
}
}
/**
* 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 SearchResultKind}.
*/
private static enum ReferenceKind {
REFERENCE,
READ,
WRITE;
public static ReferenceKind of(String searchResultKind) {
if (searchResultKind.equals(SearchResultKind.READ)) {
return READ;
}
if (searchResultKind.equals(SearchResultKind.DECLARATION)
|| searchResultKind.equals(SearchResultKind.READ_WRITE)
|| searchResultKind.equals(SearchResultKind.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;
// lines and sub-items
List<Object> children = Lists.newArrayList();
children.addAll(item.lines);
children.addAll(item.children);
return children.toArray(new Object[children.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 LabelProvider implements IStyledLabelProvider {
private final ElementLabelProvider_NEW elementLabelProvider = new ElementLabelProvider_NEW();
@Override
public Image getImage(Object elem) {
// element
if (elem instanceof ElementItem) {
ElementItem item = (ElementItem) elem;
return elementLabelProvider.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 = new StyledString(item.element.getName());
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;
// Prevent exceptions if search results are out of sync with the source.
if (styleOffset >= 0 && styleLength < styledText.length()) {
styledText.setStyle(styleOffset, styleLength, style);
}
}
// may be potential match
if (item.potential) {
styledText.append(" ");
styledText.append(" (potential match)", StyledString.DECORATIONS_STYLER);
}
// done
return styledText;
}
}
}
@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, Element[] elements,
int elementIndex) {
// prepare child Element
Element childElement = null;
while (elementIndex < elements.length) {
Element element = elements[elementIndex];
if (isLocalElement(element)) {
elementIndex++;
continue;
}
childElement = element;
break;
}
// put child
ElementItem child;
{
ElementItem existingChild = itemMap.get(childElement);
if (existingChild == null) {
child = new ElementItem(childElement);
itemMap.put(childElement, child);
} else {
child = existingChild;
}
}
// bind child to parent
if (childElement != null) {
elementIndex++;
ElementItem parent = addElementItem(itemMap, elements, elementIndex);
parent.addChild(child);
}
// done
return child;
}
/**
* Builds {@link ElementItem} tree out of the given {@link SearchResult}s.
*/
private static ElementItem buildElementItemTree(List<SearchResult> searchResults) {
FileLineProvider sourceLineProvider = new FileLineProvider();
ElementItem rootItem = new ElementItem(null);
Map<Element, ElementItem> itemMap = Maps.newHashMap();
itemMap.put(null, rootItem);
for (SearchResult searchResult : searchResults) {
List<Element> elements = searchResult.getPath();
Element[] elementArray = elements.toArray(new Element[elements.size()]);
ElementItem elementItem = addElementItem(itemMap, elementArray, 0);
elementItem.addMatch(sourceLineProvider, searchResult);
}
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);
}
private static boolean isLocalElement(Element element) {
String kind = element.getKind();
if (kind.equals(ElementKind.LOCAL_VARIABLE) || kind.equals(ElementKind.PARAMETER)) {
return true;
}
// TODO(scheglov) consider adding LOCAL_FUNCTION
return false;
}
/**
* 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;
}
/**
* 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.getLocation().getOffset() - o2.element.getLocation().getOffset();
}
});
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);
setResultsDescription(rootItem.numMatches);
// 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 totalCount = 0;
private int filteredCountSdk = 0;
private int filteredCountPotential = 0;
private int filteredCountProject = 0;
private String filtersDesc;
private static final Predicate<SearchResult> FILTER_SDK = new Predicate<SearchResult>() {
@Override
public boolean apply(SearchResult input) {
// TODO (jwren/scheglov) SearchResult API has changed
// Source source = input.getSource();
// UriKind uriKind = source.getUriKind();
// return uriKind == UriKind.DART_URI || uriKind == UriKind.PACKAGE_URI;
return false;
}
};
private static final Predicate<SearchResult> FILTER_POTENTIAL = new Predicate<SearchResult>() {
@Override
public boolean apply(SearchResult input) {
return input.isPotential();
}
};
private final Predicate<SearchResult> FILTER_PROJECT = new Predicate<SearchResult>() {
@Override
public boolean apply(SearchResult input) {
IProject currentProject = getCurrentProject();
String file = input.getLocation().getFile();
IFile resource = DartUI.getSourceFile(file);
return resource != null && currentProject != null
&& currentProject.equals(resource.getProject());
}
};
private long lastQueryStartTime = 0;
private long lastQueryFinishTime = 0;
public SearchResultPage_NEW(SearchView searchView, String taskName) {
this.searchView = searchView;
this.taskName = taskName;
}
@Override
public void createControl(Composite parent) {
initFilters();
viewer = new TreeViewer(parent, SWT.FULL_SELECTION | SWT.MULTI);
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 search query.
*
* @return the {@link SearchResult}s to display.
*/
protected abstract List<SearchResult> 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
try {
for (LineItem lineItem : item.lines) {
IFile resource = DartUI.getSourceFile(lineItem.filePath);
if (resource != null && resource.exists()) {
markerResources.add(resource);
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<SearchResult> applyFilters(List<SearchResult> matches) {
filteredCountSdk = 0;
filteredCountPotential = 0;
filteredCountProject = 0;
IProject currentProject = getCurrentProject();
List<SearchResult> filtered = Lists.newArrayList();
for (SearchResult 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 "position" in the "filePath" file.
*/
private void openPosition(String filePath, Position position) {
try {
DartUI.openFilePath(filePath, true, position.offset, position.length);
} catch (Throwable e) {
ExceptionHandler.handle(e, "Search", "Exception during open.");
}
}
/**
* 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;
openPosition(lineItem.filePath, 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;
}
String filePath = itemCursor.getLine().filePath;
Position position = itemCursor.getPosition();
// open position
openPosition(filePath, 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<SearchResult> results = runQuery();
totalCount = results.size();
results = applyFilters(results);
// set description
{
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)";
}
}
}
setResultsDescription(results.size());
// process query results
rootItem = buildElementItemTree(results);
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);
}
});
}
private void setResultsDescription(int resultCount) {
setContentDescription("'" + getQueryElementName() + "' - " + resultCount + " "
+ getQueryKindName() + ", total: " + totalCount + filtersDesc);
}
/**
* Shows current {@link #itemCursor} state.
*/
private void showCursor() {
try {
LineItem lineItem = itemCursor.getLine();
viewer.setSelection(new StructuredSelection(lineItem), true);
// open editor
IEditorPart editor = DartUI.openFilePath(lineItem.filePath, false);
// 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) {
for (LineItem lineItem : item.lines) {
IFile file = DartUI.getSourceFile(lineItem.filePath);
if (file != null) {
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();
}
});
}
}