/** * Copyright (c) 2009, 2010 Mark Feber, MulgaSoft * * 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 * */ package com.mulgasoft.emacsplus.execute; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.Map.Entry; import java.util.regex.PatternSyntaxException; import org.eclipse.core.commands.Category; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.IParameter; import org.eclipse.core.commands.common.NotDefinedException; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.texteditor.ITextEditor; import com.mulgasoft.emacsplus.EmacsPlusActivator; import com.mulgasoft.emacsplus.preferences.CommandCategoryEditor; import com.mulgasoft.emacsplus.preferences.EmacsPlusPreferenceConstants; /** * Command & Category support * * @author Mark Feber - initial API and implementation */ public class CommandSupport { // \p{L} = \p{Letter}: letter from any language // \p{Mn} = \p{Non_Spacing_Mark}: a character intended to be combined with another character without taking up extra space (e.g. accents, umlauts, etc.). // \p{Nd} = \p{Decimal_Digit_Number}: a digit zero through nine in any script except ideographic scripts. private static final String IDENT_REGEX= "[\\p{L}[\\p{Mn}[\\p{Nd}]]]"; //$NON-NLS-1$ private static final String DASH = "-"; //$NON-NLS-1$ private static final String STAR = "*"; //$NON-NLS-1$ // May use preference for this later // support y-p -> yank-pop completion boolean partialCompletionMode = true; private static String[] catIncludes = null; static { IPreferenceStore store = EmacsPlusActivator.getDefault().getPreferenceStore(); if (store != null) { catIncludes = CommandCategoryEditor.parseResults(store.getString(EmacsPlusPreferenceConstants.P_COMMAND_CATEGORIES)); } } private static HashSet<Category> catHash = new HashSet<Category>(); private static TreeMap<String,Command> commandTree; /** * Used by preference page to update changes to include categories * @param newCats */ public static void setCategories(String[] newCats){ catIncludes = newCats; catHash = new HashSet<Category>(); } /** * Get the current set of defined categories corresponding to the included category names * * @param ics * @return the filtered set of categories * * @throws NotDefinedException */ private HashSet<Category> getCategories(ICommandService ics) throws NotDefinedException { if (catHash.isEmpty() && catIncludes != null) { Category[] cats = ics.getDefinedCategories(); for (int i = 0; i < cats.length; i++) { for (int j = 0; j < catIncludes.length; j++) { if (catIncludes[j].equals(cats[i].getId())) { catHash.add(cats[i]); break; } } } } return catHash; } public void getContexts(ITextEditor editor) { IContextService contextService = (IContextService) editor.getSite().getService(IContextService.class); @SuppressWarnings("unchecked") // Eclipse documents the collection type Collection<String> col = contextService.getActiveContextIds(); Iterator<String> it = col.iterator(); while (it.hasNext()) { System.out.println("context: " + it.next()); //$NON-NLS-1$ } } /** * Compute the list of commands appropriate for the current editor * The result list is filtered by: * - The set of the defined non-exclusion categories * - Having a defined handler * - All parameters optional * * @param editor * * @return the sorted tree of appropriate commands */ public TreeMap<String, Command> getCommandList(IEditorPart editor) { return getCommandList(editor,false); } public TreeMap<String, Command> getCommandList(IEditorPart editor, boolean all) { ICommandService ics = (ICommandService) editor.getSite().getService(ICommandService.class); Command[] commands = ics.getDefinedCommands(); commandTree = new TreeMap<String, Command>(); try { HashSet<Category>catHash = (all ? null : getCategories(ics)); boolean isOk = all; for (int i = 0; i < commands.length; i++) { if (!isOk && catHash.contains(commands[i].getCategory())) { IParameter[] params = commands[i].getParameters(); isOk = commands[i].isHandled(); // if the command has parameters, they must all be optional if (isOk && params != null) { for (int j = 0; j < params.length; j++) { if (!(isOk = params[j].isOptional())) { break; } } } } if (isOk) { commandTree.put(fixName(commands[i].getName()), commands[i]); } isOk = all; } } catch (NotDefinedException e) {} // getContexts(editor); return commandTree; } // TODO: Clean up /** * Compute the command subtree given the current user input * * @param map * @param subString * @return the computed sub map */ public SortedMap<String, Command> getCommandSubTree(SortedMap<String, Command> map, String subString) { return getCommandSubTree(map,subString,false); } /** * @param map * @param subString * @param isRegex - true if subString already converted to a regex * * @return the computed sub map */ public SortedMap<String, Command> getCommandSubTree(SortedMap<String, Command> map, String subString, boolean isRegex) { return this.getCommandSubTree(map, subString, isRegex, false); } /** * @param map * @param subString * @param isRegex - true if subString already converted to a regex * @param ignoreEnabled - true to return all appropriate commands * * @return the computed sub map */ public SortedMap<String, Command> getCommandSubTree(SortedMap<String, Command> map, String subString, boolean isRegex, boolean ignoreEnabled) { SortedMap<String, Command> result = null; Set<String> keySet = map.keySet(); String fromKey = null; String toKey = null; String searchStr = (isRegex ? subString : toRegex(subString)); Iterator<String> it = keySet.iterator(); String key; if (isRegex || !searchStr.equals(subString)) { // we have to build the map up one by one on regex search result = new TreeMap<String,Command>(); try { while (it.hasNext()) { key = it.next(); if (key.matches(searchStr)) { Command c = map.get(key); if (ignoreEnabled || c.isEnabled()) { // make sure it's enabled result.put(key, c); } } } } catch (PatternSyntaxException e) { // ignore bad pattern - will show as no match } } else { while (it.hasNext()) { key = it.next(); if (key.startsWith(subString)) { if (fromKey == null) { fromKey = key; } } else if (fromKey != null) { toKey = key; break; } } // too bad we can't use 1.6 if (fromKey != null) { if (toKey == null) { result = map.tailMap(fromKey); } else { result = map.subMap(fromKey, toKey); } } if (!ignoreEnabled && result != null) { // enforce enabled for the current position/selection SortedMap<String, Command> firstResult = result; result = new TreeMap<String, Command>(); Set<Entry<String, Command>> entrySet = firstResult.entrySet(); Iterator<Entry<String, Command>> itr = entrySet.iterator(); while (itr.hasNext()) { Entry<String, Command> e = itr.next(); if (e.getValue().isEnabled()) { result.put(e.getKey(), e.getValue()); } } } } if (result == null && partialCompletionMode && !isRegex) { // recurse once with modified search string // searchStr.replace("-", "\\w*-") + "\\w*"; searchStr = searchStr.replace(DASH, IDENT_REGEX + STAR + DASH) + IDENT_REGEX + STAR; result = getCommandSubTree(map, searchStr, true, ignoreEnabled); } return result; } /** * Replace all spaces in the name with dashes a la emacs * Also, avoid any name collisions by adding an index if necessary * * @param name * @return fixed name */ private String fixName(String name){ String result = name.trim(); result = result.toLowerCase().replace(" ","-"); //$NON-NLS-1$ //$NON-NLS-2$ // avoid collisions if (commandTree.get(result) != null) { int i = 1; String tmp = result + "(" + i + ")"; //$NON-NLS-1$ //$NON-NLS-2$ while (commandTree.get(tmp)!= null){ tmp = result + "(" + ++i + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } result = tmp; } return result; } /** * Determine the longest common command name substring that starts with the current substring * * @param subTree * @param subString * @return the longest common name */ public String getCommonString(SortedMap<String, Command> subTree, String subString) { String result = (isWildCarded(subString) ? "" : subString); //$NON-NLS-1$ Set<String> keySet = subTree.keySet(); Iterator<String> it = keySet.iterator(); String key; String possible; key = it.next(); do { if (key.length() > result.length()) { possible = key.substring(0, result.length()+1); while (it.hasNext()) { key = it.next(); if (!key.startsWith(possible)) { return result; } } result = possible; it = keySet.iterator(); key = it.next(); } } while (result.length() < key.length()); return result; } /** * Does the command string contain wild cards? * * @param searchStr * @return true if wildcards present */ protected boolean isWildCarded(String searchStr){ return (searchStr.matches(".*[\\?|\\*].*")); //$NON-NLS-1$ } /** * Convert command string to simple regex * * @param searchStr * @return searchStr with simple wildcards replaced with regexp syntax */ protected String toRegex(String searchStr){ String result = searchStr; if (searchStr != null && isWildCarded(searchStr)) { result = searchStr.replace("*", ".*"); //$NON-NLS-1$ //$NON-NLS-2$ result = result.replace("?", "."); //$NON-NLS-1$ //$NON-NLS-2$ } return result; } }