I found few resources on search functionality, here are some from the Eclipse FAQs:
- How do I implement a search operation
- How do I write a Search dialog
- How do I display search results
Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.
Step 1: Search Results
First create a new Plug-in Project and create a class FileSearchResult that implements ISearchResult. Therefore you need a dependency to org.eclipse.search.
package com.codeandme.searchprovider; import java.io.File; import java.util.Collection; import java.util.HashSet; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.search.ui.ISearchQuery; import org.eclipse.search.ui.ISearchResult; import org.eclipse.search.ui.ISearchResultListener; import org.eclipse.search.ui.SearchResultEvent; public class FileSearchResult implements ISearchResult { private final ISearchQuery fQuery; private final ListenerList fListeners = new ListenerList(); private final Collection<File> fResult = new HashSet<File>(); public FileSearchResult(ISearchQuery query) { fQuery = query; } @Override public String getLabel() { return fResult.size() + " file(s) found"; } @Override public String getTooltip() { return "Found files in the filesystem"; } @Override public ImageDescriptor getImageDescriptor() { return null; } @Override public ISearchQuery getQuery() { return fQuery; } @Override public void addListener(ISearchResultListener l) { fListeners.add(l); } @Override public void removeListener(ISearchResultListener l) { fListeners.remove(l); } private void notifyListeners(File file) { SearchResultEvent event = new FileSearchResultEvent(this, file); for (Object listener : fListeners.getListeners()) ((ISearchResultListener) listener).searchResultChanged(event); } public void addFile(File file) { fResult.add(file); notifyListeners(file); } }Nothing special here, we will use addFile() later in this tutorial to add more and more files to our result set. Whenever the result changes, we need to inform interested listeners. So lets see how FileSearchResultEvent looks like:
package com.codeandme.searchprovider; import java.io.File; import org.eclipse.search.ui.ISearchResult; import org.eclipse.search.ui.SearchResultEvent; public class FileSearchResultEvent extends SearchResultEvent { private final File fAddedFile; protected FileSearchResultEvent(ISearchResult searchResult, File addedFile) { super(searchResult); fAddedFile = addedFile; } public File getAddedFile() { return fAddedFile; } }Even simpler! To update results incrementally we hold a reference to the added file.
Step 2: The search query
The main logic for a search is implemented as an ISearchQuery. It has 2 jobs: do the actual search and populate the search result.
package com.codeandme.searchprovider; import java.io.File; import java.io.FileFilter; import java.util.Collection; import java.util.HashSet; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.search.ui.ISearchQuery; import org.eclipse.search.ui.ISearchResult; public class FileSearchQuery implements ISearchQuery { private final File fRoot; private final String fFilter; private final boolean fRecursive; private final FileSearchResult fSearchResult; public FileSearchQuery(String root, String filter, boolean recursive) { fRoot = (root.isEmpty()) ? File.listRoots()[0] : new File(root); fFilter = filter; fRecursive = recursive; fSearchResult = new FileSearchResult(this); } @Override public IStatus run(IProgressMonitor monitor) throws OperationCanceledException { Collection<File> entries = new HashSet<File>(); entries.add(fRoot); do { File entry = entries.iterator().next(); entries.remove(entry); entry.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { if ((pathname.isFile()) && (pathname.getName().contains(fFilter))) { // accept file fSearchResult.addFile(pathname); return true; } if ((pathname.isDirectory()) && (fRecursive)) entries.add(pathname); return false; } }); } while (!entries.isEmpty()); return Status.OK_STATUS; } @Override public String getLabel() { return "Filesystem search"; } @Override public boolean canRerun() { return true; } @Override public boolean canRunInBackground() { return true; } @Override public ISearchResult getSearchResult() { return fSearchResult; } }Our query needs 3 parameters:
- a root object
- a file name filter
- a recursive flag to scan subfolders
The logic is implemented, now lets add some nice UI elements.
Step 3: Search Dialog
To add a new search page to the search dialog (main menu/Search/Search...) we use the extension point org.eclipse.search.searchPages.
Just provide id, label and a class reference to:
package com.codeandme.searchprovider.ui; import org.eclipse.jface.dialogs.DialogPage; import org.eclipse.search.ui.ISearchPage; import org.eclipse.search.ui.ISearchPageContainer; import org.eclipse.search.ui.NewSearchUI; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import com.codeandme.searchprovider.FileSearchQuery; public class FileSearchPage extends DialogPage implements ISearchPage { private ISearchPageContainer fContainer; @Override public boolean performAction() { FileSearchQuery searchQuery = new FileSearchQuery("/home", "txt", true); NewSearchUI.runQueryInForeground(fContainer.getRunnableContext(), searchQuery); return true; } @Override public void setContainer(ISearchPageContainer container) { fContainer = container; } @Override public void createControl(Composite parent) { Composite root = new Composite(parent, SWT.NULL); [...] // need to set the root element setControl(root); } }I have removed the UI code from createControl() as it is not relevant here. If interested you can find it on github. The only important thing here is to set the root control using setControl().
performAction() starts the actual search. Therefore we create a query and run it either as foreground or as background job. By returning true we indicate that the search dialog shall be closed.
Step 4: Display search results
To add a new page to the Search View we need another extension point: org.eclipse.search.searchResultViewPages.
Add a new viewPage with an id and label. To link this page with search results of a certain kind we provide the class of search results this page can display. Set it to our FileSearchResult type. Finally we provide a class implementation for the page:
package com.codeandme.searchprovider.ui; import org.eclipse.search.ui.ISearchResult; import org.eclipse.search.ui.ISearchResultListener; import org.eclipse.search.ui.ISearchResultPage; import org.eclipse.search.ui.ISearchResultViewPart; import org.eclipse.search.ui.SearchResultEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IMemento; import org.eclipse.ui.PartInitException; import org.eclipse.ui.part.IPageSite; import com.codeandme.searchprovider.FileSearchResultEvent; public class SearchResultPage implements ISearchResultPage, ISearchResultListener { private String fId; private Composite fRootControl; private IPageSite fSite; private Text ftext; @Override public Object getUIState() { return null; } @Override public void setInput(ISearchResult search, Object uiState) { search.addListener(this); } @Override public void setViewPart(ISearchResultViewPart part) { } @Override public void setID(String id) { fId = id; } @Override public String getID() { return fId; } @Override public String getLabel() { return "Filesystem Search Results"; } @Override public IPageSite getSite() { return fSite; } @Override public void init(IPageSite site) throws PartInitException { fSite = site; } @Override public void createControl(Composite parent) { fRootControl = new Composite(parent, SWT.NULL); fRootControl.setLayout(new FillLayout(SWT.HORIZONTAL)); ftext = new Text(fRootControl, SWT.BORDER | SWT.READ_ONLY | SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI); } @Override public void dispose() { // nothing to do } @Override public Control getControl() { return fRootControl; } @Override public void setActionBars(IActionBars actionBars) { } @Override public void setFocus() { fRootControl.setFocus(); } @Override public void restoreState(IMemento memento) { // nothing to do } @Override public void saveState(IMemento memento) { // nothing to do } @Override public void searchResultChanged(SearchResultEvent event) { if (event instanceof FileSearchResultEvent) { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { String newText = ftext.getText() + "\n" + ((FileSearchResultEvent) event).getAddedFile().getAbsolutePath(); ftext.setText(newText); } }); } } }Lots of getters and setters, but nothing complicated. For this example we use a simple text control to add the names of found files. setInput() will be called by the framework to link this view with a search result. Now we attach a listeners to get notified on changes on the result.
I found one caveat while implementing: when your query code (from there we trigger searchResultChanged()) raises an exception, you do not get the output on the console as you might expect. Basically this means that your result page remains empty without any notification that something went wrong. Use the debugger to single step through your code in that case.
There should be a null check:
ReplyDelete@Override
public void setInput(ISearchResult search, Object uiState) {
if (search != null){
search.addListener(this);
}
}