/**
* 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.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.EmacsPlusUtils;
import com.mulgasoft.emacsplus.RingBuffer;
import com.mulgasoft.emacsplus.execute.BufferDialog;
/**
* Implement switch-to-buffer minibuffer
* Support wild cards, regex's, completion and i-search within buffer list
*
* @author Mark Feber - initial API and implementation
*/
public class SwitchMinibuffer extends CompletionMinibuffer {
private TreeMap<String, BufRef> bufferMap = null;
private IEditorReference defaultFile = null;
private String defaultFilePrefix = null;
private IWorkbenchPage page = null;
private String prefix = null;
private boolean withSelf = false;
private TreeMap<String, BufRef> getBufferMap() {
return bufferMap;
}
/**
* @param executable
*/
public SwitchMinibuffer(IMinibufferExecutable executable) {
super(executable);
}
public SwitchMinibuffer(IMinibufferExecutable executable, boolean withSelf) {
this(executable);
this.withSelf = withSelf;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#initializeBuffer(org.eclipse.ui.texteditor.ITextEditor, org.eclipse.ui.IWorkbenchPage)
*/
@Override
protected boolean initializeBuffer(ITextEditor editor, IWorkbenchPage page) {
this.page = page;
EmacsPlusUtils.clearMessage(editor);
List<BufRef> refs = setupRefs();
defaultFile = refs.get(0).getRef();
defaultFilePrefix = prePre + defaultFile.getName() + prePost;
setHistoryRing(refs);
return true;
}
/**
* Populate prefix with default buffer if possible
*
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#getMinibufferPrefix()
*/
@Override
protected String getMinibufferPrefix() {
if (prefix == null) {
prefix = super.getMinibufferPrefix();
}
String postFix = defaultFilePrefix;
if (isSearching()) {
postFix = ' ' + getSearchingPrefix();
} else if (getMBLength() > 0) {
postFix = EMPTY_STR;
}
return String.format(prefix, postFix);
}
protected void resetSearch() {
setMBString(EMPTY_STR);
super.resetSearch();
}
/**
* Convert buffer name to editor object before call back to command executable
*
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#executeResult(org.eclipse.ui.texteditor.ITextEditor, java.lang.Object)
*/
@Override
protected boolean executeResult(ITextEditor editor, Object minibufferResult) {
String bufferName = (String)minibufferResult;
IEditorReference result = null;
if (bufferName != null && bufferName.length() > 0) {
// get the editor reference by name
BufRef buf = getBufferMap().get(bufferName);
result = buf != null ? buf.getRef() : null;
if (result == null) {
try {
// Attempt auto-completion if name fetch failed
SortedMap<String, BufRef> viewTree = getBuffers(bufferName,false, false);
if (viewTree.size() == 1) {
bufferName = viewTree.firstKey();
result = viewTree.get(bufferName).getRef();
}
} catch (Exception e) {
// Could be a java.util.regex.PatternSyntaxException on weird input
// when looking for a match; just ignore and command will abort
}
}
} else if (defaultFile != null) {
result = defaultFile;
}
if (result != null) {
addToHistory(result.getName());
setExecuting(true);
// remember the executable as getEditor(true), if it has to restore the editor,
// will automatically give it focus, which results in a call to out focusLost....
IMinibufferExecutable ex = getExecutable();
super.executeResult(editor, ex, result.getEditor(true));
} else {
beep();
}
return true;
}
/**
* Enable history commands
*
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#handlesAlt()
*/
@Override
protected boolean handlesAlt() {
return true;
}
protected SortedMap<String,?> getCompletions(String searcher) {
return getBuffers(RWILD + searcher + RWILD, false, true);
}
protected SortedMap<String,?> getCompletions() {
return getCompletions(getSearchStr());
}
/**
* Convert the platforms array of editor references in activation order into
* structures used by the history and completion mechanisms
*
* @return the List of converted references
*/
private List<BufRef> setupRefs() {
List<BufRef> buffers = new ArrayList<BufRef>();
if (bufferMap == null) {
IEditorReference[] tmp = EmacsPlusUtils.getSortedEditors(page);
this.page = null;
if (tmp == null || tmp.length <= 1) {
leave(true);
} else {
bufferMap = new TreeMap<String,BufRef>();
Set<BufRef> checkSet = new HashSet<BufRef>();
BufRef collider = null;
for (int i=(withSelf ? 0 : 1); i< tmp.length; i++) {
BufRef rr = new BufRef(tmp[i].getName().trim(), tmp[i]);
if ((collider = bufferMap.get(rr.getName())) != null) {
checkSet.add(collider);
rr.setName(fixName(rr.getName(),rr.getRef()));
}
bufferMap.put(rr.getName(), rr);
buffers.add(rr);
}
// fix up the colliders
Iterator<BufRef> it = checkSet.iterator();
while (it.hasNext()) {
BufRef rr = it.next();
bufferMap.remove(rr.getName());
rr.setName(fixName(rr.getName(),rr.getRef()));
bufferMap.put(rr.getName(), rr);
}
}
}
return buffers;
}
/**
* When there are multiple buffers with the same name, disambiguate
* by adding the title tool tip to the string we're going to use
*
* @param name - the name that collides with others
* @param ref - the editor reference object
* @return a disambiguated name
*/
private String fixName(String name, IEditorReference ref) {
String result = name;
String tip = ref.getTitleToolTip();
if (tip != null) {
String subPart;
int sublen = tip.length() - (result.length() + 1);
// some plugins construct inconsiderate (empty) names
if (sublen < 0) {
subPart = ref.getTitle();
} else {
// remove the buffer name part of the tip
subPart = tip.substring(0, sublen);
}
result = result + '(' + subPart + ')';
}
return result;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#showCompletions()
*/
@Override
protected void showCompletions() {
String name = getMBString();
SortedMap<String,?> bufferTree;
if (isSearching() && getSearchStr().length() > 0) {
bufferTree = getSearchResults();
} else {
bufferTree = getBuffers(name,false,false);
}
if (bufferTree == null) {
beep();
leave();
} else {
if (bufferTree.size() > 1) {
if (getMiniDialog() == null) {
setMiniDialog(new BufferDialog(null, this, getEditor()));
}
((BufferDialog)getMiniDialog()).open(bufferTree);
setShowingCompletions(true);
}
showCompletionStatus(bufferTree,name);
}
}
/**
* Compute the set of buffers that match the subString
* subString may be null, initial text, or wildcarded (* , ?)
* If no match on initial pass, try with case insensitivity
*
* @param subString
* @return a SortedMap of buffers that match
*/
private SortedMap<String,BufRef> getBuffers(String subString, boolean insensitive, boolean regex) {
SortedMap<String,BufRef> result = null;
if (subString != null && subString.length() > 0) {
result = new TreeMap<String, BufRef>();
Set<String> keySet = getBufferMap().keySet();
String searchStr = (regex ? subString : toRegex(subString));
boolean isRegex = (regex || isRegex(searchStr,subString));
if (insensitive || isRegex) {
try {
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()) {
BufRef c = getBufferMap().get(key);
result.put(key, c);
}
}
} catch (Exception e) {
// ignore any PatternSyntaxException
}
if (result.size() == 0 && !insensitive) {
// try non-regex lookup
result = getSubBuffers(subString, keySet);
}
} else {
result = getSubBuffers(subString, keySet);
}
if ((result == null || result.size() == 0) && !insensitive) {
// try once with case insensitivity
return getBuffers(subString, true, regex);
}
} else {
result = getBufferMap();
}
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, BufRef> getSubBuffers(String subString, Set<String> keySet) {
SortedMap<String, BufRef> 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 = getBufferMap().tailMap(fromKey);
} else {
result = getBufferMap().subMap(fromKey, toKey);
}
}
return result;
}
/**
* @see com.mulgasoft.emacsplus.execute.ISelectExecute#execute(java.lang.Object)
*/
public void execute(Object selection) {
String key = (String)selection;
setExecuting(true);
executeResult(getEditor(),key);
leave(true);
}
private class BufRef {
private IEditorReference ref;
private String name;
BufRef(String name, IEditorReference ref) {
this.name = name;
this.ref = ref;
}
void setName(String name) {
this.name = name;
}
String getName() {
return name;
}
IEditorReference getRef() {
return ref;
}
public String toString() {
return getName();
}
}
/**** Local RingBuffer ****/
/**
* Initialize with the current buffer set
*
* @see com.mulgasoft.emacsplus.minibuffer.HistoryMinibuffer#getHistoryRing()
*/
private RingBuffer<BufRef> ring = null;
private void setHistoryRing(List<BufRef> refs) {
ring = new RingBuffer<BufRef>(refs);
ring.setInfiniteLoop(false);
}
@Override
@SuppressWarnings("unchecked")
protected RingBuffer<BufRef> getHistoryRing() {
return ring;
}
}