Project: grails-ide
/*******************************************************************************
 * Copyright (c) 2012 VMWare, Inc. 
 * 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: 
 *     VMWare, Inc. - initial API and implementation 
 *******************************************************************************/
package org.grails.ide.eclipse.ui.internal.inplace; 
 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.Iterator; 
import java.util.List; 
 
import org.eclipse.core.resources.IFile; 
import org.eclipse.core.resources.IProject; 
import org.eclipse.core.runtime.IProgressMonitor; 
import org.eclipse.core.runtime.IStatus; 
import org.eclipse.core.runtime.Status; 
import org.eclipse.core.runtime.jobs.ISchedulingRule; 
import org.eclipse.core.runtime.jobs.Job; 
import org.eclipse.jface.bindings.Trigger; 
import org.eclipse.jface.bindings.TriggerSequence; 
import org.eclipse.jface.bindings.keys.KeyStroke; 
import org.eclipse.jface.fieldassist.ControlDecoration; 
import org.eclipse.jface.fieldassist.FieldDecoration; 
import org.eclipse.jface.fieldassist.FieldDecorationRegistry; 
import org.eclipse.jface.fieldassist.IContentProposal; 
import org.eclipse.jface.fieldassist.IContentProposalProvider; 
import org.eclipse.jface.fieldassist.TextContentAdapter; 
import org.eclipse.jface.viewers.ILabelProvider; 
import org.eclipse.jface.viewers.LabelProvider; 
import org.eclipse.swt.SWT; 
import org.eclipse.swt.graphics.Image; 
import org.eclipse.swt.widgets.Text; 
import org.eclipse.ui.PlatformUI; 
import org.eclipse.ui.keys.IBindingService; 
import org.grails.ide.eclipse.core.GrailsCoreActivator; 
import org.grails.ide.eclipse.core.model.GrailsBuildSettingsHelper; 
import org.grails.ide.eclipse.core.model.IGrailsInstall; 
import org.springframework.core.io.Resource; 
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 
import org.springframework.util.StringUtils; 
import org.springsource.ide.eclipse.commons.frameworks.ui.internal.contentassist.ContentProposalAdapter; 
import org.w3c.dom.Document; 
import org.w3c.dom.Node; 
 
import org.grails.ide.eclipse.ui.contentassist.ClassContentAssistCalculator; 
import org.grails.ide.eclipse.ui.contentassist.IContentAssistContext; 
import org.grails.ide.eclipse.ui.contentassist.IContentAssistProposalRecorder; 
 
/**
 * @author Christian Dupuis 
 * @author Andrew Eisenberg 
 * @author Kris De Volder 
 * @author Nieraj Singh 
 * @since 2.2.0 
 */
 
public abstract class GrailsCompletionUtils { 
 
