blob: 3b0b219927b1b86af91973f861abfede6f4075b3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
* 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
*
* Contributors:
* IBM Corporation - initial API and implementation
* Felix Pahl (fpahl@web.de) - fixed https://bugs.eclipse.org/bugs/show_bug.cgi?id=51820
*******************************************************************************/
package org.eclipse.ui.texteditor;
import java.util.Stack;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IExecutionListener;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.internal.texteditor.NLSUtility;
/**
* An incremental find target. Replace is always disabled.
* @since 2.0
*/
class IncrementalFindTarget implements IFindReplaceTarget, IFindReplaceTargetExtension, VerifyKeyListener, MouseListener, FocusListener, ISelectionChangedListener, ITextListener, IExecutionListener {
/** The string representing rendered tab */
private final static String TAB= EditorMessages.Editor_FindIncremental_render_tab;
/**
* The string representing "Reverse Incremental Find"
* @since 3.0
*/
private final static String FIELD_NAME= EditorMessages.Editor_FindIncremental_name;
/**
* The string representing "Incremental Find"
* @since 3.0
*/
private final static String REVERSE_FIELD_NAME= EditorMessages.Editor_FindIncremental_reverse_name;
/**
* The string representing reverse
* @since 2.1
*/
private final static String REVERSE= EditorMessages.Editor_FindIncremental_reverse;
/**
* The string representing wrapped
* @since 2.1
*/
private final static String WRAPPED= EditorMessages.Editor_FindIncremental_wrapped;
/** The text viewer to operate on */
private final ITextViewer fTextViewer;
/** The status line manager for output */
private final IStatusLineManager fStatusLine;
/** The find replace target to delegate find requests */
private final IFindReplaceTarget fTarget;
/** The current find string */
private StringBuffer fFindString= new StringBuffer();
/** The position of the first upper case character, -1 if none */
private int fCasePosition;
/**
* The position in the stack of the first wrap search, -1 if none
* @since 2.1
*/
private int fWrapPosition;
/** The position of the last successful find */
private int fCurrentIndex;
/** A flag indicating if last find was successful */
private boolean fFound;
/**
* A flag indicating if the last search was forward
* @since 2.1
*/
private boolean fForward= true;
/** A flag indicating listeners are installed. */
private boolean fInstalled;
/**
* A flag indicating that a search is currently active.
* Used to ignore selection callbacks generated by the incremental search itself.
* @since 2.1
*/
private boolean fSearching;
/** The current find stack */
private Stack<SearchResult> fSessionStack;
/**
* The previous search string
* @since 2.1
*/
private String fPrevFindString= ""; //$NON-NLS-1$
/**
* The previous position of the first upper case character, -1 if none
* @since 3.0
*/
private int fPrevCasePosition= -1;
/**
* The find status field.
* @since 3.0
*/
private IStatusField fStatusField;
/**
* Tells whether the status field implements
* <code>IStatusFieldExtension</code>.
* @see IStatusFieldExtension
* @since 3.0
*/
private boolean fIsStatusFieldExtension;
/**
* Data structure for a search result.
* @since 2.1
*/
private class SearchResult {
int selection, length, index, findLength;
boolean found, forward;
/**
* Creates a new search result data object and fills
* it with the current values of this target.
*/
public SearchResult() {
Point p= fTarget.getSelection();
selection= p.x;
length= p.y;
index= fCurrentIndex;
findLength= fFindString.length();
found= fFound;
forward= fForward;
}
}
/**
* Stores the search result.
*/
private void saveState() {
fSessionStack.push(new SearchResult());
}
/**
* Restores the search result.
*
* @since 2.1
*/
private void restoreState() {
StyledText text= fTextViewer.getTextWidget();
if (text == null || text.isDisposed())
return;
SearchResult searchResult= null;
if (!fSessionStack.empty())
searchResult= fSessionStack.pop();
if (searchResult == null) {
text.getDisplay().beep();
return;
}
text.setSelectionRange(searchResult.selection, searchResult.length);
text.showSelection();
// relies on the contents of the StringBuffer
fFindString.setLength(searchResult.findLength);
fCurrentIndex= searchResult.index;
fFound= searchResult.found;
fForward= searchResult.forward;
// Recalculate the indices
if (fFindString.length() <= fCasePosition)
fCasePosition= -1;
if (fSessionStack.size() < fWrapPosition)
fWrapPosition= -1;
}
/**
* Sets the direction for the next search.
* This can be called before <code>beginSession</code> to set the initial search direction.
* @param forward <code>true</code> if the next search should be forward
* @see #beginSession()
* @since 2.1
*/
public void setDirection(boolean forward) {
fForward= forward;
}
/**
* Creates an instance of an incremental find target.
* @param viewer the text viewer to operate on
* @param manager the status line manager for output
*/
public IncrementalFindTarget(ITextViewer viewer, IStatusLineManager manager) {
Assert.isNotNull(viewer);
Assert.isNotNull(manager);
fTextViewer= viewer;
fStatusLine= manager;
fTarget= viewer.getFindReplaceTarget();
}
@Override
public boolean canPerformFind() {
return fTarget.canPerformFind();
}
@Override
public int findAndSelect(int offset, String findString, boolean searchForward, boolean caseSensitive, boolean wholeWord) {
return fTarget.findAndSelect(offset, findString, searchForward, caseSensitive, wholeWord);
}
@Override
public Point getSelection() {
return fTarget.getSelection();
}
@Override
public String getSelectionText() {
return fTarget.getSelectionText();
}
@Override
public boolean isEditable() {
return false;
}
@Override
public void replaceSelection(String text) {
}
@Override
public void beginSession() {
fSearching= true;
// Workaround since some accelerators get handled directly by the OS
if (fInstalled) {
saveState();
repeatSearch(fForward);
updateStatus();
fSearching= false;
return;
}
fFindString.setLength(0);
fSessionStack= new Stack<>();
fCasePosition= -1;
fWrapPosition= -1;
fFound= true;
// clear initial selection
StyledText text= fTextViewer.getTextWidget();
if (text != null && !text.isDisposed()) {
fCurrentIndex= text.getCaretOffset();
text.setSelection(fCurrentIndex);
} else {
fCurrentIndex= 0;
}
install();
// Set the mark
if (fTextViewer instanceof ITextViewerExtension) {
int modelOffset;
if (fTextViewer instanceof ITextViewerExtension5)
modelOffset= fCurrentIndex == -1 ? -1 : ((ITextViewerExtension5)fTextViewer).widgetOffset2ModelOffset(fCurrentIndex);
else
modelOffset= fCurrentIndex;
((ITextViewerExtension)fTextViewer).setMark(modelOffset);
}
updateStatus();
if (fTarget instanceof IFindReplaceTargetExtension)
((IFindReplaceTargetExtension) fTarget).beginSession();
fSearching= false;
}
@Override
public void endSession() {
if (fTarget instanceof IFindReplaceTargetExtension)
((IFindReplaceTargetExtension) fTarget).endSession();
// will uninstall itself
}
@Override
public IRegion getScope() {
return null;
}
@Override
public void setScope(IRegion scope) {
}
@Override
public void setReplaceAllMode(boolean replaceAll) {
}
/**
* Installs this target. I.e. adds all required listeners.
*/
private void install() {
if (fInstalled)
return;
StyledText text= fTextViewer.getTextWidget();
if (text == null)
return;
text.addMouseListener(this);
text.addFocusListener(this);
fTextViewer.addTextListener(this);
ISelectionProvider selectionProvider= fTextViewer.getSelectionProvider();
if (selectionProvider != null)
selectionProvider.addSelectionChangedListener(this);
if (fTextViewer instanceof ITextViewerExtension)
((ITextViewerExtension) fTextViewer).prependVerifyKeyListener(this);
else
text.addVerifyKeyListener(this);
ICommandService commandService= PlatformUI.getWorkbench().getAdapter(ICommandService.class);
if (commandService != null)
commandService.addExecutionListener(this);
fInstalled= true;
}
/**
* Uninstalls itself. I.e. removes all listeners installed in <code>install</code>.
*/
private void uninstall() {
fTextViewer.removeTextListener(this);
ISelectionProvider selectionProvider= fTextViewer.getSelectionProvider();
if (selectionProvider != null)
selectionProvider.removeSelectionChangedListener(this);
StyledText text= fTextViewer.getTextWidget();
if (text != null) {
text.removeMouseListener(this);
text.removeFocusListener(this);
}
if (fTextViewer instanceof ITextViewerExtension) {
((ITextViewerExtension) fTextViewer).removeVerifyKeyListener(this);
} else {
if (text != null)
text.removeVerifyKeyListener(this);
}
ICommandService commandService= PlatformUI.getWorkbench().getAdapter(ICommandService.class);
if (commandService != null)
commandService.removeExecutionListener(this);
fInstalled= false;
}
/**
* Updates the status line.
* @since 2.1
*/
private void updateStatus() {
if (!fInstalled)
return;
String string= fFindString.toString();
String wrapPrefix= fWrapPosition == -1 ? "" : WRAPPED; //$NON-NLS-1$
String reversePrefix= fForward ? "" : REVERSE; //$NON-NLS-1$
if (!fFound) {
String pattern= EditorMessages.Editor_FindIncremental_not_found_pattern;
statusError(NLSUtility.format(pattern, new Object[] { reversePrefix, wrapPrefix, string }));
} else if (string.length() == 0) {
if (fForward)
statusMessage(FIELD_NAME);
else
statusMessage(REVERSE_FIELD_NAME);
} else if (!fForward || fWrapPosition > -1) {
String pattern= EditorMessages.Editor_FindIncremental_found_pattern;
statusMessage(NLSUtility.format(pattern, new Object[] { reversePrefix, wrapPrefix, string }));
} else {
statusMessage(string);
}
}
@Override
public void verifyKey(VerifyEvent event) {
if (!event.doit)
return;
fSearching= true;
if (event.character == 0) {
switch (event.keyCode) {
// ALT, CTRL, ARROW_LEFT, ARROW_RIGHT == leave
case SWT.ARROW_LEFT:
case SWT.ARROW_RIGHT:
case SWT.HOME:
case SWT.END:
case SWT.PAGE_DOWN:
case SWT.PAGE_UP:
leave();
break;
case SWT.ARROW_DOWN:
saveState();
setDirection(true);
repeatSearch(fForward);
event.doit= false;
break;
case SWT.ARROW_UP:
saveState();
setDirection(false);
repeatSearch(fForward);
event.doit= false;
break;
}
// event.character != 0
} else {
switch (event.character) {
// ESC, CR = quit
case 0x1B:
case 0x0D:
leave();
event.doit= false;
break;
// backspace and delete
case 0x08:
case 0x7F:
restoreState();
event.doit= false;
break;
default:
int stateMask= event.stateMask;
if (stateMask == 0
|| stateMask == SWT.SHIFT
|| !Util.isMac() && stateMask == (SWT.ALT | SWT.CTRL) // AltGr (see bug 43049)
|| Util.isMac() && (stateMask == (SWT.ALT | SWT.SHIFT) || stateMask == SWT.ALT) ) { // special chars on Mac (bug 272994)
saveState();
addCharSearch(event.character);
event.doit= false;
}
break;
}
}
updateStatus();
fSearching= false;
}
/**
* Repeats the last search while possibly changing the direction.
*
* @param forward <code>true</code> iff the next search should be forward
* @return if the search was successful
* @since 2.1
*/
private boolean repeatSearch(boolean forward) {
if (fFindString.length() == 0) {
fFindString= new StringBuffer(fPrevFindString);
fCasePosition= fPrevCasePosition;
}
String string= fFindString.toString();
if (string.length() == 0) {
fFound= true;
return true;
}
StyledText text= fTextViewer.getTextWidget();
// Cannot use fTarget.getSelection since that does not return which side of the
// selection the caret is on.
int startIndex= text.getCaretOffset();
if (!forward)
startIndex -= 1;
// Check to see if a wrap is necessary
if (!fFound && (fForward == forward)) {
startIndex= -1;
if (fWrapPosition == -1)
fWrapPosition= fSessionStack.size();
}
fForward = forward;
// Find the string
text.setRedraw(false);
int index= fTarget.findAndSelect(startIndex, string, fForward, fCasePosition != -1, false);
// Set the caret on the left if the search is reversed
if (!forward) {
Point p= fTarget.getSelection();
text.setSelectionRange(p.x + p.y, -p.y);
p= null;
}
text.setRedraw(true);
// Take appropriate action
boolean found = (index != -1);
if (!found && fFound) {
text= fTextViewer.getTextWidget();
if (text != null && !text.isDisposed())
text.getDisplay().beep();
}
if (found)
fCurrentIndex= startIndex;
fFound= found;
return found;
}
/**
* Adds the given character to the search string and repeats the search with the last parameters.
*
* @param c the character to append to the search pattern
* @return <code>true</code> the search found a match
* @since 2.1
*/
private boolean addCharSearch(char c) {
// Add char to pattern
if (fCasePosition == -1 && Character.isUpperCase(c) && Character.toLowerCase(c) != c)
fCasePosition= fFindString.length();
fFindString.append(c);
String string= fFindString.toString();
StyledText text= fTextViewer.getTextWidget();
text.setRedraw(false);
int index= fTarget.findAndSelect(fCurrentIndex, string, fForward, fCasePosition != -1, false);
// Set the caret on the left if the search is reversed
if (!fForward) {
Point p= fTarget.getSelection();
text.setSelectionRange(p.x + p.y, -p.y);
}
text.setRedraw(true);
// Take appropriate action
boolean found = (index != -1);
if (!found && fFound) {
text= fTextViewer.getTextWidget();
if (text != null && !text.isDisposed())
text.getDisplay().beep();
}
fFound= found;
return found;
}
/**
* Leaves this incremental search session.
*/
private void leave() {
if (fFindString.length() != 0) {
fPrevFindString= fFindString.toString();
fPrevCasePosition= fCasePosition;
}
statusClear();
uninstall();
fSessionStack = null;
}
@Override
public void textChanged(TextEvent event) {
if (event.getDocumentEvent() != null)
leave();
}
/*
* @see MouseListener##mouseDoubleClick(MouseEvent)
*/
@Override
public void mouseDoubleClick(MouseEvent e) {
leave();
}
@Override
public void mouseDown(MouseEvent e) {
leave();
}
@Override
public void mouseUp(MouseEvent e) {
leave();
}
@Override
public void focusGained(FocusEvent e) {
leave();
}
@Override
public void focusLost(FocusEvent e) {
leave();
}
/**
* Sets the given string as status message, clears the status error message.
* @param string the status message
*/
private void statusMessage(String string) {
if (fStatusField != null) {
if (fIsStatusFieldExtension) {
((IStatusFieldExtension)fStatusField).setErrorText(null);
fStatusField.setText(escapeTabs(string));
((IStatusFieldExtension)fStatusField).setVisible(true);
fStatusLine.update(true);
} else {
fStatusLine.setErrorMessage(null);
fStatusField.setText(escapeTabs(string));
}
} else {
fStatusLine.setErrorMessage(null);
fStatusLine.setMessage(escapeTabs(string));
}
}
/**
* Sets the status error message, clears the status message.
* @param string the status error message
*/
private void statusError(String string) {
if (fStatusField != null) {
if (fIsStatusFieldExtension) {
((IStatusFieldExtension)fStatusField).setErrorText(escapeTabs(string));
fStatusField.setText(""); //$NON-NLS-1$
((IStatusFieldExtension)fStatusField).setVisible(true);
fStatusLine.update(true);
} else {
fStatusLine.setErrorMessage(escapeTabs(string));
fStatusField.setText(""); //$NON-NLS-1$
}
} else {
fStatusLine.setErrorMessage(escapeTabs(string));
fStatusLine.setMessage(null);
}
}
/**
* Clears the status message and the status error message.
*/
private void statusClear() {
if (fStatusField != null) {
if (fIsStatusFieldExtension) {
fStatusField.setText(""); //$NON-NLS-1$
((IStatusFieldExtension)fStatusField).setErrorText(null);
((IStatusFieldExtension)fStatusField).setVisible(false);
fStatusLine.update(true);
} else {
fStatusField.setText(""); //$NON-NLS-1$
fStatusLine.setErrorMessage(null);
}
} else {
fStatusLine.setErrorMessage(null);
fStatusLine.setMessage(null);
}
}
/**
* Translates all tab characters into a proper status line presentation.
* @param string the string in which to translate the tabs
* @return the given string with all tab characters replace with a proper status line presentation
*/
private String escapeTabs(String string) {
StringBuffer buffer= new StringBuffer();
int begin= 0;
int end= string.indexOf('\t', begin);
while (end >= 0) {
buffer.append(string.substring(begin, end));
buffer.append(TAB);
begin= end + 1;
end= string.indexOf('\t', begin);
}
buffer.append(string.substring(begin));
return buffer.toString();
}
@Override
public Point getLineSelection() {
if (fTarget instanceof IFindReplaceTargetExtension)
return ((IFindReplaceTargetExtension) fTarget).getLineSelection();
return null; // XXX: should not return null
}
@Override
public void setSelection(int offset, int length) {
if (fTarget instanceof IFindReplaceTargetExtension)
((IFindReplaceTargetExtension) fTarget).setSelection(offset, length);
}
@Override
public void setScopeHighlightColor(Color color) {
}
@Override
public void selectionChanged(SelectionChangedEvent e) {
boolean ignore= false;
ISelection selection= e.getSelection();
if (selection instanceof ITextSelection) {
ITextSelection textSelection= (ITextSelection)selection;
Point range= getSelection();
ignore= textSelection.getOffset() + textSelection.getLength() == range.x + range.y;
}
if (!fSearching && !ignore)
leave();
}
/**
* Sets the find status field for this incremental find target.
*
* @param statusField the status field
* @since 3.0
*/
void setStatusField(IStatusField statusField) {
fStatusField= statusField;
fIsStatusFieldExtension= fStatusField instanceof IStatusFieldExtension;
}
@Override
public void notHandled(String commandId, NotHandledException exception) {
}
@Override
public void postExecuteFailure(String commandId, ExecutionException exception) {
}
@Override
public void postExecuteSuccess(String commandId, Object returnValue) {
}
@Override
public void preExecute(String commandId, ExecutionEvent event) {
if (IWorkbenchActionDefinitionIds.FIND_INCREMENTAL.equals(commandId)
|| IWorkbenchActionDefinitionIds.FIND_INCREMENTAL_REVERSE.equals(commandId))
return;
leave();
}
}