package com.gratex.perconik.activity.ide.listeners; import java.util.List; import com.google.common.base.Joiner; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.search.internal.ui.text.FileSearchQuery; import org.eclipse.search.internal.ui.text.FileSearchResult; import org.eclipse.search.ui.ISearchQuery; import org.eclipse.search.ui.text.FileTextSearchScope; import org.eclipse.search.ui.text.Match; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkingSet; import com.gratex.perconik.activity.ide.IdeData; import com.gratex.perconik.activity.uaca.IdeUacaProxy; import com.gratex.perconik.activity.uaca.IdeUacaUris; import com.gratex.perconik.services.uaca.ide.IdeFindEventRequest; import com.gratex.perconik.services.uaca.ide.IdeFindFileResultData; import com.gratex.perconik.services.uaca.ide.IdeFindResultRowData; import sk.stuba.fiit.perconik.core.annotations.Dependent; import sk.stuba.fiit.perconik.core.listeners.SearchQueryListener; import sk.stuba.fiit.perconik.eclipse.core.resources.Projects; import sk.stuba.fiit.perconik.eclipse.jface.text.Documents; import sk.stuba.fiit.perconik.eclipse.search.ui.text.MatchUnit; import sk.stuba.fiit.perconik.eclipse.swt.widgets.DisplayTask; import sk.stuba.fiit.perconik.eclipse.ui.Pages; import static com.google.common.base.Throwables.propagate; import static com.google.common.collect.Lists.newArrayListWithCapacity; import static com.gratex.perconik.activity.ide.IdeData.setApplicationData; import static com.gratex.perconik.activity.ide.IdeData.setEventData; import static com.gratex.perconik.activity.ide.IdeData.setProjectData; import static com.gratex.perconik.activity.ide.listeners.Utilities.currentTime; /** * A listener of IDE find events. This listener handles desired * events and eventually builds corresponding data transfer objects * of type {@link IdeFindEventRequest} and passes them to the * {@link IdeUacaProxy} to be transferred into the <i>User Activity Central * Application</i> for further processing. * * <p>Find operations are logged when a file search is performed. * * <p>Data available in an {@code IdeFindEventRequest}: * * <ul> * <li>{@code derivedResources} - set to {@code true} if search should * consider derived resources, {@code false} otherwise. * <li>{@code fileTypes} - file name patterns separated by {@code ", "}. * Set to {@code "*"} by default, other examples produce strings such as * {@code "Map*.*, String*.class"}. * <li>{@code queryText} - the search query string. * <li>{@code lookinTypeUri} - search scopes separated by {@code ", "}. * In case of enclosed projects or selected resources the string * consists of a list of resource (project) paths relative to workspace * root (but starting with {@code "/"}), and separated by {@code ", "}. * In case of working sets the string starts with {@code "working sets "} * concatenated to a list of working set names separated by {@code ", "}. * Set to {@code "workspace"} by default, other examples produce strings * such as {@code "/com.gratex.perconik.activity"}, {@code "com.gratex.perconik.activity/src/com/gratex/perconik/activity/ide/listeners/IdeCommitListener.java"}, * (for enclosed projects or selected resources) or {@code "working sets PerConIK Core, PerConIK Gratex, PerConIK Site"} (for working sets). * <li>{@code matchCase} - set to {@code true} if search is case sensitive, * {@code false} otherwise. * <li>{@code matchWholeWord} - always {@code null}, can not be determined. * <li>{@code patternSyntaxTypeUri} - set to {@code "Regular expressions"} when * enabled or {@code "Wildcards"} by default. * <li>{@code resultsPerFiles} - a list of matched files, * see {@code IdeFindFileResultDto} below. * <li>{@code searchSubfolders} - always {@code null}, in fact it is always * {@code true} (whole directory tree is always searched) but {@code null} * indicates that it is not a search option nor accessible via search API. * <li>{@code totalFilesSearched} - always {@code null}, not accessible * via search API. * <li>See {@link IdeListener} for documentation of inherited data. * </ul> * * <p>Data available in an {@code IdeFindFileResultDto}: * * <ul> * <li>{@code file} - matched file, see documentation of * {@code IdeDocumentDto} in {@link IdeDocumentListener} for more details. * <li>{@code rows} - a list of file matches, * see {@code IdeFindResultRowDto} below. * </ul> * * <p>Data available in an {@code IdeFindResultRowDto}: * * <ul> * <li>{@code column} - zero based match position on line, * or {@code null} if can not be determined. * <li>{@code row} - zero based match line number. * <li>{@code text} - matched text. * </ul> * * <p>Note that row and column offsets in documents start from zero * instead of one. * * <p><b>Warning:</b> this listener depends on some Eclipse search API * internals, therefore correct functionality in next versions of Eclipse * IDE is not guaranteed. * * @author Pavol Zbell * @since 1.0 */ @Dependent({FileSearchQuery.class, FileSearchResult.class}) public final class IdeFindListener extends IdeListener implements SearchQueryListener { public IdeFindListener() {} static IdeFindEventRequest build(final long time, final IProject project, final FileSearchQuery query) { final IdeFindEventRequest data = new IdeFindEventRequest(); data.setQueryText(query.getSearchString()); data.setMatchCase(query.isCaseSensitive()); data.setMatchWholeWord(null); data.setSearchSubfolders(null); data.setTotalFilesSearched(null); FileTextSearchScope scope = query.getSearchScope(); String[] patterns = scope.getFileNamePatterns(); IWorkingSet[] sets = scope.getWorkingSets(); IResource[] roots = scope.getRoots(); data.setDerivedResources(scope.includeDerived()); data.setFileTypes(patterns == null ? "*" : Joiner.on(",").join(patterns)); data.setLookinTypeUri(IdeUacaUris.forLookinType(sets == null ? toString(roots) : toString(sets))); data.setPatternSyntaxTypeUri(IdeUacaUris.forPatternSyntaxType(query.isRegexSearch() ? "regex" : "wildcard")); FileSearchResult result = (FileSearchResult) query.getSearchResult(); data.setResultsPerFiles(buildResults(result)); setProjectData(data, project); setApplicationData(data); setEventData(data, time); return data; } private static List<IdeFindFileResultData> buildResults(final FileSearchResult result) { Object[] elements = result.getElements(); List<IdeFindFileResultData> list = newArrayListWithCapacity(elements.length); for (Object element: elements) { IFile file = result.getFile(element); Match[] matches = result.getMatches(element); list.add(buildResult(file, matches)); } return list; } private static IdeFindFileResultData buildResult(final IFile file, final Match[] matches) { IdeFindFileResultData data = new IdeFindFileResultData(); data.setFile(IdeData.newDocumentData(file)); data.setRows(buildMatches(Documents.fromFile(file), matches)); return data; } private static List<IdeFindResultRowData> buildMatches(final IDocument document, final Match[] matches) { List<IdeFindResultRowData> list = newArrayListWithCapacity(matches.length); for (Match match: matches) { list.add(buildMatch(document, match)); } return list; } private static IdeFindResultRowData buildMatch(final IDocument document, final Match match) { IdeFindResultRowData data = new IdeFindResultRowData(); int offset = match.getOffset(); int length = match.getLength(); try { switch (MatchUnit.valueOf(match.getBaseUnit())) { case CHARACTER: data.setRow(document.getLineOfOffset(offset)); data.setColumn(offset - document.getLineOffset(data.getRow())); data.setText(document.get(offset, length)); break; case LINE: data.setRow(offset); data.setColumn(null); data.setText(document.get(document.getLineOffset(offset), length)); break; default: throw new IllegalStateException(); } } catch (BadLocationException e) { propagate(e); } return data; } private static String toString(final IResource[] resources) { if (resources.length == 1 && resources[0] instanceof IWorkspaceRoot) { return "workspace"; } List<String> parts = newArrayListWithCapacity(resources.length); for (IResource resource: resources) { parts.add(resource.getFullPath().toString()); } return Joiner.on(",").join(parts); } private static String toString(final IWorkingSet[] sets) { List<String> parts = newArrayListWithCapacity(sets.length); for (IWorkingSet set: sets) { parts.add(set.getLabel()); } return "working sets " + Joiner.on(",").join(parts); } void process(final long time, final ISearchQuery query) { IWorkbenchPage page = execute(DisplayTask.of(Pages.activePageSupplier())); IProject project = Projects.fromPage(page); // TODO project can not be always determined: when IClassFile is in editor, or when nothing is selected // TODO handle other query types such as JavaSearchQuery if (query instanceof FileSearchQuery) { this.proxy.sendFindEvent(build(time, project, (FileSearchQuery) query)); } } public void queryAdded(final ISearchQuery query) {} public void queryRemoved(final ISearchQuery query) {} public void queryStarting(final ISearchQuery query) {} public void queryFinished(final ISearchQuery query) { final long time = currentTime(); execute(new Runnable() { public void run() { process(time, query); } }); } }