/** * Copyright (c) 2015 by Brainwy Software Ltda. All Rights Reserved. * Licensed under1 the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.debug.newconsole.prefs; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; 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.ParameterizedCommand; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.action.IAction; import org.eclipse.jface.bindings.Binding; import org.eclipse.jface.bindings.TriggerSequence; import org.eclipse.jface.bindings.keys.KeySequence; import org.eclipse.jface.bindings.keys.ParseException; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; import org.python.pydev.editor.PyEdit; import org.python.pydev.editor.actions.IExecuteLineAction; import org.python.pydev.shared_core.callbacks.CallbackWithListeners; import org.python.pydev.shared_core.callbacks.ICallbackListener; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.path_watch.IFilesystemChangesListener; import org.python.pydev.shared_core.path_watch.IPathWatch; import org.python.pydev.shared_core.path_watch.PathWatch; import org.python.pydev.shared_core.preferences.IScopedPreferences; import org.python.pydev.shared_core.preferences.ScopedPreferences; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.TextSelectionUtils; import org.python.pydev.shared_ui.bindings.BindKeysHelper; import org.python.pydev.shared_ui.bindings.BindKeysHelper.IFilter; import org.python.pydev.shared_ui.bindings.KeyBindingHelper; import org.python.pydev.shared_ui.utils.RunInUiThread; /** * Used to deal with the interactive console commands defined by the user. * * These commands can be bound to a key which when triggered will send some * content to the console based on the current selection and defined text. */ public class InteractiveConsoleCommand { public static final class InteractiveCommandCustomHandler extends AbstractHandler { private final InteractiveConsoleCommand interactiveConsoleCommand; public InteractiveCommandCustomHandler(InteractiveConsoleCommand interactiveConsoleCommand) { this.interactiveConsoleCommand = interactiveConsoleCommand; } @Override public Object execute(ExecutionEvent event) throws ExecutionException { Object applicationContext = event.getApplicationContext(); if (applicationContext instanceof org.eclipse.core.expressions.IEvaluationContext) { org.eclipse.core.expressions.IEvaluationContext iEvaluationContext = (org.eclipse.core.expressions.IEvaluationContext) applicationContext; Object activeEditor = iEvaluationContext.getVariable("activeEditor"); if (activeEditor instanceof PyEdit) { PyEdit pyEdit = (PyEdit) activeEditor; execute(pyEdit); } else { Log.log("Expected PyEdit. Found: " + activeEditor); } } return null; } public void execute(PyEdit pyEdit) { IAction action = pyEdit.getAction("org.python.pydev.editor.actions.execLineInConsole"); if (action instanceof IExecuteLineAction) { IExecuteLineAction executeLineAction = (IExecuteLineAction) action; String commandText = this.interactiveConsoleCommand.commandText; TextSelectionUtils ts = pyEdit.createTextSelectionUtils(); String selectedText = ts.getSelectedText(); if (selectedText.length() == 0) { selectedText = ts.getCursorLineContents(); } executeLineAction.executeText(new FastStringBuffer(commandText, selectedText.length() * 2).replaceAll( "${text}", selectedText).toString()); } else { Log.log("Expected: " + action + " to implement IExecuteLineAction."); } } } // The name is always the USER_COMMAND_PREFIX + int (saying which command is bound). public static final String USER_COMMAND_PREFIX = "org.python.pydev.custom.interactive_console.user_command.InteractiveConsoleUserCommand"; /** * The name for the command (caption for the user/keybindings). */ public final String name; public String keybinding = ""; public String commandText = ""; private static final CallbackWithListeners<Object> interactiveConsoleCommandsChanged = new CallbackWithListeners<>(); private static final IFilesystemChangesListener pathChangesListener = new IFilesystemChangesListener() { @Override public void removed(File file) { interactiveConsoleCommandsChanged.call(null); } @Override public void added(File file) { interactiveConsoleCommandsChanged.call(null); } }; /** * @param name the name of the command. * Note that other fields must be properly filled later on. */ public InteractiveConsoleCommand(String name) { Assert.isNotNull(name); Assert.isTrue(name.length() > 0); this.name = name; } /** * @return a map (which we can persist and use later to create a new command). */ public Map<String, Object> asMap() { HashMap<String, Object> map = new HashMap<>(); map.put("name", name); map.put("keybinding", keybinding); map.put("commandText", commandText); return map; } /** * Creates the command from a map. May return null if the contents are not valid. */ public static InteractiveConsoleCommand createFromMap(Map<String, Object> map) { String name = (String) map.get("name"); if (name == null || name.length() == 0) { return null; } String keybinding = (String) map.get("keybinding"); if (keybinding == null) { return null; } String commandText = (String) map.get("commandText"); if (commandText == null) { return null; } InteractiveConsoleCommand ret = new InteractiveConsoleCommand(name); ret.keybinding = keybinding; ret.commandText = commandText; return ret; } /** * Loads the commands that the user created previously in the preferences. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static List<InteractiveConsoleCommand> loadExistingCommands() { List<InteractiveConsoleCommand> ret = new ArrayList<InteractiveConsoleCommand>(); try { IScopedPreferences scopedPreferences = getScopedPreferences(); File workspaceSettingsLocation = scopedPreferences.getWorkspaceSettingsLocation(); Map<String, Object> contentsAsMap = scopedPreferences.getYamlFileContents(workspaceSettingsLocation); if (contentsAsMap != null) { Object commands = contentsAsMap.get("commands"); if (commands instanceof Collection) { Collection collection = (Collection) commands; for (Iterator iterator = collection.iterator(); iterator.hasNext();) { Object object = iterator.next(); if (object instanceof Map) { Map<String, Object> map = (Map<String, Object>) object; InteractiveConsoleCommand created = InteractiveConsoleCommand.createFromMap(map); if (created != null) { ret.add(created); } } } } } } catch (Exception e) { Log.log(e); } return ret; } /** * @return the preferences we'll use to save the commands. */ public static IScopedPreferences getScopedPreferences() { return ScopedPreferences.get("org.python.pydev.interactive_console.commands"); } /** * Helper to track changes to the preferences. */ private static IPathWatch watcher = null; /** * Helper for locking. */ private static final Object lock = new Object(); /** * Whenever the preferences related to the command change the passed callback will be called. */ public static void registerOnCommandsChangedCallback(ICallbackListener<Object> iCallbackListener) { final File workspaceSettingsLocation = getScopedPreferences().getWorkspaceSettingsLocation(); File folderToTrack = workspaceSettingsLocation.getParentFile(); if (folderToTrack.exists()) { if (!folderToTrack.isDirectory()) { folderToTrack.delete(); folderToTrack.mkdirs(); } } else { // It doesn't exist: create it. folderToTrack.mkdirs(); } interactiveConsoleCommandsChanged.registerListener(iCallbackListener); // Make sure that we're really tracking the needed folder synchronized (lock) { if (watcher != null) { if (!watcher.hasTracker(folderToTrack, pathChangesListener)) { watcher.dispose(); watcher = null; } } if (watcher == null) { watcher = new PathWatch(); watcher.setDirectoryFileFilter(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.equals(workspaceSettingsLocation); } }, new FileFilter() { @Override public boolean accept(File pathname) { return false; } }); watcher.track(folderToTrack, pathChangesListener); } } } public static void unregisterOnCommandsChangedCallback(ICallbackListener<Object> iCallbackListener) { interactiveConsoleCommandsChanged.unregisterListener(iCallbackListener); } /** * Helper which when called will synchronize the interactive console commands to the * actual commands/bindings in the eclipse preferences. * * Note that it may be called even if nothing changed... */ private static ICallbackListener<Object> syncCommands = new ICallbackListener<Object>() { @Override public Object call(Object obj) { Runnable r = new Runnable() { @Override public void run() { syncCommands(); } }; RunInUiThread.async(r, false); return null; } }; private static boolean alreadyListening = false; /** * Makes sure that whenever the user commands are changed the keybindings/commands are kept up to date. */ public static void keepBindingsUpdated() { if (alreadyListening) { return; } alreadyListening = true; try { registerOnCommandsChangedCallback(syncCommands); //On the first call make sure that we have the initial state synchronized. syncCommands.call(null); } catch (Exception e) { Log.log(e); } } /** * Creates a handler for the given command. */ protected static InteractiveCommandCustomHandler createHandler( final InteractiveConsoleCommand interactiveConsoleCommand) { return new InteractiveCommandCustomHandler(interactiveConsoleCommand); } /** * Makes sure that the commands are always updated (to be called in the UI-thread). Not thread safe. */ private static void syncCommands() { IWorkbench workbench; try { workbench = PlatformUI.getWorkbench(); } catch (Throwable e) { // It's already disposed or not even created -- and the class may be unavailable on unit-tests. // Log.log(e); -- don't even log (if we're in a state we can't use it, there's no point in doing anything). return; } ICommandService commandService = (ICommandService) workbench.getService(ICommandService.class); //Note: hardcoding that we want to deal with the PyDev editor category. Category pydevCommandsCategory = commandService.getCategory("org.python.pydev.ui.category.source"); if (!pydevCommandsCategory.isDefined()) { Log.log("Expecting org.python.pydev.ui.category.source to be a a defined commands category."); return; } //First we have to remove bindings which would conflict. final Set<KeySequence> removeKeySequences = new HashSet<>(); List<InteractiveConsoleCommand> existingCommands = InteractiveConsoleCommand.loadExistingCommands(); for (InteractiveConsoleCommand interactiveConsoleCommand : existingCommands) { try { removeKeySequences.add(KeyBindingHelper.getKeySequence(interactiveConsoleCommand.keybinding)); } catch (Exception e) { Log.log("Error resolving: " + interactiveConsoleCommand.keybinding, e); } } BindKeysHelper bindKeysHelper = new BindKeysHelper(PyEdit.PYDEV_EDITOR_KEYBINDINGS_CONTEXT_ID); bindKeysHelper.removeUserBindingsWithFilter(new IFilter() { @Override public boolean removeBinding(Binding binding) { TriggerSequence triggerSequence = binding.getTriggerSequence(); if (removeKeySequences.contains(triggerSequence)) { return true; } ParameterizedCommand command = binding.getParameterizedCommand(); if (command == null) { return false; } String id = command.getId(); if (id.startsWith(USER_COMMAND_PREFIX)) { return true; } return false; } }); Map<String, InteractiveCommandCustomHandler> commandIdToHandler = new HashMap<>(); // Now, define the commands and the bindings for the user-commands. int i = 0; for (InteractiveConsoleCommand interactiveConsoleCommand : existingCommands) { try { String commandId = USER_COMMAND_PREFIX + i; Command cmd = commandService.getCommand(commandId); if (!cmd.isDefined()) { cmd.define(interactiveConsoleCommand.name, interactiveConsoleCommand.name, pydevCommandsCategory); } InteractiveCommandCustomHandler handler = createHandler(interactiveConsoleCommand); commandIdToHandler.put(commandId, handler); cmd.setHandler(handler); KeySequence keySequence; try { if (interactiveConsoleCommand.keybinding == null || interactiveConsoleCommand.keybinding.length() == 0) { continue; } keySequence = KeyBindingHelper.getKeySequence(interactiveConsoleCommand.keybinding); } catch (IllegalArgumentException | ParseException e) { Log.log("Error resolving: " + interactiveConsoleCommand.keybinding, e); continue; } bindKeysHelper.addUserBindings(keySequence, new ParameterizedCommand(cmd, null)); } catch (Exception e) { Log.log(e); } i++; } // Unbind any command we may have previously created. for (; i < 100; i++) { Command cmd = commandService .getCommand(USER_COMMAND_PREFIX + i); if (cmd.isDefined()) { cmd.undefine(); } } bindKeysHelper.saveIfChanged(); setCommandIdToHandler(commandIdToHandler); } /** * API to know that the list of commands pointing from command id to the handler changed. */ public static final CallbackWithListeners<Object> onCommandIdToHandlerChanged = new CallbackWithListeners<>(); private static Map<String, InteractiveCommandCustomHandler> commandIdToHandler = new HashMap<>(); private static void setCommandIdToHandler(Map<String, InteractiveCommandCustomHandler> commandIdToHandler0) { commandIdToHandler = commandIdToHandler0; onCommandIdToHandlerChanged.call(commandIdToHandler0); } public static Map<String, InteractiveCommandCustomHandler> getCommandIdToHandler() { return commandIdToHandler; } public boolean isValid() { if (this.name != null && this.name.trim().length() > 0 && this.keybinding != null && this.keybinding.trim().length() > 0 && this.commandText != null) { //it may be valid, let's check if the keybinding actually resolves. try { KeyBindingHelper.getKeySequence(keybinding); } catch (IllegalArgumentException | ParseException e) { return false; } return true; } return false; } }