/**
* Copyright (c) 2009, 2014 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.minibuffer;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.EmacsPlusActivator;
import com.mulgasoft.emacsplus.EmacsPlusUtils;
import com.mulgasoft.emacsplus.RingBuffer.IRingBufferElement;
import com.mulgasoft.emacsplus.execute.SelectionDialog;
import com.mulgasoft.emacsplus.execute.ISelectExecute;
// TODO support y-p -> yank-pop completion
/**
* Completion base class
*
* @author Mark Feber - initial API and implementation
*/
public abstract class CompletionMinibuffer extends ExecutingMinibuffer implements IPartListener, ISelectExecute {
static final String SEARCH_PREFIX = EmacsPlusActivator.getResourceString("IS_Forward"); //$NON-NLS-1$
protected abstract SortedMap<String,?> getCompletions();
final static String RWILD = ".*"; //$NON-NLS-1$
final static String prePre = " ("; //$NON-NLS-1$
final static String prePost = ")"; //$NON-NLS-1$
// Flag whether we are searching
private boolean isSearching = false;
// Hold the text of the search string entered by the user
private StringBuilder searchStr = null;
// Hold the of array of (ordered) buffers that match the search string
private String[] searchArray = null;
// Index into searchArray
private int searchIndex = -1;
// Direction of search
private int searchDir = 1;
// Remember last successful search string
static String lastSearch = null;
// Hold the full search results
private SortedMap<String,?> searchResults = null;
// Remember if we were showing completions during search
private boolean showingCompletions = false;
private SelectionDialog miniDialog = null;
/**
* @param executable
*/
public CompletionMinibuffer(IMinibufferExecutable executable) {
super(executable);
}
/**
* @return the miniDialog
*/
protected SelectionDialog getMiniDialog() {
return miniDialog;
}
/**
* @param miniDialog the miniDialog to set
*/
protected void setMiniDialog(SelectionDialog miniDialog) {
this.miniDialog = miniDialog;
}
protected void closeDialog() {
showingCompletions = false;
try {
if (miniDialog != null) {
// miniDialog.shutdown();
miniDialog.close();
miniDialog = null;
}
} catch (Exception e) {}
}
private boolean mouseInDialog() {
boolean result = false;
if (miniDialog != null) {
result = miniDialog.mouseIn();
}
return result;
}
protected boolean isCompleting() {
return true;
}
boolean isSearching() {
return isSearching;
}
String getSearchStr() {
return (searchStr == null ? EMPTY_STR : searchStr.toString());
}
void setShowingCompletions(boolean value) {
showingCompletions = value;
}
boolean isShowingCompletions() {
return showingCompletions;
}
SortedMap<String,?> getSearchResults() {
return searchResults;
}
String getSearchResult() {
String result = null;
if (searchIndex > -1) {
result = searchArray[searchIndex];
}
return result;
}
protected String getCompletionMinibufferPrefix() {
String result = super.getMinibufferPrefix();
if (isSearching()) {
result = getSearchingPrefix() + ' ' + result;
}
return result;
}
protected String getSearchingPrefix() {
String str = getSearchStr();
return '(' + SEARCH_PREFIX + ((str != null && str.length() > 0) ? ": " + str : EMPTY_STR) + ')';
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#showCompletions()
*/
@Override
protected void showCompletions() {
String name = getMBString();
SortedMap<String, ?> compTree;
if (isSearching() && getSearchStr().length() > 0) {
compTree = getSearchResults();
} else {
compTree = getCompletions(name);
}
if (compTree != null) {
if (compTree.size() > 1) {
if (getMiniDialog() == null) {
setMiniDialog(new SelectionDialog(null, this, getEditor()));
}
((SelectionDialog) getMiniDialog()).open(compTree);
setShowingCompletions(true);
}
EmacsPlusUtils.forceStatusUpdate(getEditor());
showCompletionStatus(compTree,name);
} else {
updateStatusLine(name + NOMATCH_MSG);
}
}
/**
* Update the status line/minibuffer text based on the content of the completion tree
*
* @param compTree the Map of the available completions
* @param name the contents of the minibuffer so far
*/
protected void showCompletionStatus(SortedMap<String, ?> compTree, String name) {
String newName;
if (compTree.size() == 0) {
updateStatusLine((isSearching() ? EMPTY_STR : name) + NOMATCH_MSG);
} else if (compTree.size() == 1) {
closeDialog();
newName = compTree.firstKey();
if (!name.equals(newName) && !isSearching()) {
initMinibuffer(newName);
}
updateStatusLine(newName + COMPLETE_MSG);
} else if (!isSearching()) {
if (name.length() > 0) {
newName = getCommonString(compTree.keySet(),name);
if (!name.equals(newName)) {
initMinibuffer(newName);
}
}
updateStatusLine(getMBString());
}
}
protected SortedMap<String, ?> getCompletions(String searchSubstr) {
return getCompletions(searchSubstr, false, false);
}
protected SortedMap<String, ?> getCompletions(String searchSubstr, boolean insensitive, boolean regex) {
SortedMap<String, ?> result = getCompletions();
if (searchSubstr != null && result != null) {
Set<String> keySet = result.keySet();
String searchStr = (regex ? searchSubstr : toRegex(searchSubstr));
boolean isRegex = (regex || isRegex(searchStr,searchSubstr));
if (insensitive || isRegex) {
try {
SortedMap<String,? super Object> regResult = new TreeMap<String, Object>();
Pattern pat = Pattern.compile(searchStr + RWILD, (insensitive ? Pattern.CASE_INSENSITIVE : 0)); //$NON-NLS-1$
// we have to build the map up one by one on regex search
for (String key : keySet) {
if (pat.matcher(key).matches()) {
regResult.put(key, result.get(key));
}
}
result = regResult;
} catch (Exception e) {
// ignore any PatternSyntaxException
}
if (result.size() == 0 && !insensitive) {
// try non-regex lookup
result = getCompletions(searchSubstr, keySet);
}
} else {
result = getCompletions(searchSubstr, keySet);
}
if ((result == null || result.size() == 0) && !insensitive) {
// try once with case insensitivity
return getCompletions(searchSubstr, true, regex);
}
}
return result;
}
/**
* Walk the buffer list looking for matches with subString on initial characters
*
* @param subString
* @param result
* @param keySet
* @return subsection of map, each of whose entries start with subString
*/
private SortedMap<String, ?> getCompletions(String subString, Set<String> keySet) {
SortedMap<String, ?> result = null;
String fromKey = null;
String toKey = null;
for (String key : keySet) {
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 = getCompletions().tailMap(fromKey);
} else {
result = getCompletions().subMap(fromKey, toKey);
}
}
return result;
}
/**
* Determine the longest common command name substring that starts with the current substring
*
* @param keySet
* @param subString
* @return the longest common name
*/
protected String getCommonString(Set<String> keySet, String subString) {
String result = (isWildCarded(subString) ? EMPTY_STR : subString);
Iterator<String> it = keySet.iterator();
String key;
String possible;
key = it.next();
do {
if (key.length() > result.length()) {
// advance possible by on character
possible = key.substring(0, result.length()+1);
// check the rest to see if they all have the new possible
while (it.hasNext()) {
key = it.next();
if (!key.startsWith(possible)) {
return result;
}
}
result = possible;
// reset the iterator and re-initialize key
it = keySet.iterator();
key = it.next();
}
} while (result.length() < key.length());
return result;
}
protected void executeCR(VerifyEvent event) {
if (isSearching()){
String key = getSearchResult();
if (key != null) {
// save search characters
lastSearch = searchStr.toString();
resetSearch();
// and put full key into buffer for execution
setMBString(key);
} else {
// Mimic Emacs by restoring to initial state, but not leaving
setMBString(EMPTY_STR);
event.doit = false;
resetSearch();
return;
}
}
super.executeCR(event);
}
/**
* Turn off searching before invoking history
*
* @see com.mulgasoft.emacsplus.minibuffer.HistoryMinibuffer#replaceFromHistory(com.mulgasoft.emacsplus.RingBuffer.IRingBufferElement)
*/
protected void replaceFromHistory(IRingBufferElement<?> rbe) {
if (rbe != null && isSearching()) {
resetSearch();
}
super.replaceFromHistory(rbe);
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#handlesTab()
*/
@Override
protected boolean handlesTab() {
return true;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#dispatchTab(org.eclipse.swt.events.VerifyEvent)
*/
protected boolean dispatchTab(VerifyEvent event) {
// handle tab after disabling tab traversal
showCompletions();
return false;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#handlesCtrl()
*/
@Override
protected boolean handlesCtrl() {
return true;
}
/**
* Support ^s and ^r for searching within buffer list and ^g to break out of search
*
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#dispatchCtrl(org.eclipse.swt.events.VerifyEvent)
*/
@Override
protected boolean dispatchCtrl(VerifyEvent event) {
boolean result = false;
switch (event.keyCode) {
case 's':
forwardSearch();
break;
case 'r':
backwardSearch();
break;
case 'k':
// revert to initial state
resetSearch();
break;
case 'g':
if (isSearching) {
resetSearch();
break;
}
// otherwise, ^g interrupts
default:
leave();
result = true;
}
return result;
}
/**
* Turn off searching and reset state
*/
protected void resetSearch() {
isSearching = false;
searchStr = null;
searchArray = null;
searchResults = null;
searchIndex = -1;
if (showingCompletions) {
closeDialog();
}
updateStatusLine(getMBString());
}
private void forwardSearch() {
searchDir = 1;
search();
}
private void backwardSearch() {
searchDir = -1;
search();
}
/**
* Turn on searching, or continue through array if already searching
*/
private void search() {
if (!isSearching) {
isSearching = true;
searchStr = new StringBuilder();
// add any text so far
if (getMB().getLength() > 0) {
searchStr.append(getMBString());
}
updateSearch();
} else if (searchArray != null && searchArray.length > 0) {
updateStatusLine(searchArray[getNextIndex()]);
} else if (lastSearch != null) {
searchStr = new StringBuilder();
searchStr.append(lastSearch);
setMBString(lastSearch);
updateSearch();
}
}
private int getNextIndex() {
searchIndex += searchDir;
if (searchIndex >= searchArray.length) {
searchIndex = 0;
} else if (searchIndex < 0) {
searchIndex = searchArray.length -1;
}
return searchIndex;
}
/**
* Recompute the search results
*/
private void updateSearch() {
searchIndex = -1;
searchArray = null;
searchResults = null;
if (searchStr.length() > 0) {
searchResults = getCompletions(searchStr.toString());
if (searchResults != null && !searchResults.isEmpty()) {
searchArray = new String[searchResults.size()];
searchResults.keySet().toArray(searchArray);
updateStatusLine(searchArray[getNextIndex()]);
if (showingCompletions) {
closeDialog();
showCompletions();
}
} else {
updateStatusLine(EMPTY_STR);
beep();
}
} else {
updateStatusLine(EMPTY_STR);
}
}
/**
* When searching, add a character to the search string
*
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#addIt(org.eclipse.swt.events.VerifyEvent)
*/
protected void addIt(VerifyEvent event) {
if (isSearching) {
searchStr.append(event.character);
updateSearch();
event.doit = false;
} else {
super.addIt(event);
}
}
/**
* When searching, remove a character from the search string
*
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#backSpaceChar(org.eclipse.swt.events.VerifyEvent)
*/
protected void backSpaceChar(VerifyEvent event) {
if (isSearching) {
int index = searchStr.length();
if (index > 0) {
searchStr.deleteCharAt(index-1);
updateSearch();
} else {
beep();
}
event.doit = false;
} else {
super.backSpaceChar(event);
}
}
// Regex/wildcard support
/**
* 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$
}
protected boolean isRegex(String searchStr, String subStr) {
boolean result = false;
if (!searchStr.equals(subStr)) {
result = true;
} else
for (char c : searchStr.toCharArray()) {
// Always treat . as a path separator
if (c != '.' && !Character.isJavaIdentifierPart(c)) {
result = true;
break;
}
}
return result;
}
/**
* Convert command string to simple regexp
*
* @param searchStr
* @return regexp string
*/
protected String toRegex(String searchStr){
String result = searchStr;
if (searchStr != null && isWildCarded(searchStr)) {
result = searchStr.replaceAll("([^.]|^)\\*","$1.*"); //$NON-NLS-1$ //$NON-NLS-2$
result = result.replace('?', '.');
}
return result;
}
// Listener support
/**
* @see com.mulgasoft.emacsplus.minibuffer.HistoryMinibuffer#addOtherListeners(IWorkbenchPage, ISourceViewer, StyledText)
*/
@Override
protected void addOtherListeners(IWorkbenchPage page, ISourceViewer viewer, StyledText widget) {
if (page != null) {
page.addPartListener(this);
}
super.addOtherListeners(page, viewer, widget);
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.HistoryMinibuffer#removeOtherListeners(IWorkbenchPage, ISourceViewer, StyledText)
*/
@Override
protected void removeOtherListeners(IWorkbenchPage page, ISourceViewer viewer, StyledText widget) {
if (page != null) {
page.removePartListener(this);
}
super.removeOtherListeners(page, viewer, widget);
}
// ISelectionChangedListener
public void selectionChanged(SelectionChangedEvent event) {
leave();
}
/**
* @see FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
*/
@Override
public void focusGained(FocusEvent e) {
if (!isExecuting()) {
leave();
}
}
/**
* @see FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
*/
public void focusLost(FocusEvent e) {
// check if focus was lost to anything but the completions dialog
if (!isShowingCompletions() || !mouseInDialog()) {
leave(true);
}
}
// TraverseListener - enable tab completion
/**
* Enable tab completion by disabling tab traversal
*
* @see com.mulgasoft.emacsplus.minibuffer.HistoryMinibuffer#keyTraversed(org.eclipse.swt.events.TraverseEvent)
*/
public void keyTraversed(TraverseEvent e) {
switch (e.detail) {
case SWT.TRAVERSE_TAB_NEXT:
// ignore tab traversal
e.doit = false;
break;
default:
super.keyTraversed(e);
}
}
// Part activation handles the case where we want the dialog to stay open for
// double click (which gives it focus), but it should be removed if focus goes
// to any other part (which we can detect on part deactivation)
/**
* @see org.eclipse.ui.IPartListener#partActivated(org.eclipse.ui.IWorkbenchPart)
*/
public void partActivated(IWorkbenchPart part) {
}
/**
* @see org.eclipse.ui.IPartListener#partBroughtToTop(org.eclipse.ui.IWorkbenchPart)
*/
public void partBroughtToTop(IWorkbenchPart part) {
}
/**
* If we are the part deactivated, then time to go
*
* @param part
*/
private void checkDeactivation(IWorkbenchPart part) {
// getAdapter gets the correct part when dealing with multi-page, otherwise identity
if ((IWorkbenchPart) part.getAdapter(ITextEditor.class) == getEditor() || (IWorkbenchPart)getEditor() == part){
leave(true);
}
}
/**
* @see org.eclipse.ui.IPartListener#partClosed(org.eclipse.ui.IWorkbenchPart)
*/
public void partClosed(IWorkbenchPart part) {
}
/**
* @see org.eclipse.ui.IPartListener#partDeactivated(org.eclipse.ui.IWorkbenchPart)
*/
public void partDeactivated(IWorkbenchPart part) {
checkDeactivation(part);
}
/**
* @see org.eclipse.ui.IPartListener#partOpened(org.eclipse.ui.IWorkbenchPart)
*/
public void partOpened(IWorkbenchPart part) {
}
}