Project: eclipse-instasearch
/*
 * Copyright (c) 2009 Andrejs Jermakovics. 
 *  
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License v1.0 
 * which accompanies this distribution, and is available at 
 * http://www.eclipse.org/legal/epl-v10.html 
 * 
 * Contributors: 
 *     Andrejs Jermakovics - initial implementation 
 */
package it.unibz.instasearch.ui; 
 
import it.unibz.instasearch.InstaSearchPlugin; 
import it.unibz.instasearch.actions.CheckUpdatesActionDelegate; 
import it.unibz.instasearch.actions.ShowExceptionAction; 
import it.unibz.instasearch.indexing.Field; 
import it.unibz.instasearch.indexing.SearchQuery; 
import it.unibz.instasearch.indexing.SearchResultDoc; 
import it.unibz.instasearch.jobs.CheckUpdatesJob; 
import it.unibz.instasearch.prefs.PreferenceConstants; 
import it.unibz.instasearch.ui.ResultContentProvider.MatchLine; 
 
import java.util.ArrayList; 
import java.util.Collections; 
import java.util.Comparator; 
 
import org.eclipse.core.runtime.ILogListener; 
import org.eclipse.core.runtime.IProgressMonitor; 
import org.eclipse.core.runtime.IStatus; 
import org.eclipse.core.runtime.Status; 
import org.eclipse.core.runtime.jobs.IJobChangeEvent; 
import org.eclipse.core.runtime.jobs.ISchedulingRule; 
import org.eclipse.core.runtime.jobs.Job; 
import org.eclipse.core.runtime.jobs.JobChangeAdapter; 
import org.eclipse.jface.action.Action; 
import org.eclipse.jface.action.IAction; 
import org.eclipse.jface.action.IContributionItem; 
import org.eclipse.jface.action.IMenuListener; 
import org.eclipse.jface.action.IMenuManager; 
import org.eclipse.jface.action.IToolBarManager; 
import org.eclipse.jface.action.MenuManager; 
import org.eclipse.jface.text.IFindReplaceTarget; 
import org.eclipse.jface.util.IPropertyChangeListener; 
import org.eclipse.jface.util.PropertyChangeEvent; 
import org.eclipse.jface.viewers.DecoratingStyledCellLabelProvider; 
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider; 
import org.eclipse.jface.viewers.DoubleClickEvent; 
import org.eclipse.jface.viewers.IBaseLabelProvider; 
import org.eclipse.jface.viewers.IDoubleClickListener; 
import org.eclipse.jface.viewers.IStructuredSelection; 
import org.eclipse.jface.viewers.ITreeViewerListener; 
import org.eclipse.jface.viewers.TreeExpansionEvent; 
import org.eclipse.jface.viewers.TreeViewer; 
import org.eclipse.jface.viewers.ViewerCell; 
import org.eclipse.swt.SWT; 
import org.eclipse.swt.custom.StyleRange; 
import org.eclipse.swt.custom.StyledText; 
import org.eclipse.swt.events.KeyAdapter; 
import org.eclipse.swt.events.KeyEvent; 
import org.eclipse.swt.events.ModifyEvent; 
import org.eclipse.swt.events.ModifyListener; 
import org.eclipse.swt.events.MouseEvent; 
import org.eclipse.swt.events.MouseTrackAdapter; 
import org.eclipse.swt.graphics.Point; 
import org.eclipse.swt.widgets.Composite; 
import org.eclipse.swt.widgets.Display; 
import org.eclipse.swt.widgets.Menu; 
import org.eclipse.ui.IEditorPart; 
import org.eclipse.ui.IViewSite; 
import org.eclipse.ui.PartInitException; 
import org.eclipse.ui.part.ViewPart; 
 
/**
 *  
 */
 
public class InstaSearchView extends ViewPart implements ModifyListener, ILogListener, ITreeViewerListener, IPropertyChangeListener { 
 
 /** The view ID */ 
 public static final String ID = InstaSearchView.class.getName(); // we have just one view 
  
 /** Tree for results */ 
 private TreeViewer resultViewer; 
 /** Textbox for search query */ 
 private StyledText searchText; 
  
 private IAction openAction; 
 private SearchJob searchJob; 
 private ExpandCollapseJob expandCollapseJob; 
 
 private ResultContentProvider contentProvider; 
 private int lastIncrementalSearchPos = 0
  
 // preferences 
 private int maxResults; 
 private int typingSearchDelay; 
 private boolean incrementalSearchEnabled; 
  
 private SearchViewControl searchViewControl; 
  
 /**
  *  
  */
 
