/**
* 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.minibuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.EmacsPlusActivator;
import com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds;
import com.mulgasoft.emacsplus.MarkUtils;
import com.mulgasoft.emacsplus.RingBuffer.IRingBufferElement;
/**
* Support regular and regexp i-search minibuffer processing.
* Used directly by: isearch-forward, isearch-backward, isearch-forward-regexp, isearch-backward-regexp
*
* Support typing `M-c' within an incremental search to toggle the case sensitivity of that search.
* The effect does not extend beyond the current incremental search to the next one, but it does override
* the effect of adding or removing an upper-case letter in the current search.
*
* @author Mark Feber - initial API and implementation
*/
public class ISearchMinibuffer extends SearchMinibuffer {
private static final String IS_WRAP_MARKER = EmacsPlusActivator.getResourceString("IS_Wrap_Marker"); //$NON-NLS-1$
private static final String IS_NOT_FOUND = EmacsPlusActivator.getResourceString("IS_Not_Found"); //$NON-NLS-1$
private static final String IS_FORWARD = EmacsPlusActivator.getResourceString("IS_Forward"); //$NON-NLS-1$
private static final String IS_BACKWARD = EmacsPlusActivator.getResourceString("IS_Backward"); //$NON-NLS-1$
private static final String IS_REGEXP = EmacsPlusActivator.getResourceString("IS_Regexp"); //$NON-NLS-1$
private final static char SPACE = ' ';
private static final String IS_BACKWARD_P = IS_FORWARD + SPACE + IS_BACKWARD ;
private static final String IS_FORWARD_REGEXP_P = IS_REGEXP + SPACE + IS_FORWARD;
private static final String IS_BACKWARD_REGEXP_P = IS_REGEXP + SPACE + IS_BACKWARD_P;
private static final String ISF = IEmacsPlusCommandDefinitionIds.ISEARCH_FORWARD;
private static final String ISB = IEmacsPlusCommandDefinitionIds.ISEARCH_BACKWARD;
private static final String ISRF = IEmacsPlusCommandDefinitionIds.ISEARCH_REGEXP_FORWARD;
private static final String ISRB = IEmacsPlusCommandDefinitionIds.ISEARCH_REGEXP_BACKWARD;
private static final int FORWARD = -1;
private static final int REVERSE = -2;
private boolean historyUpdated = false;
private boolean checkHistoryUpdated = false;
// hash table to cache current bindings of i-search(regexp)-forward/backward commands
private Map<Integer,Integer> keyHash = new HashMap<Integer,Integer>();
// temporary flag for prefix computation
boolean isAdding = false;
/**
* Default is forward, not regexp
*/
public ISearchMinibuffer() {
this(true, false);
}
/**
* @param forward - true if searching forward
* @param regexp - true if regexp searching
*/
public ISearchMinibuffer(boolean forward, boolean regexp) {
super(forward, regexp);
}
protected void addIt(VerifyEvent event, boolean searcher) {
try {
isAdding = true;
super.addIt(event, searcher);
} finally {
isAdding = false;
updateStatusLine();
}
}
private String getSearchPrefix() {
String result;
if (isForward()) {
result = (isRegexp() ? IS_FORWARD_REGEXP_P : IS_FORWARD);
} else {
result = (isRegexp() ? IS_BACKWARD_REGEXP_P : IS_BACKWARD_P);
}
return result;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.ExecutingMinibuffer#getMinibufferPrefix()
*/
public String getMinibufferPrefix() {
boolean notFound = (!isAdding && !isFound() && getMBLength() > 0);
String prefix = (notFound ? IS_NOT_FOUND : (isWrapped() ? IS_WRAP_MARKER : EMPTY_STR)) + getSearchPrefix();
return prefix + KOLON;
}
protected void setWrapPosition(){
super.setWrapPosition();
updateStatusLine();
}
private void setHistoryUpdated(boolean val) {
historyUpdated = val;
}
boolean wasHistoryUpdated() {
return checkHistoryUpdated;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#backSpaceChar(org.eclipse.swt.events.VerifyEvent)
*/
protected void backSpaceChar(VerifyEvent event) {
popSearchState();
event.doit = false;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#deleteChar(org.eclipse.swt.events.VerifyEvent)
*/
protected void deleteChar(VerifyEvent event) {
popSearchState();
event.doit = false;
}
/**
* Emacs style cancel:
* "Ctrl-g: while searching or when search has failed cancels input back to what has been found successfully.
* when search is successful aborts and moves point to starting point.";
*/
protected boolean cancelSearch() {
if (!goToFoundState()) {
addToHistory(getMBString(),getRXString());
leave(getStartOffset());
beep();
}
return true;
}
protected void leave() {
// don't set mark if we're back at start
if (isFound() || getStartOffset() != getTextWidget().getCaretOffset()) {
MarkUtils.setMark(getEditor(),getMarkOffset());
}
super.leave();
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#replaceFromHistory(com.mulgasoft.emacsplus.RingBuffer.IRingBufferElement)
*/
@Override
protected void replaceFromHistory(IRingBufferElement<?> history) {
try {
setFound(false); // on incremental search, history update invalidates found state
isAdding = true; // flag search string change for minibuffer prefix computation
super.replaceFromHistory(history);
if (history != null) {
setHistoryUpdated(true);
}
} finally {
isAdding = false;
}
}
private boolean isSpecial(VerifyEvent event) {
return ((event.stateMask & SWT.MODIFIER_MASK) == 0 &&
((event.keyCode == SWT.ALT ) || (event.keyCode == SWT.CTRL) || (event.keyCode == SWT.SHIFT)));
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#handleKey(org.eclipse.swt.events.VerifyEvent)
*/
@Override
protected void handleKey(VerifyEvent event) {
// ensure flag is only enabled for one round
// to allow CR after history update
if (!isSpecial(event) && (checkHistoryUpdated = historyUpdated)) {
setHistoryUpdated(false);
}
super.handleKey(event);
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#findNext(java.lang.String)
*/
@Override
protected boolean findNext(String searchStr) {
boolean wasFound = isFound();
boolean result = super.findNext(searchStr);
if (!result && wasFound) {
updateStatusLine();
}
return result;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#executeResult(org.eclipse.ui.texteditor.ITextEditor, java.lang.Object)
*/
@Override
protected boolean executeResult(ITextEditor editor, Object commandResult) {
if (wasHistoryUpdated()) {
findNext(getSearchString());
return false;
} else {
return super.executeResult(editor, commandResult);
}
}
private boolean repeatSearch(boolean direction) {
boolean result = true;
try {
setSearching(true);
saveState();
String searchStr = getSearchString();
if (searchStr == null || searchStr.length() == 0) {
// if empty, try to initialize search string on repeat
replaceFromHistory();
searchStr = getSearchString();
if (searchStr == null || searchStr.length() == 0) {
return result;
}
} else {
addToHistory(getMBString(), getRXString());
if (isFound() || wasHistoryUpdated()) {
if (isForward()) {
Point p = getSelection();
// get (actual or implicit) width to increment offset
if (p != null ){
setSearchOffset(getSearchOffset()+ (p.y == 0 ? getEol().length() : p.y));
}
}
} else {
// its a wrap
setSearchOffset(WRAP_INDEX);
if (!isWrapped()) {
setWrapPosition();
}
}
}
findNext(searchStr);
} finally {
setSearching(false);
updateStatusLine();
}
return result;
}
boolean wasFound = true;
boolean quoteIt = false;
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#findNext(java.lang.String, boolean)
*/
@Override
protected boolean findNext(String searchStr, boolean addit) {
boolean result = super.findNext(searchStr, addit);
if (!result && getMBLength() > 0) {
// ignore regexp syntax failures as we build string
boolean skipIt = addit && !checkRegexp();
if (wasFound && (!skipIt && !quoteIt)) {
beep();
wasFound = false;
}
if (!skipIt) {
quoteIt = false;
}
} else {
wasFound = true;
}
return result;
}
/**
* Sanity check the regular expression
*
* @return true if not regexp or is regexp and pattern compiles, else false
*/
private boolean checkRegexp() {
boolean result = true;
if (isRegexp()) {
try {
Pattern.compile(getSearchString());
} catch (Exception e) {
result = false;
}
}
return result;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#dispatchCtrl(org.eclipse.swt.events.VerifyEvent)
*/
@Override
protected boolean dispatchCtrl(VerifyEvent event) {
boolean result = true;
switch (checkKeyCode(event)) {
case FORWARD:
forwardSearch();
break;
case REVERSE:
reverseSearch();
break;
default:
result = super.dispatchCtrl(event);
}
return result;
}
/**
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#dispatchAlt(org.eclipse.swt.events.VerifyEvent)
*/
protected boolean dispatchAlt(VerifyEvent event) {
boolean result = true;
switch (checkKeyCode(event)) {
case FORWARD:
forwardSearch();
break;
case REVERSE:
reverseSearch();
break;
case CASE:
toggleCase(super.isCaseSensitive());
break;
default:
result = super.dispatchAlt(event);
}
return result;
}
/**
* Check for C-M bindings for the i-search forward/reverse commands
*
* @see com.mulgasoft.emacsplus.minibuffer.SearchMinibuffer#dispatchAltCtrl(org.eclipse.swt.events.VerifyEvent)
*/
@Override
protected boolean dispatchAltCtrl(VerifyEvent event) {
boolean result = true;
switch (checkKeyCode(event)) {
case FORWARD:
forwardSearch();
break;
case REVERSE:
reverseSearch();
break;
default:
result = super.dispatchAltCtrl(event);
}
return result;
}
private void forwardSearch() {
if (!isForward()){
if (isFound()) {
setSearchOffset(getSearchOffset() + getMBLength());
}
setForward(true);
updateStatusLine();
}
repeatSearch(isForward());
}
private void reverseSearch() {
if (isForward()){
if (isFound()) {
setSearchOffset(getSearchOffset() - getMBLength());
}
setForward(false);
updateStatusLine();
}
repeatSearch(isForward());
}
/**
* Dynamically determine the bindings for forward and reverse i-search
* For repeat search, Emacs treats i-search and i-search-regexp identically
*
* @param event
* @return the FORWARD, REVERSE, or the source keyCode
*/
private int checkKeyCode(VerifyEvent event) {
int result = event.keyCode;
Integer val = keyHash.get(Integer.valueOf(event.keyCode + event.stateMask));
if (val == null) {
KeyStroke keyst = KeyStroke.getInstance(event.stateMask, Character.toUpperCase(result));
IBindingService bindingService = getBindingService();
Binding binding = bindingService.getPerfectMatch(KeySequence.getInstance(keyst));
if (binding != null) {
if (ISF.equals(binding.getParameterizedCommand().getId())){
result = FORWARD;
keyHash.put(Integer.valueOf(event.keyCode + event.stateMask),Integer.valueOf(FORWARD));
} else if (ISB.equals(binding.getParameterizedCommand().getId())) {
result = REVERSE;
keyHash.put(Integer.valueOf(event.keyCode + event.stateMask),Integer.valueOf(REVERSE));
} else if (ISRF.equals(binding.getParameterizedCommand().getId())) {
result = FORWARD;
keyHash.put(Integer.valueOf(event.keyCode + event.stateMask),Integer.valueOf(FORWARD));
} else if (ISRB.equals(binding.getParameterizedCommand().getId())) {
result = REVERSE;
keyHash.put(Integer.valueOf(event.keyCode + event.stateMask),Integer.valueOf(REVERSE));
}
}
} else {
result = val;
}
return result;
}
}