blob: 7628042b552dc75fb746d0a5cdb337d4c66a8ec0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018, 2019 1C-Soft LLC.
*
* This program and the accompanying materials are made available under
* the terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Vladimir Piskarev (1C) - initial API and implementation
*******************************************************************************/
package org.eclipse.handly.ui.search;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Consumer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.handly.internal.ui.Activator;
import org.eclipse.handly.model.Elements;
import org.eclipse.handly.model.IElement;
import org.eclipse.handly.model.adapter.IContentAdapter;
import org.eclipse.handly.model.adapter.NullContentAdapter;
import org.eclipse.handly.ui.IInputElementProvider;
import org.eclipse.search.ui.ISearchQuery;
import org.eclipse.search.ui.NewSearchUI;
import org.eclipse.search.ui.text.AbstractTextSearchResult;
import org.eclipse.search.ui.text.IEditorMatchAdapter;
import org.eclipse.search.ui.text.IFileMatchAdapter;
import org.eclipse.search.ui.text.Match;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.ide.ResourceUtil;
import org.eclipse.ui.part.FileEditorInput;
/**
* A partial implementation of Handly-based search result.
* Assumes that {@link Match matches} are reported against {@link IElement}s
* (or elements that can be adapted to <code>IElement</code>s via a {@link
* #getContentAdapter() content adapter}); also supports matches that are
* reported against {@link IResource}s. Implements {@link #getEditorMatchAdapter()}
* and {@link #getFileMatchAdapter()} methods of {@link AbstractTextSearchResult}
* by returning appropriate adapters. An {@link HandlySearchResultUpdater updater}
* can be provided at construction time that will update the content of the
* search result on element change events.
*/
public abstract class AbstractHandlySearchResult
extends AbstractTextSearchResult
implements IEditorMatchAdapter, IFileMatchAdapter
{
private static final Match[] NO_MATCHES = new Match[0];
/**
* Constructs a new <code>AbstractHandlySearchResult</code>.
*
* @param updater a search result updater, or <code>null</code>
* if updating is not desired
*/
public AbstractHandlySearchResult(HandlySearchResultUpdater updater)
{
if (updater != null)
{
AbstractHandlySearchResult searchResult = this;
NewSearchUI.addQueryListener(new QueryListenerAdapter()
{
@Override
public void queryFinished(ISearchQuery query)
{
if (query.getSearchResult() == searchResult)
updater.add(searchResult);
}
@Override
public void queryRemoved(ISearchQuery query)
{
if (query.getSearchResult() == searchResult)
{
updater.remove(searchResult);
NewSearchUI.removeQueryListener(this);
}
}
});
}
}
/**
* {@inheritDoc}
* <p>
* If the match element is not an {@link IResource} and could be adapted
* to an {@link IElement} through the {@link #getContentAdapter() content
* adapter}, this implementation uses the {@link #getInputElementProvider()
* input element provider} to determine the corresponding <code>IElement</code>
* for the editor input and, if there is such an input <code>IElement</code>,
* returns <code>true</code> if and only if the input element {@link
* Elements#isAncestorOf(IElement, IElement) contains} the adapter element.
* Otherwise, this implementation returns <code>true</code> if and only
* if the corresponding {@link IResource} (if any) for the match element
* equals the resource {@link ResourceUtil#getResource(IEditorInput)
* corresponding} to the editor input. The corresponding resource for
* the match element is determined as follows:
* </p>
* <ul>
* <li>
* If the match element is an {@link IResource}, the corresponding resource
* is the element itself.
* </li>
* <li>
* Otherwise, if the match element could be adapted to an {@link IElement}
* through the {@link #getContentAdapter() content adapter}, the corresponding
* resource is obtained via {@link Elements#getResource(IElement)}.
* </li>
* </ul>
*/
@Override
public boolean isShownInEditor(Match match, IEditorPart editor)
{
IEditorInput editorInput = editor.getEditorInput();
ContainmentContext context = new ContainmentContext(
getInputElementProvider().getElement(editorInput),
ResourceUtil.getResource(editorInput));
if (context.isEmpty())
return false;
ContainmentAdapter containmentAdapter = new ContainmentAdapter(
getContentAdapter());
return containmentAdapter.contains(context, match.getElement());
}
/**
* {@inheritDoc}
* <p>
* If the editor input could be adapted to an {@link IFile},
* this implementation collects all matches reported against the
* file, as returned by {@link #getMatches(Object)}. In addition,
* this implementation uses the {@link #getInputElementProvider() input
* element provider} to determine the corresponding {@link IElement} for
* the editor input and collects all matches reported against elements
* that correspond to the <code>IElement</code> and any of its descendant
* elements (the corresponding elements are determined via the
* {@link IContentAdapter#getCorrespondingElement(IElement)
* getCorrespondingElement(IElement)} method of the {@link
* #getContentAdapter() content adapter}).
* </p>
*/
@Override
public Match[] computeContainedMatches(AbstractTextSearchResult result,
IEditorPart editor)
{
return computeContainedMatches(editor.getEditorInput());
}
/**
* {@inheritDoc}
* <p>
* This implementation collects all matches reported against the given
* file itself, as returned by {@link #getMatches(Object)}. In addition,
* this implementation uses the {@link #getInputElementProvider() input
* element provider} to determine the corresponding {@link IElement} for
* the given file and collects all matches reported against elements that
* correspond to the <code>IElement</code> and any of its descendant
* elements (the corresponding elements are determined via the
* {@link IContentAdapter#getCorrespondingElement(IElement)
* getCorrespondingElement(IElement)} method of the {@link
* #getContentAdapter() content adapter}).
* </p>
*/
@Override
public Match[] computeContainedMatches(AbstractTextSearchResult result,
IFile file)
{
return computeContainedMatches(new FileEditorInput(file));
}
/**
* {@inheritDoc}
* <p>
* If the given element has a corresponding resource that is an {@link
* IFile}, this implementation returns the file. The corresponding resource
* is determined as follows:
* </p>
* <ul>
* <li>
* If the given element is an {@link IResource}, the corresponding resource
* is the element itself.
* </li>
* <li>
* Otherwise, if the given element could be adapted to an {@link IElement}
* through the {@link #getContentAdapter() content adapter}, the corresponding
* resource is obtained via {@link Elements#getResource(IElement)}.
* </li>
* </ul>
*/
@Override
public IFile getFile(Object element)
{
IResource resource = null;
if (element instanceof IResource)
resource = (IResource)element;
else
{
IElement adapterElement = getContentAdapter().adapt(element);
if (adapterElement != null)
resource = Elements.getResource(adapterElement);
}
if (resource instanceof IFile)
return (IFile)resource;
return null;
}
/**
* {@inheritDoc}
* <p>
* This implementation returns this search result, which implements
* <code>IEditorMatchAdapter</code>.
* </p>
*/
@Override
public IEditorMatchAdapter getEditorMatchAdapter()
{
return this;
}
/**
* {@inheritDoc}
* <p>
* This implementation returns this search result, which implements
* <code>IFileMatchAdapter</code>.
* </p>
*/
@Override
public IFileMatchAdapter getFileMatchAdapter()
{
return this;
}
/**
* Returns the content adapter that defines a mapping between
* {@link IElement}s and elements of this search result.
* <p>
* Default implementation returns a {@link NullContentAdapter}.
* Subclasses may override.
* </p>
*
* @return an {@link IContentAdapter} (never <code>null</code>)
*/
public IContentAdapter getContentAdapter()
{
return NullContentAdapter.INSTANCE;
}
/**
* Returns the input element provider for this search result.
*
* @return the input element provider
*/
protected abstract IInputElementProvider getInputElementProvider();
private Match[] computeContainedMatches(IEditorInput editorInput)
{
ArrayList<Match> matches = new ArrayList<>();
IResource resource = ResourceUtil.getResource(editorInput);
if (resource != null)
matches.addAll(Arrays.asList(getMatches(resource)));
IElement element = getInputElementProvider().getElement(editorInput);
if (element != null)
collectContainedMatches(element, into(matches));
return matches.toArray(NO_MATCHES);
}
private void collectContainedMatches(IElement element,
Consumer<Match> acceptor)
{
Match[] matches = getMatches(
getContentAdapter().getCorrespondingElement(element));
for (Match match : matches)
{
acceptor.accept(match);
}
try
{
IElement[] children = Elements.getChildren(element);
for (IElement child : children)
{
collectContainedMatches(child, acceptor);
}
}
catch (CoreException e)
{
if (!Elements.exists(element))
; // ignore
else
Activator.logError(e);
}
}
static <T> Consumer<T> into(Collection<T> c)
{
return (T t) -> c.add(t);
}
static final class ContainmentContext
{
final IElement element;
final IResource resource;
ContainmentContext(IElement element, IResource resource)
{
this.element = element;
this.resource = resource;
}
boolean isEmpty()
{
return element == null && resource == null;
}
}
static final class ContainmentAdapter
{
private final IContentAdapter contentAdapter;
ContainmentAdapter(IContentAdapter contentAdapter)
{
this.contentAdapter = contentAdapter;
}
boolean contains(ContainmentContext context, Object element)
{
IResource resource = null;
if (element instanceof IResource)
resource = (IResource)element;
else
{
IElement adapterElement = contentAdapter.adapt(element);
if (adapterElement != null)
{
if (context.element != null)
return Elements.isAncestorOf(context.element,
adapterElement);
resource = Elements.getResource(adapterElement);
}
}
if (resource != null)
{
if (context.resource instanceof IFile)
return resource.equals(context.resource); // optimization
if (context.resource != null)
return context.resource.getFullPath().isPrefixOf(
resource.getFullPath());
}
return false;
}
boolean isContainedInAnyOf(Object element,
Iterable<ContainmentContext> contexts)
{
for (ContainmentContext context : contexts)
{
if (contains(context, element))
return true;
}
return false;
}
boolean exists(Object element)
{
if (element instanceof IResource)
return ((IResource)element).exists();
IElement adapterElement = contentAdapter.adapt(element);
if (adapterElement != null)
return Elements.exists(adapterElement);
return false;
}
}
}