 public InstaSearchView() { 
 
  
 @Override 
 public void init(IViewSite site) throws PartInitException  
 
  super.init(site); 
   
  scheduleUpdateCheck(); // schedule update check if view is opened 
   
  InstaSearchPlugin.getDefault().getLog().addLogListener(this); // listen for exceptions 
   
  initPrefs(); 
   
  InstaSearchPlugin.addPreferenceChangeListener(this); 
 
 
 private void initPrefs() { 
  typingSearchDelay = InstaSearchPlugin.getIntPref(PreferenceConstants.P_TYPING_SEARCH_DELAY); 
  maxResults = InstaSearchPlugin.getIntPref(PreferenceConstants.P_SHOWN_FILES_COUNT); 
  incrementalSearchEnabled = InstaSearchPlugin.getBoolPref(PreferenceConstants.P_INCREMENTAL_SEARCH); 
 
  
 @Override 
 public void createPartControl(Composite parent) { 
   
  this.searchViewControl = new SearchViewControl(parent, this); 
   
  searchText = searchViewControl.getSearchText(); 
  resultViewer = searchViewControl.getResultViewer(); 
   
  searchText.addModifyListener(this); 
   
  contentProvider = new ResultContentProvider(); 
  IStyledLabelProvider labelProvider = new ResultLabelProvider(contentProvider); 
  IBaseLabelProvider decoratedLabelProvider = new DecoratingStyledCellLabelProvider(labelProvider, nullnull); 
   
  configureResultViewer(contentProvider, decoratedLabelProvider); 
  searchViewControl.setContentProposalAdapter(new SearchContentProposalProvider(contentProvider)); 
   
  searchJob = new SearchJob(this); 
  expandCollapseJob = new ExpandCollapseJob(); 
   
  makeActions(); 
  hookContextMenu(); 
  hookDoubleClickAction(); 
 
  
 public void modifyText(ModifyEvent e) 
 
  searchJob.cancel(); 
   
  StyleRange[] styleRanges = createStyledSearchString(searchText.getText()); 
  searchText.setStyleRanges(styleRanges); 
   
  if( searchViewControl.isShowingSearchTip() ) return// showing search tip 
   
  searchJob.schedule(getSearchQuery(), false, typingSearchDelay); // start with a delay since user might still be typing 
   
  if( incrementalSearchEnabled ) 
   doIncrementalSearch(); 
 
 
 private void doIncrementalSearch()  
 
  IEditorPart editor = InstaSearchUI.getActiveEditor(); 
   
  if( editor != null ) 
  
   IFindReplaceTarget target = (IFindReplaceTarget) editor.getAdapter(IFindReplaceTarget.class); 
    
   if( target != null ) 
    lastIncrementalSearchPos = target.findAndSelect(lastIncrementalSearchPos, searchText.getText(), truefalsefalse) + searchText.getText().length(); 
  
 
  
 /**
  * Highlight fields 
  *  
  * @param text 
  * @return bold ranges  
  */
 
 private static StyleRange[] createStyledSearchString(String text)  
 
  //TODO: use parser 
  ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>(); 
  String lcaseText = text.toLowerCase(); 
   
  ArrayList<String> fieldsToHighlight = new ArrayList<String>(Field.values().length); 
  for(Field field: Field.values()) // add all field names 
   fieldsToHighlight.add(field.toString()); 
   
  for(String fieldName: fieldsToHighlight)  
  
   int pos = lcaseText.indexOf(fieldName + ':'); // should use a parser here 
    
   while( pos != -1 ) { 
    styleRanges.add(new StyleRange(pos, fieldName.length(), nullnull, SWT.BOLD)); 
    pos = lcaseText.indexOf(fieldName + ':', pos+fieldName.length()-1); // find next 
   
  
   
  // ranges must be sorted by start position 
  Collections.sort(styleRanges, new Comparator<StyleRange>() { 
   public int compare(StyleRange sr1, StyleRange sr2) { 
    return sr1.start - sr2.start; 
   
  }); 
   
  //TODO: highlight AND, OR  
  return styleRanges.toArray(new StyleRange[styleRanges.size()]); 
 
 
 void setSearchString(String searchString) 
 
  searchText.setText(searchString); 
 
  
 /**
  * Starts the search by giving search string as input to the viewer 
  *  
  * @param searchQuery  
  * @param selectLast whether to select the item which is currently last 
  */
 
 void search(SearchQuery searchQuery, boolean selectLast) { 
  searchJob.cancel(); // cancel previous search 
  searchQuery.setFilter( searchViewControl.getFilter() ); 
  searchJob.schedule(searchQuery, selectLast, 0); 
 
  
 private SearchQuery getSearchQuery()  
 
  SearchQuery sq = new SearchQuery(getSearchText(), maxResults ); 
  sq.setFilter( searchViewControl.getFilter() ); 
  return sq; 
 
  
 String getSearchText() { 
  return searchText.getText().trim(); 
 
 
 TreeViewer getResultViewer() { 
  return resultViewer; 
 
  
 public void treeExpanded(TreeExpansionEvent event) { 
    
   if( event.getElement() instanceof SearchQuery ) { 
    search((SearchQuery)event.getElement(), true); 
   } 
 
  
 public void treeCollapsed(TreeExpansionEvent event) { 
 
  
 private void configureResultViewer(ResultContentProvider contentProvider, IBaseLabelProvider decoratedLabelProvider) { 
   
  resultViewer.setContentProvider(contentProvider); 
  resultViewer.setLabelProvider(decoratedLabelProvider); 
  resultViewer.setSorter(null); 
   
  getViewSite().setSelectionProvider(resultViewer); 
  resultViewer.addTreeListener(this); 
   
  resultViewer.getControl().addMouseTrackListener(new MouseTrackAdapter()  
  
   public void mouseHover(MouseEvent e)  
   
    ViewerCell cell = resultViewer.getCell(new Point(e.x, e.y)); 
     
    if( cell != null && cell.getElement() instanceof SearchResultDoc )  
    
     SearchResultDoc doc = (SearchResultDoc) cell.getElement(); 
     resultViewer.getTree().setToolTipText(doc.getFilePath()); 
    
    else 
    
     resultViewer.getTree().setToolTipText(""); 
    
   
  }); 
   
  KeyAdapter keyListener = new KeyAdapter()  
  
   public void keyReleased(KeyEvent e)  
   
    onSearchTextKeyPress(e); 
   
  }; 
   
  resultViewer.getControl().addKeyListener(keyListener); 
  searchText.addKeyListener(keyListener); 
 
  
 private void hookContextMenu()  
 
  MenuManager menuMgr = new MenuManager("#PopupMenu"); 
   
  menuMgr.setRemoveAllWhenShown(true); 
  menuMgr.addMenuListener(new IMenuListener() { 
   public void menuAboutToShow(IMenuManager manager) { 
    fillContextMenu(manager); 
   
  }); 
   
  Menu menu = menuMgr.createContextMenu(resultViewer.getControl()); 
  resultViewer.getControl().setMenu(menu); 
  getSite().registerContextMenu(menuMgr, resultViewer); 
 
 
 private void onSearchTextKeyPress(KeyEvent e)  
 
  if( e.keyCode == SWT.F5 )  
  {  
   refreshSearch(); 
  
  if( e.keyCode == SWT.DEL )  
  
   deleteSelectedMatch(); 
  
  if( e.keyCode == (int)'j' && (e.stateMask & SWT.CTRL)!=0 )  
  
   doIncrementalSearch(); 
  
  else if( e.keyCode == SWT.TAB ) 
  
   resultViewer.getTree().setFocus(); 
  
  else if( e.keyCode == SWT.ESC )  
  
   if( expandCollapseJob.getState() == Job.RUNNING )  
   
    expandCollapseJob.cancel(); 
   
   else  
   
    if( searchText.getSelectionText().equals(searchText.getText()) ) 
    
     searchText.setText(""); 
    
    else 
    
     searchText.setFocus(); 
     searchText.selectAll(); 
    
   
  
  else if( e.getSource() == searchText && e.keyCode == SWT.CR && (e.stateMask & SWT.CTRL)!=0 ) 
  
   showAllResults(); 
  
 
  
 private void fillContextMenu(IMenuManager manager)  
 
  boolean haveSelection = ! resultViewer.getSelection().isEmpty(); 
  SearchQuery sq = (SearchQuery) resultViewer.getInput(); 
   
  openAction.setEnabled( haveSelection  ); 
  manager.add(openAction); 
   
  boolean showingItems = resultViewer.getTree().getItemCount() > 0
   
  Action expandAll = new Action("Expand All", InstaSearchPlugin.getImageDescriptor("expandall")) { 
   public void run() { 
    expandAll(); 
   
  }; 
  expandAll.setEnabled( showingItems  ); 
  manager.add(expandAll); 
   
  Action collapseAll = new Action("Collapse All", InstaSearchPlugin.getImageDescriptor("collapseall")) { 
   public void run() { 
    collapseAll(); 
   
  }; 
  collapseAll.setEnabled( showingItems  ); 
  manager.add(collapseAll); 
   
  Action refresh = new Action("Refresh") { 
   public void run() { 
    refreshSearch(); 
   
  }; 
  refresh.setAccelerator(SWT.F5); 
  manager.add(refresh); 
   
  Action delete = new Action("Delete Match") { 
   public void run() { 
    deleteSelectedMatch(); 
   
  }; 
  delete.setAccelerator(SWT.DEL); 
  manager.add(delete); 
   
  Action moreResults = new Action("More Results...") { 
   public void run() { 
    showAllResults(); 
   
  }; 
  moreResults.setEnabled( showingItems ); 
  manager.add(moreResults); 
   
  if( sq == null || !sq.isLimited() ) 
   moreResults.setEnabled(false); 
     
 
  
 
 private void deleteSelectedMatch() { 
  if( getResultViewer().getSelection() == null ) 
   return
  IStructuredSelection selection = (IStructuredSelection)resultViewer.getSelection(); 
  getResultViewer().remove(selection.toArray()); 
 
  
 /**
  *  
  */
 
 public void showAllResults() { 
  SearchQuery sq = (SearchQuery)resultViewer.getInput(); 
  SearchQuery newSq = new SearchQuery(sq); 
  newSq.setMaxResults(SearchQuery.UNLIMITED_RESULTS); 
  search(newSq, false); 
 
  
 /**
  *  
  */
 
 public void expandAll() { 
  expandCollapseJob.schedule(true); 
 
  
 /**
  *  
  */
 
 public void collapseAll() { 
  expandCollapseJob.schedule(false); 
 
  
 private void openSelection() throws Exception { 
  IStructuredSelection selection = (IStructuredSelection)resultViewer.getSelection(); 
  Object obj = selection.getFirstElement(); 
   
  SearchResultDoc doc = null
  MatchLine selectedLineMatches = null
   
  if(obj instanceof SearchResultDoc) { 
   doc = (SearchResultDoc) obj; 
  else if(obj instanceof MatchLine) { 
   selectedLineMatches = (MatchLine) obj; 
   doc = selectedLineMatches.getResultDoc(); 
  else if(obj instanceof Exception) { 
   InstaSearchUI.showError((Exception)obj); 
   return
  else if(obj instanceof SearchQuery ) { 
   search( (SearchQuery) obj, true ); 
   return
  else 
   return
   
   
  new MatchHighlightJob(doc, selectedLineMatches, contentProvider, searchJob, getSite().getPage()).schedule(); 
 
 
  
  
 private void hookDoubleClickAction() { 
  resultViewer.addDoubleClickListener(new IDoubleClickListener() { 
   public void doubleClick(DoubleClickEvent event) { 
    openAction.run(); 
   
  }); 
 
   
 public void setFocus() { 
  searchText.setFocus(); 
  //searchText.selectAll(); 
 
  
 private void makeActions() { 
  openAction = new Action("Open") { 
   public void run() { 
    try { 
     openSelection(); 
    catch (Exception e) { 
     InstaSearchPlugin.log(e); 
    
   
  }; 
 
  
 private void scheduleUpdateCheck() 
 
  boolean checkUpdates = InstaSearchPlugin.getBoolPref(PreferenceConstants.P_CHECK_UPDATES); 
  if( !checkUpdates ) 
   return
   
  CheckUpdatesJob checkUpdatesJob = new CheckUpdatesJob(); 
  checkUpdatesJob.setSystem(true); 
  checkUpdatesJob.addJobChangeListener(new UpdateJobChangeListener()); 
  checkUpdatesJob.schedule(InstaSearchPlugin.getIntPref(PreferenceConstants.P_UPDATE_CHECK_DELAY)); 
 
 
  
 /**
  * Logging an error in the plugin 
  * Create an action that allows reporting it 
  */
 
 public void logging(IStatus status, String plugin) 
 
  IMenuManager menuManager = getViewSite().getActionBars().getMenuManager(); 
   
  IContributionItem item = menuManager.find(ShowExceptionAction.ID); 
   
  if( item != null ) 
   menuManager.remove(item); 
   
  ShowExceptionAction action = new ShowExceptionAction(status); 
  action.setText("Report Bug"); 
   
  menuManager.add(action); 
 
  
 @Override 
 public void dispose() 
 {   
  super.dispose(); 
   
  if( InstaSearchPlugin.getDefault() != null ) 
  
   InstaSearchPlugin.getDefault().getLog().removeLogListener(this); 
   InstaSearchPlugin.removePreferenceChangeListener(this); 
  
 
  
 private void refreshSearch()  
 
  InstaSearchPlugin.getInstaSearch().updateIndex(); 
   
  SearchQuery input = (SearchQuery) resultViewer.getInput(); 
  if( input == null ) return
  resultViewer.setInput(null); // clear cached search results 
   
  searchJob.cancel(); 
  searchJob.schedule(input, false, typingSearchDelay); 
 
 
 public void propertyChange(PropertyChangeEvent event)  
 
  typingSearchDelay = InstaSearchPlugin.getIntPref(PreferenceConstants.P_TYPING_SEARCH_DELAY); 
  maxResults = InstaSearchPlugin.getIntPref(PreferenceConstants.P_SHOWN_FILES_COUNT); 
  incrementalSearchEnabled = InstaSearchPlugin.getBoolPref(PreferenceConstants.P_INCREMENTAL_SEARCH); 
 
  
 /**
  * Waits for {@link CheckUpdatesJob} to finish and notifies if update is available  
  * by placing an Update button in the view's toolbar 
  */
 
 private class UpdateJobChangeListener extends JobChangeAdapter { 
  
  public void done(IJobChangeEvent event) 
  
   IStatus status = event.getResult(); 
    
   if( status.getSeverity() == IStatus.OK ) 
   
    boolean updateAvailable = (status.getCode() == CheckUpdatesJob.UPDATE_AVAILABLE_CODE);  
    if( updateAvailable )  
    
     getViewSite().getShell().getDisplay().asyncExec(new Runnable() { 
      public void run() 
      
       addUpdateAction(); 
       setTitleToolTip("New version available"); 
       getViewSite().getActionBars().getStatusLineManager().setMessage(getTitleImage(), "New version available"); 
      }      
     }); 
    }  
     
     
   
  
   
  private void addUpdateAction() {  
   IAction updateAction = CheckUpdatesJob.createUpdateNotificationAction(); 
   updateAction.setImageDescriptor( InstaSearchPlugin.getImageDescriptor("lightbulb") ); 
    
   IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager(); 
   mgr.add(updateAction); 
   mgr.update(true); 
    
   IMenuManager menuMgr = getViewSite().getActionBars().getMenuManager(); 
    
   menuMgr.add(updateAction); 
    
   IContributionItem checkUpdatesItem = mgr.find(CheckUpdatesActionDelegate.ID); 
   if( checkUpdatesItem != null )  
    checkUpdatesItem.setVisible(false); // hide Check for Updates action 
  
 
  
 /**
  * Background job that expands/collapses all entries 
  */
 
 private class ExpandCollapseJob extends Job implements ISchedulingRule { 
   
  /**   */ 
  public ExpandCollapseJob() { 
   super("Expand All"); 
    
   setRule(this); 
   //setUser(true); 
    
   // listen to searchJob changes. stop expanding on new search 
   searchJob.addJobChangeListener(new JobChangeAdapter() { 
    public void scheduled(IJobChangeEvent event) { 
     cancel(); // new search 
    
    public void done(IJobChangeEvent event) { 
     cancel(); // canceled search 
    
   }); 
    
  
  
  public void schedule(boolean expandAll) { 
    
   this.cancel(); 
    
   if( !expandAll ) { 
    resultViewer.collapseAll(); 
    return
   
    
   if( resultViewer.getTree().getItemCount() == 0 ) { 
    return
   
    
   this.schedule(); 
  
   
  protected IStatus run(IProgressMonitor monitor) { 
    
   Display display = getViewSite().getShell().getDisplay(); 
    
   Object[] elements = contentProvider.getElements(); 
   monitor.beginTask("InstaSearch Expanding", elements.length); 
    
   for(int i = 0; i < elements.length && !monitor.isCanceled(); i++) { 
    final Object curDoc = elements[i]; 
    if( curDoc == null ) continue
    if( !(curDoc instanceof SearchResultDoc) ) continue
     
    contentProvider.getChildren(curDoc); // get lines from file (they become cached) 
     
    Runnable expander = new Runnable() { 
     public void run() { 
      resultViewer.setExpandedState(curDoc, true); 
     
    }; 
    display.syncExec(expander); // expand in UI thread 
     
    monitor.worked(1); 
   
    
   monitor.done(); 
    
   return Status.OK_STATUS; 
  
 
  public boolean contains(ISchedulingRule rule) { 
   return rule.getClass() == this.getClass(); 
  
 
  public boolean isConflicting(ISchedulingRule rule) { 
   return rule.getClass() == this.getClass(); 
  
   
 
 
}