package org.rascalmpl.eclipse.library.util;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.Category;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandler;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.CompoundContributionItem;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.eclipse.ui.services.IServiceLocator;
import org.rascalmpl.eclipse.terms.TermLanguageRegistry;
import org.rascalmpl.eclipse.util.RascalInvoker;
import org.rascalmpl.interpreter.asserts.NotYetImplemented;
import org.rascalmpl.interpreter.result.ICallableValue;
import io.usethesource.vallang.IConstructor;
import io.usethesource.vallang.ISet;
import io.usethesource.vallang.ISourceLocation;
import io.usethesource.vallang.IString;
import io.usethesource.vallang.IValue;
import io.usethesource.vallang.IValueFactory;
import io.usethesource.vallang.type.Type;
import io.usethesource.vallang.type.TypeFactory;
import org.rascalmpl.values.ValueFactoryFactory;
public class NonRascalMenuContributionItem extends CompoundContributionItem {
private static String NON_RASCAL_CONTRIBUTION_COMMAND_CATEGORY = "org.rascalmpl.eclipse.library.util.NRCMCC";
private static String NON_RASCAL_CONTRIBUTION_COMMAND_PREFIX = "org.rascalmpl.eclipse.library.util.NRCMCP";
private final static TypeFactory TF = TypeFactory.getInstance();
private final static IValueFactory VF = ValueFactoryFactory.getValueFactory();
/**
* Creates a compound contribution item with a <code>null</code> id.
*/
public NonRascalMenuContributionItem() {
super();
}
/**
* Creates a compound contribution item with the given (optional) id.
*
* @param id the contribution item identifier, or <code>null</code>
*/
public NonRascalMenuContributionItem(String id) {
super(id);
}
private class ContributionCacheItem {
public WeakReference<ISet> rascalContributions; // avoid being to only one with a reference to a bunch of rascal callbacks with evaluators
public List<String> eclipseContributionsIds;
}
private static Map<String, ContributionCacheItem> contributionCache = new ConcurrentHashMap<String, ContributionCacheItem>();
private static void cleanupCacheFor(String currentEditorId) {
ContributionCacheItem cachedItemIds;
synchronized (contributionCache) { // avoid double deletion
cachedItemIds = contributionCache.remove(currentEditorId);
}
if (cachedItemIds != null){
ICommandService cmdService = getCommandService();
for (String cmdId : cachedItemIds.eclipseContributionsIds) {
Command currentCommand = cmdService.getCommand(cmdId);
IHandler currentHandler = currentCommand.getHandler();
currentCommand.undefine();
currentHandler.dispose();
}
}
}
@Override
protected IContributionItem[] getContributionItems() {
String currentEditorId = getCurrentEditorID();
if (currentEditorId.isEmpty()) {
return new IContributionItem[0];
}
ISet contribs = TermLanguageRegistry.getInstance().getNonRascalContributions(currentEditorId);
if (contribs == null) {
cleanupCacheFor(currentEditorId);
return new IContributionItem[0];
}
ContributionCacheItem cachedItemIds = contributionCache.get(currentEditorId);
List<String> contributionItemIds;
if (cachedItemIds != null && cachedItemIds.rascalContributions.get() == contribs) {
contributionItemIds = cachedItemIds.eclipseContributionsIds;
}
else {
cleanupCacheFor(currentEditorId); // first cleanup the cache items to avoid reusing an old one
contributionItemIds = generateContributions(contribs);
Collections.sort(contributionItemIds);// make sure the members are always in the same order.
// updating the cache
cachedItemIds = new ContributionCacheItem();
cachedItemIds.rascalContributions = new WeakReference<ISet>(contribs);
cachedItemIds.eclipseContributionsIds = contributionItemIds;
contributionCache.put(currentEditorId, cachedItemIds);
}
// we cannot cache this because eclipse disposes these menu items.
IContributionItem[] result = new IContributionItem[contributionItemIds.size()];
IServiceLocator serviceLocator = getServiceLocator();
for (int i = 0; i < contributionItemIds.size(); i++ ) {
CommandContributionItemParameter newCommandParams = new CommandContributionItemParameter(
serviceLocator, null, contributionItemIds.get(i), CommandContributionItem.STYLE_PUSH);
result[i] = new CommandContributionItem(newCommandParams);
}
return result;
}
private List<String> generateContributions(ISet contribs) {
ICommandService cmdService = getCommandService();
IHandlerService handlerService = getHandlerService();
Category defaultCategory = getDefaultCategory(cmdService);
List<String> result = new ArrayList<String>();
for (IValue contrib : contribs) {
IConstructor node = (IConstructor) contrib;
if (node.getName().equals("popup")) {
result.add(contribute(defaultCategory, cmdService, handlerService, (IConstructor)node.get("menu")));
}
}
return result;
}
private String contribute(Category defaultCategory, ICommandService cmdService, IHandlerService handlerService, IConstructor menu) {
String label = ((IString) menu.get("label")).getValue();
if (menu.getName().equals("edit")) {
throw new RuntimeException("Edit is not support by non rascal windows");
}
else if (menu.getName().equals("action") && menu.has("handler")) {
//Because we are not sure a label will not break the characters in a command id, we encode it.
String commandId = NON_RASCAL_CONTRIBUTION_COMMAND_PREFIX + encodeLabel(label);
Command newCommand = cmdService.getCommand(commandId);
if (!newCommand.isDefined()) {
newCommand.define(label, "A non rascal contribution", defaultCategory);
}
final ICallableValue func = (ICallableValue) menu.get("handler");
IHandler handler = new AbstractHandler() {
public Object execute(ExecutionEvent event) throws ExecutionException {
ITextSelection selection = (ITextSelection)HandlerUtil.getActiveWorkbenchWindowChecked(event).getSelectionService().getSelection();
IEditorInput activeEditorInput = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor().getEditorInput();
ISourceLocation fileRef = new Resources(VF).makeFile(activeEditorInput);
final ISourceLocation selectedLine = VF.sourceLocation(fileRef, selection.getOffset(), selection.getLength(), selection.getStartLine() + 1, selection.getEndLine() + 1, 0, 0);
final IString selectedText = VF.string(selection.getText());
if (selectedLine != null) {
RascalInvoker.invokeAsync(new Runnable() {
public void run() {
func.getEval().__setInterrupt(false);
func.call(new Type[] { TF.stringType(), TF.sourceLocationType() }, new IValue[] { selectedText, selectedLine }, null);
}
}, func.getEval());
}
return null;
}
};
handlerService.activateHandler(commandId, handler);
return commandId;
}
else {
throw new NotYetImplemented("Advanced menu structures are not yet implemented.");
}
}
private String encodeLabel(String label) {
try {
return URLEncoder.encode(label, "UTF-8");
} catch (UnsupportedEncodingException e) {
return label;
}
}
private static String getCurrentEditorID() {
return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor().getEditorSite().getId();
}
private static IServiceLocator getServiceLocator() {
return PlatformUI.getWorkbench().getActiveWorkbenchWindow();
}
private static IHandlerService getHandlerService() {
return (IHandlerService) getServiceLocator().getService(IHandlerService.class);
}
private static ICommandService getCommandService() {
return (ICommandService)getServiceLocator().getService(ICommandService.class);
}
private Category getDefaultCategory(ICommandService cmdService) {
Category defaultCategory = cmdService.getCategory(NON_RASCAL_CONTRIBUTION_COMMAND_CATEGORY);
if (!defaultCategory.isDefined()) {
defaultCategory.define("Non Rascal Contributions", "A category for non rascal contributions");
}
return defaultCategory;
}
}