/*******************************************************************************
* Copyright (c) 2012 VMWare, Inc. and others
* 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
* Pivotal Software, Inc - bugfix STS-3820, open up for regression testing
*******************************************************************************/
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.internal.classpath.PerProjectDependencyDataCache;
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.runtime.shared.DependencyData;
import org.grails.ide.eclipse.ui.contentassist.ClassContentAssistCalculator;
import org.grails.ide.eclipse.ui.contentassist.IContentAssistContext;
import org.grails.ide.eclipse.ui.contentassist.IContentAssistProposalRecorder;
import org.grails.ide.eclipse.ui.internal.inplace.GrailsCompletionUtils.ITextWidget;
/**
* @author Christian Dupuis
* @author Andrew Eisenberg
* @author Kris De Volder
* @author Nieraj Singh
* @since 2.2.0
*/
public abstract class GrailsCompletionUtils {
public static class SWTTextWidget implements ITextWidget {
private Text text;
public SWTTextWidget(Text text) {
this.text = text;
}
public void setText(String content) {
text.setText(content);
}
public String getText() {
return text.getText();
}
public void setSelection(int start) {
text.setSelection(start);
}
}
public interface ITextWidget {
void setText(String content);
String getText();
/**
* Sets position of the cursor (i.e. selection of length 0 at given position).
*/
void setSelection(int length);
}
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==0) return 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 ITextWidget text;
private IProject project;
public GrailsProposalProvider(final IProject project,
Text text) {
this(project, new SWTTextWidget(text));
}
public GrailsProposalProvider(IProject project, ITextWidget textWidget) {
this.text = textWidget;
this.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;
DependencyData data = PerProjectDependencyDataCache.get(project);
if (data!=null) {
String pluginsDir = data.getPluginsDirectory();
System.out.println("pluginsDir: "+pluginsDir);
if (pluginsDir!=null) {
String pluginScripts = "file:" + pluginsDir + "/**/scripts/*.groovy";
System.out.println("looking for plugin scripts: "+pluginScripts);
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 -- ", "",
null, null) };
}
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 + " ", null, null));
}
}
if (!StringUtils.hasLength(environment)) {
for (String proposal : ENVIRONMENTS) {
if (proposal.startsWith(prefix)) {
newProposals.add(new GrailsContentProposal(proposal, environment + proposal + " ", null, null));
}
}
}
// 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);
}
}
}