 public static String getScriptName(String name) { 
  if (name == null
   return null
 
  if (name.endsWith(".groovy")) { 
   name = name.substring(0, name.length() - 7); 
  
  String naturalName = getNaturalName(getShortName(name)); 
  return naturalName.replaceAll("\\s""-").toLowerCase(); 
 
 
 public static String getShortName(String className) { 
  int i = className.lastIndexOf("."); 
  if (i > -1) { 
   className = className.substring(i + 1, className.length()); 
  
  return className; 
 
 
 public static String getNaturalName(String name) { 
  List<String> words = new ArrayList<String>(); 
  int i = 0
  char[] chars = name.toCharArray(); 
  for (int j = 0; j < chars.length; j++) { 
   char c = chars[j]; 
   String w; 
   if (i >= words.size()) { 
    w = ""
    words.add(i, w); 
   
   else { 
    w = words.get(i); 
   
 
   if (Character.isLowerCase(c) || Character.isDigit(c)) { 
    if (Character.isLowerCase(c) && w.length() == 0
     c = Character.toUpperCase(c); 
    else if (w.length() > 1 && Character.isUpperCase(w.charAt(w.length() - 1))) { 
     w = ""
     words.add(++i, w); 
    
 
    words.set(i, w + c); 
   
   else if (Character.isUpperCase(c)) { 
    if ((i == 0 && w.length() == 0) || Character.isUpperCase(w.charAt(w.length() - 1))) { 
     words.set(i, w + c); 
    
    else { 
     words.add(++i, String.valueOf(c)); 
    
   
 
  
 
  StringBuilder buf = new StringBuilder(); 
 
  for (Iterator<String> j = words.iterator(); j.hasNext();) { 
   String word = j.next(); 
   buf.append(word); 
   if (j.hasNext()) 
    buf.append(' '); 
  
  return buf.toString(); 
 
 
 public static ContentProposalAdapter addTypeFieldAssistToText(Text text, IProject project) { 
  KeyStroke triggerKeys = getKeyBindingFor("org.eclipse.ui.edit.text.contentAssist.proposals"); 
  if (triggerKeys==null) { 
   //There's no workable active keybinding for content assist, so we don't provide content assist.  
   return null
  
  if (project == null) { 
   //Without an active/selected project, there's no context to compute proposals. 
   return null
  
 
  int bits = SWT.TOP | SWT.LEFT; 
  ControlDecoration controlDecoration = new ControlDecoration(text, bits); 
  controlDecoration.setMarginWidth(0); 
  controlDecoration.setShowHover(true); 
  controlDecoration.setShowOnlyOnFocus(true); 
  FieldDecoration contentProposalImage = FieldDecorationRegistry.getDefault().getFieldDecoration( 
    FieldDecorationRegistry.DEC_CONTENT_PROPOSAL); 
  controlDecoration.setImage(contentProposalImage.getImage()); 
 
  // Create the proposal provider 
  GrailsProposalProvider proposalProvider = new GrailsProposalProvider(project, text); 
  TextContentAdapter textContentAdapter = new TextContentAdapter(); 
  ContentProposalAdapter adapter = new ContentProposalAdapter(text, textContentAdapter, proposalProvider, 
    triggerKeys, null); 
  ILabelProvider labelProvider = new LabelProvider(); 
  adapter.setLabelProvider(labelProvider); 
  adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); 
  adapter.setFilterStyle(ContentProposalAdapter.FILTER_NONE); 
  return adapter; 
 
 
 private static KeyStroke getKeyBindingFor(String commandId) { 
  IBindingService bindingService= (IBindingService) PlatformUI.getWorkbench().getAdapter(IBindingService.class); 
  TriggerSequence[] bindings = bindingService.getActiveBindingsFor(commandId); 
  if (bindings==null || bindings.length==0) { 
   return null
  else { 
   if (bindings.length > 1) { 
    GrailsCoreActivator.log(new Status(IStatus.WARNING, GrailsCoreActivator.PLUGIN_ID, "Multiple bindings for 'Content assist', ignoring all except first one")); 
   
   TriggerSequence binding = bindings[0]; 
   Trigger[] triggers = binding.getTriggers(); 
   if (triggers.length==0return null
   if (triggers.length > 1) { 
    GrailsCoreActivator.log(new Status(IStatus.WARNING, GrailsCoreActivator.PLUGIN_ID, "Multiple triggers in sequence for 'Content assist'. Only one keyStroke is supported. Content assist in Grails Command prompt will not work")); 
    return null
   
   Trigger trigger = triggers[0]; 
   if (trigger instanceof KeyStroke) { 
    return (KeyStroke) trigger; 
   else { 
    GrailsCoreActivator.log(new Status(IStatus.WARNING, GrailsCoreActivator.PLUGIN_ID, "Trigger for 'Content assist' isn't a KeyStroke")); 
    return null
   }     
  
 
 
 public static class GrailsProposalProvider implements IContentProposalProvider { 
 
  /**
   * Scheduling rule to ensure proposal gatherer jobs do not run concurrently. 
   */
 
  private static final ISchedulingRule jobRule = new ISchedulingRule() { 
   public boolean isConflicting(ISchedulingRule other) { 
    return this==other; 
   
   public boolean contains(ISchedulingRule other) { 
    return this==other; 
   
  }; 
  private Job gatherProposalsJob = null
 
  private static final PathMatchingResourcePatternResolver RESOLVER = new PathMatchingResourcePatternResolver(); 
 
  private static final List<String> ENVIRONMENTS = Arrays.asList(new String[] { "prod""test""dev" }); 
 
  private volatile List<String> proposals = null
 
  private final Text text; 
  private IProject project; 
 
  public GrailsProposalProvider(final IProject project, 
    Text text) { 
   this.text = text; 
   setProject(project); 
  
 
  /**
   * Update content assist when selected project changes. 
   */
 
  public void setProject(final IProject project) { 
   this.proposals = null// Shouldn't be used until initialized 
   this.project = project; 
   final IGrailsInstall install = GrailsCoreActivator.getDefault().getInstallManager().getGrailsInstall(project); 
   final String baseDir = GrailsBuildSettingsHelper.getBaseDir(project); 
   if (gatherProposalsJob!=null) gatherProposalsJob.cancel(); 
   gatherProposalsJob = new Job("Retrieving available scripts") { 
 
    @Override 
    protected IStatus run(IProgressMonitor monitor) { 
     List<String> proposals = new ArrayList<String>(); 
      
     String grailsScripts = "file:" + install.getHome() + "scripts/*.groovy"
     scanForScripts(grailsScripts, proposals); 
     if (monitor.isCanceled()) return Status.CANCEL_STATUS; 
      
     String projectScripts = "file:" + baseDir + "/scripts/*.groovy"
     scanForScripts(projectScripts, proposals); 
     if (monitor.isCanceled()) return Status.CANCEL_STATUS; 
      
     String userHome = System.getProperty("user.home"); 
     String globalScripts = "file:" + userHome + "/.grails/scripts/*.groovy"
     scanForScripts(globalScripts, proposals); 
     if (monitor.isCanceled()) return Status.CANCEL_STATUS; 
      
     String pluginScripts = "file:" + install.getPluginHome(project) + "/**/scripts/*.groovy"
     scanForScripts(pluginScripts, proposals); 
     if (monitor.isCanceled()) return Status.CANCEL_STATUS; 
 
     GrailsProposalProvider.this.proposals = proposals; 
     gatherProposalsJob=null
     return Status.OK_STATUS; 
    
 
   }; 
   gatherProposalsJob.setSystem(true); 
   gatherProposalsJob.setPriority(Job.INTERACTIVE); 
   gatherProposalsJob.setRule(jobRule); 
   gatherProposalsJob.schedule(); 
  
 
  private void scanForScripts(String pattern, List<String> proposals) { 
   try { 
    Resource[] scripts = RESOLVER.getResources(pattern); 
    for (Resource script : scripts) { 
     String scriptName = getScriptName(script.getFilename()); 
     if (!isFiltered(scriptName)) { 
      proposals.add(scriptName); 
     
    
   
   catch (Exception e) { 
    // swallow exception as Spring can't really decide what to throw if dir does not exist 
   
  
   
  private boolean isInitialized() { 
   return proposals!=null
  
 
  protected boolean isFiltered(String scriptName) { 
   return scriptName.startsWith("_") || scriptName.matches("create-app|create-plugin"); 
  
 
  public IContentProposal[] getProposals(String contents, int position) { 
   if (!isInitialized()) { 
    return new IContentProposal[] { new GrailsContentProposal(" -- content assist not ready yet -- """
      nullnull) }; 
   
 
   String prefix = contents.substring(0, position); 
   // split out environments 
   String[] prefixes = StringUtils.split(prefix, " "); 
   String environment = ""
   if (prefixes != null && prefixes.length > 1) { 
    String potentialEnvironment = prefixes[0]; 
    if (ENVIRONMENTS.contains(potentialEnvironment)) { 
     prefix = prefixes[1]; 
     environment = potentialEnvironment + " "
 
    
   
 
   List<IContentProposal> newProposals = new ArrayList<IContentProposal>(); 
 
   // do it again to check second parameter for class name 
   prefixes = StringUtils.split(prefix, " "); 
   if (prefixes != null && prefixes.length > 1) { 
    String potentialClassName = prefixes[1]; 
    new ClassContentAssistCalculator().computeProposals(new GrailsContentAssistContext(project, 
      potentialClassName), new GrailsContentAssistProposalRecorder(environment + prefixes[0], 
      newProposals)); 
   
 
   for (String proposal : proposals) { 
    if (proposal.startsWith(prefix)) { 
     newProposals.add(new GrailsContentProposal(proposal, environment + proposal + " "nullnull)); 
    
   
   if (!StringUtils.hasLength(environment)) { 
    for (String proposal : ENVIRONMENTS) { 
     if (proposal.startsWith(prefix)) { 
      newProposals.add(new GrailsContentProposal(proposal, environment + proposal + " "nullnull)); 
     
    
   
 
    
   // if only one proposal is found apply it immediately 
   if (newProposals.size() == 1) { 
    text.setText(newProposals.get(0).getContent()); 
    text.setSelection(text.getText().length()); 
    return new IContentProposal[0]; 
   
 
   return newProposals.toArray(new IContentProposal[newProposals.size()]); 
  
 
 
 
 private static class GrailsContentAssistProposalRecorder implements IContentAssistProposalRecorder { 
 
  private final String prefix; 
 
  private final List<IContentProposal> proposals; 
 
  public GrailsContentAssistProposalRecorder(String prefix, List<IContentProposal> proposals) { 
   this.prefix = prefix; 
   this.proposals = proposals; 
  
 
  public void recordProposal(Image image, int relevance, String displayText, String replaceText) { 
   proposals.add(new GrailsContentProposal(displayText, prefix + " " + replaceText, null, image)); 
  
 
  public void recordProposal(Image image, int relevance, String displayText, String replaceText, 
    Object proposedObject) { 
   proposals.add(new GrailsContentProposal(displayText, prefix + " " + replaceText, null, image)); 
  
 
 
 private static class GrailsContentAssistContext implements IContentAssistContext { 
 
  private final IProject project; 
 
  private final String prefix; 
 
  public GrailsContentAssistContext(IProject project, String prefix) { 
   this.project = project; 
   this.prefix = prefix; 
  
 
  public String getAttributeName() { 
   // no op 
   return null
  
 
  public Document getDocument() { 
   // no op 
   return null
  
 
  public IFile getFile() { 
   return project.getFile(".project"); 
  
 
  public String getMatchString() { 
   return prefix; 
  
 
  public Node getNode() { 
   // no op 
   return null
  
 
  public Node getParentNode() { 
   // no op 
   return null
  
 
 
 
 private static class GrailsContentProposal implements IContentProposal, Comparable<GrailsContentProposal> { 
 
  private String fLabel; 
 
  private String fContent; 
 
  private String fDescription; 
 
  private Image fImage; 
 
  public GrailsContentProposal(String label, String content, String description, Image image) { 
   fLabel = label; 
   fContent = content; 
   fDescription = description; 
   fImage = image; 
  
 
  public String getContent() { 
   return fContent; 
  
 
  public int getCursorPosition() { 
   if (fContent != null) { 
    return fContent.length(); 
   
   return 0
  
 
  public String getDescription() { 
   return fDescription; 
  
 
  public String getLabel() { 
   return fLabel; 
  
 
  @SuppressWarnings("unused"
  public Image getImage() { 
   return fImage; 
  
 
  public String toString() { 
   return fLabel; 
  
 
  public int compareTo(GrailsContentProposal o) { 
   return this.fContent.compareTo(o.fContent); 
  
 
}