blob: 3dcecb79521b2260e85473f5bf6f69341817d4dd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.editor.search;
import java.awt.Insets;
import java.awt.Rectangle;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.WeakReference;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.text.JTextComponent;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import javax.swing.text.Position;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.settings.FontColorNames;
import org.netbeans.api.editor.settings.SimpleValueNames;
import org.netbeans.api.editor.NavigationHistory;
import org.netbeans.api.editor.caret.EditorCaret;
import org.netbeans.modules.editor.lib2.ComponentUtils;
import org.netbeans.modules.editor.lib2.DocUtils;
import org.netbeans.modules.editor.lib2.highlighting.BlockHighlighting;
import org.netbeans.modules.editor.lib2.highlighting.Factory;
import org.netbeans.modules.editor.search.DocumentFinder.FindReplaceResult;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
/**
* Find management
*
* @author Miloslav Metelka
* @version 1.00
*/
public final class EditorFindSupport {
private static final Logger LOG = Logger.getLogger(EditorFindSupport.class.getName());
/* Find properties.
* They are read by FindSupport when its instance is being initialized.
* FIND_WHAT: java.lang.String - search expression
* FIND_REPLACE_BY: java.lang.String - replace string
* FIND_HIGHLIGHT_SEARCH: java.lang.Boolean - highlight matching strings in text
* FIND_INC_SEARCH: java.lang.Boolean - show matching strings immediately
* FIND_BACKWARD_SEARCH: java.lang.Boolean - search in backward direction
* FIND_WRAP_SEARCH: java.lang.Boolean - if end of doc reached, start from begin
* FIND_MATCH_CASE: java.lang.Boolean - match case of letters
* FIND_SMART_CASE: java.lang.Boolean - case insensitive search if FIND_MATCH_CASE
* is false and all letters of FIND_WHAT are small, case sensitive otherwise
* FIND_WHOLE_WORDS: java.lang.Boolean - match only whole words
* FIND_REG_EXP: java.lang.Boolean - use regular expressions in search expr
* FIND_HISTORY: java.util.List - History of search expressions
* FIND_HISTORY_SIZE: java.lang.Integer - Maximum size of the history
* FIND_BLOCK_SEARCH: java.lang.Boolean - search in block
* FIND_BLOCK_SEARCH_START: javax.swing.text.Position - start position of the block in block search
* FIND_BLOCK_SEARCH_END: javax.swing.text.Position - end position of the block in block search
*
*/
public static final String FIND_WHAT = "find-what"; // NOI18N
public static final String FIND_REPLACE_WITH = "find-replace-with"; // NOI18N
public static final String FIND_HIGHLIGHT_SEARCH = "find-highlight-search"; // NOI18N
public static final String FIND_INC_SEARCH = "find-inc-search"; // NOI18N
public static final String FIND_INC_SEARCH_DELAY = "find-inc-search-delay"; // NOI18N
public static final String FIND_BACKWARD_SEARCH = "find-backward-search"; // NOI18N
public static final String FIND_WRAP_SEARCH = "find-wrap-search"; // NOI18N
public static final String FIND_MATCH_CASE = "find-match-case"; // NOI18N
public static final String FIND_SMART_CASE = "find-smart-case"; // NOI18N
public static final String FIND_PRESERVE_CASE = "find-preserve-case"; // NOI18N
public static final String FIND_WHOLE_WORDS = "find-whole-words"; // NOI18N
public static final String FIND_REG_EXP = "find-reg-exp"; // NOI18N
public static final String FIND_HISTORY = "find-history"; // NOI18N
public static final String FIND_HISTORY_SIZE = "find-history-size"; // NOI18N
public static final String FIND_BLOCK_SEARCH = "find-block-search"; //NOI18N
public static final String FIND_BLOCK_SEARCH_START = "find-block-search-start"; //NOI18N
public static final String FIND_BLOCK_SEARCH_END = "find-block-search-end"; //NOI18N
public static final String ADD_MULTICARET = "add-multi-caret"; //NOI18N
private static final String FOUND_LOCALE = "find-found"; // NOI18N
private static final String NOT_FOUND_LOCALE = "find-not-found"; // NOI18N
private static final String WRAP_START_LOCALE = "find-wrap-start"; // NOI18N
private static final String WRAP_END_LOCALE = "find-wrap-end"; // NOI18N
private static final String WRAP_BLOCK_START_LOCALE = "find-block-wrap-start"; // NOI18N
private static final String WRAP_BLOCK_END_LOCALE = "find-block-wrap-end"; // NOI18N
private static final String ITEMS_REPLACED_LOCALE = "find-items-replaced"; // NOI18N
/** It's public only to keep backwards compatibility of the FindSupport class. */
public static final String REVERT_MAP = "revert-map"; // NOI18N
/** It's public only to keep backwards compatibility of the FindSupport class. */
public static final String FIND_HISTORY_PROP = "find-history-prop"; //NOI18N
public static final String REPLACE_HISTORY_PROP = "replace-history-prop"; //NOI18N
/** It's public only to keep backwards compatibility of the FindSupport class. */
public static final String FIND_HISTORY_CHANGED_PROP = "find-history-changed-prop"; //NOI18N
public static final String REPLACE_HISTORY_CHANGED_PROP = "replace-history-changed-prop"; //NOI18N
/**
* Default message 'importance' for messages from find and replace actions.
* <br/>
* Corresponds to StatusDisplayer.IMPORTANCE_FIND_OR_REPLACE.
*/
private static final int IMPORTANCE_FIND_OR_REPLACE = 800;
/** Shared instance of FindSupport class */
private static EditorFindSupport findSupport;
/** Find properties */
private Map<String, Object> findProps;
private WeakReference<JTextComponent> focusedTextComponent;
private final RequestProcessor executor = new RequestProcessor(EditorFindSupport.class.getName(), 1);
private final WeakHashMap<JTextComponent, Map<String, WeakReference<BlockHighlighting>>> comp2layer =
new WeakHashMap<>();
/** Support for firing change events */
private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);
private SPW lastSelected;
private List<SPW> historyList = new ArrayList<>();
private List<RP> replaceList = new ArrayList<>();
private String cachekey = "";
private int[] cacheContent = new int[0];
private static final int TIME_LIMIT = 2;
private EditorFindSupport() {
}
/** Get shared instance of find support */
public static synchronized EditorFindSupport getInstance() {
if (findSupport == null) {
findSupport = new EditorFindSupport();
}
return findSupport;
}
public Map<String, Object> createDefaultFindProperties() {
HashMap<String, Object> props = new HashMap<>();
props.put(FIND_WHAT, null);
props.put(FIND_REPLACE_WITH, null);
props.put(FIND_HIGHLIGHT_SEARCH, Boolean.TRUE);
props.put(FIND_INC_SEARCH, Boolean.TRUE);
props.put(FIND_BACKWARD_SEARCH, Boolean.FALSE);
props.put(FIND_WRAP_SEARCH, Boolean.TRUE);
props.put(FIND_MATCH_CASE, Boolean.FALSE);
props.put(FIND_SMART_CASE, Boolean.FALSE);
props.put(FIND_WHOLE_WORDS, Boolean.FALSE);
props.put(FIND_REG_EXP, Boolean.FALSE);
props.put(FIND_HISTORY, Integer.valueOf(30));
props.put(FIND_PRESERVE_CASE, Boolean.FALSE);
props.put(ADD_MULTICARET, Boolean.FALSE);
return props;
}
private int getBlockEndOffset(){
Position pos = (Position) getFindProperties().get(FIND_BLOCK_SEARCH_END);
return (pos != null) ? pos.getOffset() : -1;
}
public Map<String, Object> getFindProperties() {
if (findProps == null) {
findProps = createDefaultFindProperties();
}
return findProps;
}
/** Get find property with specified name */
public Object getFindProperty(String name) {
return getFindProperties().get(name);
}
private Map<String, Object> getValidFindProperties(Map<String, Object> props) {
return (props != null) ? props : getFindProperties();
}
/**
* <p><b>IMPORTANT:</b> This method is public only for keeping backwards
* compatibility of the {@link org.netbeans.editor.FindSupport} class.
*/
public synchronized int[] getBlocks(final int[] blocks, final Document doc,
int startOffset, int endOffset) throws BadLocationException {
final Map<String, Object> props = getValidFindProperties(null);
String newCacheKey = calculateCacheKey(doc, startOffset, endOffset, props);
if (cachekey.equals(newCacheKey)) {
return Arrays.copyOf(cacheContent, cacheContent.length);
}
boolean blockSearch = Boolean.TRUE.equals(props.get(FIND_BLOCK_SEARCH));
Position blockSearchStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
Position blockSearchEndPos = (Position) props.get(FIND_BLOCK_SEARCH_END);
if (blockSearch && blockSearchStartPos != null && blockSearchEndPos != null){
if (endOffset >= blockSearchStartPos.getOffset() &&
startOffset <= blockSearchEndPos.getOffset())
{
startOffset = Math.max(blockSearchStartPos.getOffset(), startOffset);
endOffset = Math.min(blockSearchEndPos.getOffset(), endOffset);
} else {
return blocks;
}
}
final int so = startOffset;
final int eo = endOffset;
currentResult = null;
try {
executor.submit(new Runnable() {
@Override
public void run() {
try {
currentResult = DocumentFinder.findBlocks(doc, so, eo, props, blocks);
cacheContent = currentResult.getFoundPositions();
} catch (BadLocationException ble) {
cacheContent = Arrays.copyOf(blocks, blocks.length);
LOG.log(Level.INFO, ble.getMessage(), ble);
}
}
}).get(TIME_LIMIT, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException ex) {
cacheContent = Arrays.copyOf(blocks, blocks.length);
org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), NbBundle.getMessage(EditorFindSupport.class, "slow-search"));
LOG.log(Level.INFO, ex.getMessage(), ex);
}
if (currentResult != null && currentResult.hasErrorMsg()) {
org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), currentResult.getErrorMsg());
}
cachekey = newCacheKey;
return Arrays.copyOf(cacheContent, cacheContent.length);
}
/** Set find property with specified name and fire change.
*/
public void putFindProperty(String name, Object newValue) {
Object oldValue = getFindProperty(name);
if ((oldValue == null && newValue == null)
|| (oldValue != null && oldValue.equals(newValue))
) {
return;
}
if (newValue != null) {
getFindProperties().put(name, newValue);
} else {
getFindProperties().remove(name);
}
firePropertyChange(name, oldValue, newValue);
}
/**
* Add/replace properties from some other map
* to current find properties. If the added properties
* are different than the original ones,
* the property change is fired.
*/
public void putFindProperties(Map<String, Object> propsToAdd) {
if (getFindProperties() != propsToAdd) {
getFindProperties().putAll(propsToAdd);
}
//highlight will not be updated on empty properties
if (propsToAdd.get(FIND_WHAT) != null) {
firePropertyChange(null, null, null);
}
}
public void setFocusedTextComponent(JTextComponent component) {
focusedTextComponent = new WeakReference<>(component);
firePropertyChange(null, null, null);
}
public JTextComponent getFocusedTextComponent() {
JTextComponent jc = focusedTextComponent != null ? focusedTextComponent.get() : null;
return (jc != null) ? jc : EditorRegistry.lastFocusedComponent();
}
public void setBlockSearchHighlight(int startSelection, int endSelection){
JTextComponent comp = getFocusedTextComponent();
BlockHighlighting layer = comp == null ? null : findLayer(comp, Factory.BLOCK_SEARCH_LAYER);
if (layer != null) {
if (startSelection >= 0 && endSelection >= 0 && startSelection < endSelection ) {
layer.highlightBlock(startSelection, endSelection, FontColorNames.BLOCK_SEARCH_COLORING, true, true);
} else {
layer.highlightBlock(-1, -1, FontColorNames.BLOCK_SEARCH_COLORING, true, true);
}
}
}
public boolean incSearch(Map<String, Object> props, int caretPos) {
props = getValidFindProperties(props);
Boolean b = (Boolean)props.get(FIND_INC_SEARCH);
if (b != null && b.booleanValue()) { // inc search enabled
JTextComponent comp = getFocusedTextComponent();
if (comp != null) {
b = (Boolean)props.get(FIND_BACKWARD_SEARCH);
boolean back = (b != null && b.booleanValue());
b = (Boolean)props.get(FIND_BLOCK_SEARCH);
boolean blockSearch = (b != null && b.booleanValue());
Position blockStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
int blockSearchStartOffset = (blockStartPos != null) ? blockStartPos.getOffset() : -1;
Position endPos = (Position) props.get(FIND_BLOCK_SEARCH_END);
int blockSearchEndOffset = (endPos != null) ? endPos.getOffset() : -1;
int pos;
int len = 0;
try {
int start = (blockSearch && blockSearchStartOffset > -1) ? blockSearchStartOffset : 0;
int end = (blockSearch && blockSearchEndOffset > 0) ? blockSearchEndOffset : -1;
if (start > 0 && end == -1) {
return false;
}
int findRet[] = findInBlock(comp, caretPos,
start,
end,
props, false);
if (findRet == null) {
incSearchReset();
return false;
}
pos = findRet[0];
len = findRet.length > 1 ? findRet[1] - pos : 0;
} catch (BadLocationException e) {
LOG.log(Level.WARNING, e.getMessage(), e);
return false;
}
if (pos >= 0) {
// Find the layer
BlockHighlighting layer = findLayer(comp, Factory.INC_SEARCH_LAYER);
if (len > 0) {
if (comp.getSelectionEnd() > comp.getSelectionStart()){
comp.select(caretPos, caretPos);
}
if (layer != null) {
layer.highlightBlock(
pos,
pos + len,
blockSearch ? FontColorNames.INC_SEARCH_COLORING : FontColorNames.SELECTION_COLORING,
false,
false
);
}
Preferences prefs = MimeLookup.getLookup(MimePath.EMPTY).lookup(Preferences.class);
if (prefs.get(SimpleValueNames.EDITOR_SEARCH_TYPE, "default").equals("closing")) { // NOI18N
ensureVisible(comp, pos, pos);
} else {
selectText(comp, pos, pos + len, back);
}
return true;
}
}
}
} else { // inc search not enabled
incSearchReset();
}
return false;
}
public void incSearchReset() {
// Find the layer
JTextComponent comp = getFocusedTextComponent();
BlockHighlighting layer = comp == null ? null : findLayer(comp, Factory.INC_SEARCH_LAYER);
if (layer != null) {
layer.highlightBlock(-1, -1, null, false, false);
}
}
private boolean isBackSearch(Map<String, Object> props, boolean oppositeDir) {
Boolean b = (Boolean)props.get(FIND_BACKWARD_SEARCH);
boolean back = (b != null && b.booleanValue());
if (oppositeDir) {
back = !back;
}
return back;
}
private void addCaretSelectText(JTextComponent c, int start, int end, boolean back) {
Caret eCaret = c.getCaret();
ensureVisible(c, start, end);
if (eCaret instanceof EditorCaret) {
EditorCaret caret = (EditorCaret) eCaret;
try {
caret.addCaret(c.getDocument().createPosition(end), Position.Bias.Forward,
c.getDocument().createPosition(start), Position.Bias.Forward);
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
}
private void selectText(JTextComponent c, int start, int end, boolean back){
Caret caret = c.getCaret();
ensureVisible(c, start, end);
if (back) {
caret.setDot(end);
caret.moveDot(start);
} else { // forward direction
caret.setDot(start);
caret.moveDot(end);
}
}
private void ensureVisible(JTextComponent c, int startOffset, int endOffset) {
// TODO: read insets from settings
ensureVisible(c, startOffset, endOffset, new Insets(10, 10, 10, 10));
}
/**
* Ensure that the given region will be visible in the view
* with the appropriate find insets.
*/
private void ensureVisible(JTextComponent c, int startOffset, int endOffset, Insets extraInsets) {
try {
Rectangle startBounds = c.modelToView(startOffset);
Rectangle endBounds = c.modelToView(endOffset);
if (startBounds != null && endBounds != null) {
startBounds.add(endBounds);
if (extraInsets != null) {
Rectangle visibleBounds = c.getVisibleRect();
int extraTop = (extraInsets.top < 0)
? -extraInsets.top * visibleBounds.height / 100 // percentage
: extraInsets.top * endBounds.height; // line count
startBounds.y -= extraTop;
startBounds.height += extraTop;
startBounds.height += (extraInsets.bottom < 0)
? -extraInsets.bottom * visibleBounds.height / 100 // percentage
: extraInsets.bottom * endBounds.height; // line count
int extraLeft = (extraInsets.left < 0)
? -extraInsets.left * visibleBounds.width / 100 // percentage
: extraInsets.left * endBounds.width; // char count
startBounds.x -= extraLeft;
startBounds.width += extraLeft;
startBounds.width += (extraInsets.right < 0)
? -extraInsets.right * visibleBounds.width / 100 // percentage
: extraInsets.right * endBounds.width; // char count
}
c.scrollRectToVisible(startBounds);
}
} catch (BadLocationException e) {
// do not scroll
}
}
private int[] findMatches = null;
private synchronized boolean findMatches(final String text, final Map<String, Object> props) {
if(text == null) {
return false;
}
try {
final PlainDocument plainDocument = new PlainDocument();
plainDocument.insertString(0, text, null);
findMatches = null;
try {
executor.submit(new Runnable() {
@Override
public void run() {
try {
findMatches = DocumentFinder.find(plainDocument, 0, text.length(), props, false);
} catch (BadLocationException ble) {
LOG.log(Level.INFO, ble.getMessage(), ble);
}
}
}).get(TIME_LIMIT, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException ex) {
org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), NbBundle.getMessage(
EditorFindSupport.class, "slow-search"));
LOG.log(Level.INFO, ex.getMessage(), ex);
}
return findMatches != null && findMatches[0] != -1;
} catch (BadLocationException ex) {
return false;
}
}
FindReplaceResult findReplaceImpl(String replaceExp,
Map<String, Object> props, boolean oppositeDir, JTextComponent c) {
incSearchReset();
props = getValidFindProperties(props);
boolean back = isBackSearch(props, oppositeDir);
if (props.get(FIND_WHAT) == null || !(props.get(FIND_WHAT) instanceof String)) {
return null;
}
String findWhat = (String) props.get(FIND_WHAT);
if (c != null) {
ComponentUtils.clearStatusText(c);
Caret caret = c.getCaret();
int dotPos = caret.getDot();
if (findMatches(c.getSelectedText(), props)) {
Object dp = props.get(FIND_BACKWARD_SEARCH);
boolean direction = (dp != null) ? ((Boolean)dp).booleanValue() : false;
if (dotPos == (oppositeDir ^ direction ? c.getSelectionEnd() : c.getSelectionStart())) {
dotPos += (oppositeDir ^ direction ? -1 : 1);
}
if (replaceExp != null) {
if (oppositeDir ^ direction) {
dotPos = c.getSelectionEnd();
} else {
dotPos = c.getSelectionStart();
}
}
}
Boolean b = (Boolean)props.get(FIND_BLOCK_SEARCH);
boolean blockSearch = (b != null && b.booleanValue());
Position blockStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
int blockSearchStart = (blockStartPos != null) ? blockStartPos.getOffset() : -1;
int blockSearchEnd = getBlockEndOffset();
boolean backSearch = Boolean.TRUE.equals(props.get(FIND_BACKWARD_SEARCH));
if (backSearch) {
blockSearchEnd = dotPos;
dotPos = 0;
}
try {
FindReplaceResult result = findReplaceInBlock(replaceExp, c, dotPos,
(blockSearch && blockSearchStart > -1) ? blockSearchStart : 0,
(blockSearch && blockSearchEnd > 0) ? blockSearchEnd : backSearch ? blockSearchEnd : -1,
props, oppositeDir);
if (result != null && result.hasErrorMsg()) {
ComponentUtils.setStatusText(c, result.getErrorMsg());
c.getCaret().setDot(c.getCaret().getDot());
return null;
}
int[] blk = null;
if (result != null){
blk = result.getFoundPositions();
}
if (blk != null) {
if (Boolean.TRUE.equals(props.get(EditorFindSupport.ADD_MULTICARET))) {
addCaretSelectText(c, blk[0], blk[1], back);
} else {
selectText(c, blk[0], blk[1], back);
}
String msg = NbBundle.getMessage(EditorFindSupport.class, FOUND_LOCALE, findWhat, DocUtils.debugPosition(c.getDocument(), Integer.valueOf(blk[0])));
// String msg = exp + NbBundle.getMessage(EditorFindSupport.class, FOUND_LOCALE)
// + ' ' + DocUtils.debugPosition(c.getDocument(), blk[0]);
if (blk[2] == 1) { // wrap was done
msg += "; "; // NOI18N
if (blockSearch && blockSearchEnd>0 && blockSearchStart >-1){
msg += back ? NbBundle.getMessage(EditorFindSupport.class, WRAP_BLOCK_END_LOCALE)
: NbBundle.getMessage(EditorFindSupport.class, WRAP_BLOCK_START_LOCALE);
}else{
msg += back ? NbBundle.getMessage(EditorFindSupport.class, WRAP_END_LOCALE)
: NbBundle.getMessage(EditorFindSupport.class, WRAP_START_LOCALE);
}
ComponentUtils.setStatusText(c, msg, IMPORTANCE_FIND_OR_REPLACE);
c.getToolkit().beep();
} else {
ComponentUtils.setStatusText(c, msg, IMPORTANCE_FIND_OR_REPLACE);
}
return result;
} else { // not found
ComponentUtils.setStatusText(c, NbBundle.getMessage(
EditorFindSupport.class, NOT_FOUND_LOCALE, findWhat), IMPORTANCE_FIND_OR_REPLACE);
// issue 14189 - selection was not removed
c.getCaret().setDot(c.getCaret().getDot());
}
} catch (BadLocationException e) {
LOG.log(Level.WARNING, e.getMessage(), e);
}
}
return null;
}
/** Find the text from the caret position.
* @param localProps search properties
* @param oppositeDir whether search in opposite direction
*/
public boolean find(Map<String, Object> props, boolean oppositeDir) {
FindReplaceResult result = findReplaceImpl(null, props, oppositeDir, getFocusedTextComponent());
return (result != null);
}
private FindReplaceResult currentResult = null;
private synchronized FindReplaceResult findReplaceInBlock(final String replaceExp, JTextComponent c, int startPos, int blockStartPos,
int blockEndPos, Map<String, Object> props, final boolean oppositeDir) throws BadLocationException {
if (c != null) {
final Map<String, Object> validProps = getValidFindProperties(props);
final Document doc = c.getDocument();
int pos = -1;
boolean wrapDone = false;
String replaced = null;
boolean back = isBackSearch(validProps, oppositeDir);
Boolean b = (Boolean)validProps.get(FIND_WRAP_SEARCH);
boolean wrap = (b != null && b.booleanValue());
int docLen = doc.getLength();
if (blockEndPos == -1) {
blockEndPos = docLen;
}
if (startPos == -1) {
startPos = docLen;
}
int retFind[];
while (true) {
//pos = doc.find(sf, startPos, back ? blockStartPos : blockEndPos);
final int off1 = startPos;
final int off2 = oppositeDir ? blockStartPos : blockEndPos;
currentResult = null;
try {
executor.submit(new Runnable() {
@Override
public void run() {
try {
currentResult = DocumentFinder.findReplaceResult(replaceExp, doc, off1, off2,
validProps, oppositeDir);
} catch (BadLocationException ble) {
LOG.log(Level.WARNING, ble.getMessage(), ble);
}
}
}).get(TIME_LIMIT, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException ex) {
org.netbeans.editor.Utilities.setStatusBoldText(getFocusedTextComponent(), NbBundle.getMessage(
EditorFindSupport.class, "slow-search"));
LOG.log(Level.INFO, ex.getMessage(), ex);
}
if (currentResult == null) {
return null;
}
if (currentResult.hasErrorMsg()) {
return currentResult;
}
retFind = currentResult.getFoundPositions();
replaced = currentResult.getReplacedString();
if (retFind == null){
break;
}
pos = retFind[0];
if (pos != -1) {
break;
}
if (wrap) {
if (back) {
//Bug #20552 the wrap search check whole document
//instead of just the remaining not-searched part to be
//able to find expressions with the cursor in it
//blockStartPos = startPos;
startPos = blockEndPos;
blockEndPos = docLen;
} else {
//blockEndPos = startPos;
startPos = blockStartPos;
}
wrapDone = true;
wrap = false; // only one loop
} else { // no wrap set
break;
}
}
if (pos != -1) {
int[] ret = new int[3];
ret[0] = pos;
ret[1] = retFind[1];
ret[2] = wrapDone ? 1 : 0;
return new FindReplaceResult(ret, replaced);
}
}
return null;
}
/** Find the searched expression
* @param startPos position from which to search. It must be inside the block.
* @param blockStartPos starting position of the block. It must
* be valid position greater or equal than zero. It must be lower than
* or equal to blockEndPos (except blockEndPos=-1).
* @param blockEndPos ending position of the block. It can be -1 for the end
* of document. It must be greater or equal than blockStartPos (except blockEndPos=-1).
* @param localProps search properties
* @param oppositeDir whether search in opposite direction
* @param displayWrap whether display messages about the wrapping
* @return either null when nothing was found or integer array with three members
* ret[0] - starting position of the found string
* ret[1] - ending position of the found string
* ret[2] - 1 or 0 when wrap was or wasn't performed in order to find the string
*/
public int[] findInBlock(JTextComponent c, int startPos, int blockStartPos,
int blockEndPos, Map<String, Object> props, boolean oppositeDir) throws BadLocationException {
FindReplaceResult result = findReplaceInBlock(null, c, startPos, blockStartPos,
blockEndPos, props, oppositeDir);
return result == null ? null : result.getFoundPositions();
}
public boolean replace(Map<String, Object> props, boolean oppositeDir)
throws BadLocationException {
incSearchReset();
return replaceImpl(props, oppositeDir, getFocusedTextComponent());
}
boolean replaceImpl(Map<String, Object> props, boolean oppositeDir, JTextComponent c) throws BadLocationException {
props = getValidFindProperties(props);
boolean back = Boolean.TRUE.equals(props.get(FIND_BACKWARD_SEARCH));
if (oppositeDir) {
back = !back;
}
boolean blockSearch = Boolean.TRUE.equals(props.get(FIND_BLOCK_SEARCH));
Position blockSearchStartPos = (Position) props.get(FIND_BLOCK_SEARCH_START);
int blockSearchStartOffset = (blockSearchStartPos != null) ? blockSearchStartPos.getOffset() : -1;
if (c != null) {
String s = (String)props.get(FIND_REPLACE_WITH);
Caret caret = c.getCaret();
if (caret.isSelectionVisible() && caret.getDot() != caret.getMark()){
Object dp = props.get(FIND_BACKWARD_SEARCH);
boolean direction = (dp != null) ? ((Boolean)dp).booleanValue() : false;
int dotPos = (oppositeDir ^ direction ? c.getSelectionEnd() : c.getSelectionStart());
c.setCaretPosition(dotPos);
}
FindReplaceResult result = findReplaceImpl(s, props, oppositeDir, c);
if (result!=null){
s = result.getReplacedString();
} else {
return false;
}
Document doc = c.getDocument();
int startOffset = c.getSelectionStart();
int len = c.getSelectionEnd() - startOffset;
DocUtils.atomicLock(doc);
try {
if (len > 0) {
doc.remove(startOffset, len);
}
if (s != null && s.length() > 0) {
try {
NavigationHistory.getEdits().markWaypoint(c, startOffset, false, true);
} catch (BadLocationException e) {
LOG.log(Level.WARNING, "Can't add position to the history of edits.", e); //NOI18N
}
doc.insertString(startOffset, s, null);
if (startOffset == blockSearchStartOffset) { // Replaced at begining of block
blockSearchStartPos = doc.createPosition(startOffset);
props.put(EditorFindSupport.FIND_BLOCK_SEARCH_START, blockSearchStartPos);
}
}
} finally {
DocUtils.atomicUnlock(doc);
if (blockSearch){
setBlockSearchHighlight(blockSearchStartOffset, getBlockEndOffset());
}
}
// adjust caret pos after replace operation
int adjustedCaretPos = (back || s == null) ? startOffset : startOffset + s.length();
caret.setDot(adjustedCaretPos);
}
return true;
}
public void replaceAll(Map<String, Object> props) {
incSearchReset();
replaceAllImpl(props, getFocusedTextComponent());
}
/**
* This method is called from unit test. It is implementation of the above method.
* @param props
* @param c
*/
void replaceAllImpl(Map<String, Object> props, JTextComponent c) {
props = getValidFindProperties(props);
Map<String,Object> localProps = new HashMap<>(props);
String replaceWithOriginal = (String)localProps.get(FIND_REPLACE_WITH);
Object findWhat = localProps.get(FIND_WHAT);
if (findWhat == null) { // nothing to search for
return;
}
if (findWhat.equals(replaceWithOriginal)) {
return;
}
Document doc = c.getDocument();
int maxCnt = doc.getLength();
int replacedCnt = 0;
int totalCnt = 0;
boolean blockSearch = Boolean.TRUE.equals(localProps.get(FIND_BLOCK_SEARCH));
boolean wrapSearch = Boolean.TRUE.equals(localProps.get(FIND_WRAP_SEARCH));
boolean backSearch = Boolean.TRUE.equals(localProps.get(FIND_BACKWARD_SEARCH));
if (wrapSearch){
localProps.put(FIND_WRAP_SEARCH, Boolean.FALSE);
localProps.put(FIND_BACKWARD_SEARCH, Boolean.FALSE);
backSearch = false;
firePropertyChange(null, null, null);
}
Position blockSearchStartPos = (Position) localProps.get(FIND_BLOCK_SEARCH_START);
int blockSearchStartOffset = (blockSearchStartPos != null) ? blockSearchStartPos.getOffset() : -1;
int blockSearchEndOffset = getBlockEndOffset();
if (c != null) {
DocUtils.atomicLock(doc);
try {
int startPosWholeSearch = 0;
int endPosWholeSearch = -1;
int caretPos = c.getCaret().getDot();
if (!wrapSearch){
if (backSearch){
startPosWholeSearch = 0;
endPosWholeSearch = caretPos;
}else{
startPosWholeSearch = caretPos;
endPosWholeSearch = -1;
}
}
int actualPos = wrapSearch ? 0 : c.getCaret().getDot();
int pos = (blockSearch && blockSearchStartOffset > -1) ? blockSearchStartOffset : (backSearch? 0 : actualPos); // actual position
while (true) {
FindReplaceResult result = findReplaceInBlock(replaceWithOriginal, c, pos,
(blockSearch && blockSearchStartOffset > -1) ? blockSearchStartOffset : startPosWholeSearch,
(blockSearch && blockSearchEndOffset > 0) ? blockSearchEndOffset : endPosWholeSearch,
localProps, false);
if (result == null){
break;
}
int[] blk = result.getFoundPositions();
String replaceWith = result.getReplacedString();
if (blk == null) {
break;
}
totalCnt++;
int len = blk[1] - blk[0];
boolean skip = false; // cannot remove (because of guarded block)?
try {
doc.remove(blk[0], len);
} catch (BadLocationException e) {
// replace in guarded block
if (ComponentUtils.isGuardedException(e)) {
skip = true;
} else {
throw e;
}
}
if (skip) {
pos = backSearch ? blk[0] : blk[0] + len;
} else { // can and will insert the new string
if (replaceWith != null && replaceWith.length() > 0) {
int offset = blk[0];
try {
NavigationHistory.getEdits().markWaypoint(c, offset, false, true);
} catch (BadLocationException e) {
LOG.log(Level.WARNING, "Can't add position to the history of edits.", e); //NOI18N
}
doc.insertString(offset, replaceWith, null);
if (offset == blockSearchStartOffset) { // Replaced at begining of block
blockSearchStartPos = doc.createPosition(offset);
// Update position in original properties
props.put(EditorFindSupport.FIND_BLOCK_SEARCH_START, blockSearchStartPos);
}
blockSearchEndOffset = getBlockEndOffset();
}
pos = backSearch ? blk[0] : blk[0] + ((replaceWith != null) ? replaceWith.length() : 0);
if (!wrapSearch && backSearch) {
endPosWholeSearch = endPosWholeSearch < blk[0] ? endPosWholeSearch : blk[0];
blockSearchEndOffset = blockSearchEndOffset < blk[0] ? blockSearchEndOffset : blk[0];
pos = (blockSearch && blockSearchStartOffset > -1) ? blockSearchStartOffset : 0;
}
replacedCnt++;
}
// The following is lame attempt to break the loop: if
// someone knows a better way please remove this but check
// that all tests in EditorFindSupportTest pass!
if (replacedCnt > maxCnt) {
break;
}
}
// Display message about replacement
if (totalCnt == 0){
String exp = "'" + findWhat + "' "; //NOI18N
ComponentUtils.setStatusText(c, exp + NbBundle.getMessage(
EditorFindSupport.class, NOT_FOUND_LOCALE), IMPORTANCE_FIND_OR_REPLACE);
}else{
MessageFormat fmt = new MessageFormat(
NbBundle.getMessage(EditorFindSupport.class, ITEMS_REPLACED_LOCALE));
String msg = fmt.format(new Object[] { Integer.valueOf(replacedCnt), Integer.valueOf(totalCnt) });
ComponentUtils.setStatusText(c, msg, IMPORTANCE_FIND_OR_REPLACE);
}
} catch (BadLocationException e) {
LOG.log(Level.WARNING, e.getMessage(), e);
} finally {
DocUtils.atomicUnlock(doc);
if (blockSearch){
setBlockSearchHighlight(blockSearchStartOffset, getBlockEndOffset());
}
}
}
}
public void hookLayer(BlockHighlighting layer, JTextComponent component) {
synchronized (comp2layer) {
Map<String, WeakReference<BlockHighlighting>> type2layer = comp2layer.get(component);
if (type2layer == null) {
type2layer = new HashMap<>();
comp2layer.put(component, type2layer);
}
type2layer.put(layer.getLayerTypeId(), new WeakReference<>(layer));
}
}
public void unhookLayer(BlockHighlighting layer, JTextComponent component) {
synchronized (comp2layer) {
Map<String, WeakReference<BlockHighlighting>> type2layer = comp2layer.get(component);
if (type2layer != null) {
type2layer.remove(layer.getLayerTypeId());
if (type2layer.isEmpty()) {
comp2layer.remove(component);
}
}
}
}
public BlockHighlighting findLayer(JTextComponent component, String layerId) {
synchronized (comp2layer) {
Map<String, WeakReference<BlockHighlighting>> type2layer = comp2layer.get(component);
BlockHighlighting layer = null;
if (type2layer != null) {
WeakReference<BlockHighlighting> ref = type2layer.get(layerId);
if (ref != null) {
layer = ref.get();
}
}
return layer;
}
}
/** Add weak listener to listen to change of any property. The caller must
* hold the listener object in some instance variable to prevent it
* from being garbage collected.
*/
public void addPropertyChangeListener(PropertyChangeListener l) {
changeSupport.addPropertyChangeListener(l);
}
public synchronized void addPropertyChangeListener(String findPropertyName,
PropertyChangeListener l) {
changeSupport.addPropertyChangeListener(findPropertyName, l);
}
/** Remove listener for changes in properties */
public void removePropertyChangeListener(PropertyChangeListener l) {
changeSupport.removePropertyChangeListener(l);
}
/**
* <p><b>IMPORTANT:</b> This method is public only for keeping backwards
* compatibility of the {@link org.netbeans.editor.FindSupport} class.
*/
public void firePropertyChange(String settingName, Object oldValue, Object newValue) {
changeSupport.firePropertyChange(settingName, oldValue, newValue);
}
public void setHistory(List<SPW> spwList){
this.historyList = new ArrayList<>(spwList);
if (!spwList.isEmpty()) {
setLastSelected(spwList.get(0));
// firePropertyChange(FIND_HISTORY_CHANGED_PROP,null,null);
}
}
public void setReplaceHistory(List<RP> rpList){
this.replaceList = new ArrayList<>(rpList);
}
public List<SPW> getHistory(){
if (historyList.isEmpty()) {
firePropertyChange(FIND_HISTORY_CHANGED_PROP,null,null);
}
return historyList;
}
public List<RP> getReplaceHistory(){
if (replaceList.isEmpty()) {
firePropertyChange(REPLACE_HISTORY_CHANGED_PROP,null,null);
}
return replaceList;
}
public void setLastSelected(SPW spw){
this.lastSelected = spw;
Map<String, Object> props = getFindProperties();
if (spw == null) {
return;
}
props.put(FIND_WHAT, spw.getSearchExpression());
props.put(FIND_MATCH_CASE, Boolean.valueOf(spw.isMatchCase()));
props.put(FIND_REG_EXP, Boolean.valueOf(spw.isRegExp()));
props.put(FIND_WHOLE_WORDS, Boolean.valueOf(spw.isWholeWords()));
}
public SPW getLastSelected(){
return lastSelected;
}
public void addToHistory(SPW spw){
if (spw == null) {
return;
}
firePropertyChange(FIND_HISTORY_PROP, null, spw);
}
public void addToReplaceHistory(RP rp) {
if (rp == null) {
return;
}
firePropertyChange(REPLACE_HISTORY_PROP, null, rp);
}
private String calculateCacheKey(Document doc, int startOffset, int endOffset, Map<String, Object> props) {
StringBuilder newCacheKey = new StringBuilder();
newCacheKey.append("#").append(doc.getLength());
newCacheKey.append("#").append(startOffset);
newCacheKey.append("#").append(endOffset);
newCacheKey.append("#").append(props.get(FIND_WHAT));
newCacheKey.append("#").append(props.get(FIND_HIGHLIGHT_SEARCH));
newCacheKey.append("#").append(props.get(FIND_INC_SEARCH));
newCacheKey.append("#").append(props.get(FIND_BACKWARD_SEARCH));
newCacheKey.append("#").append(props.get(FIND_WRAP_SEARCH));
newCacheKey.append("#").append(props.get(FIND_MATCH_CASE));
newCacheKey.append("#").append(props.get(FIND_SMART_CASE));
newCacheKey.append("#").append(props.get(FIND_WHOLE_WORDS));
newCacheKey.append("#").append(props.get(FIND_REG_EXP));
newCacheKey.append("#").append(props.get(FIND_BLOCK_SEARCH));
newCacheKey.append("#").append(props.get(FIND_BLOCK_SEARCH_START));
newCacheKey.append("#").append(props.get(FIND_BLOCK_SEARCH_END));
return newCacheKey.toString();
}
public static final class SPW{
private final String searchExpression;
private final boolean wholeWords;
private final boolean matchCase;
private final boolean regExp;
public SPW(String searchExpression, boolean wholeWords,
boolean matchCase, boolean regExp){
this.searchExpression = searchExpression;
this.wholeWords = wholeWords;
this.matchCase = matchCase;
this.regExp = regExp;
}
/** @return searchExpression */
public String getSearchExpression(){
return searchExpression;
}
/** @return true if the wholeWords parameter was used during search performing */
public boolean isWholeWords(){
return wholeWords;
}
/** @return true if the matchCase parameter was used during search performing */
public boolean isMatchCase(){
return matchCase;
}
/** @return true if the regExp parameter was used during search performing */
public boolean isRegExp(){
return regExp;
}
public @Override boolean equals(Object obj){
if (!(obj instanceof SPW)){
return false;
}
SPW sp = (SPW)obj;
return (this.searchExpression.equals(sp.getSearchExpression()) &&
this.wholeWords == sp.isWholeWords() &&
this.matchCase == sp.isMatchCase() &&
this.regExp == sp.isRegExp());
}
public @Override int hashCode() {
int result = 17;
result = 37*result + (this.wholeWords ? 1:0);
result = 37*result + (this.matchCase ? 1:0);
result = 37*result + (this.regExp ? 1:0);
result = 37*result + this.searchExpression.hashCode();
return result;
}
public @Override String toString(){
StringBuilder sb = new StringBuilder("[SearchPatternWrapper:]\nsearchExpression:"+searchExpression);//NOI18N
sb.append('\n');
sb.append("wholeWords:");//NOI18N
sb.append(wholeWords);
sb.append('\n');
sb.append("matchCase:");//NOI18N
sb.append(matchCase);
sb.append('\n');
sb.append("regExp:");//NOI18N
sb.append(regExp);
return sb.toString();
}
} // End of SPW class
public static final class RP {
private final String replaceExpression;
private final boolean preserveCase;
public RP(String replaceExpression, boolean preserveCase) {
this.replaceExpression = replaceExpression;
this.preserveCase = preserveCase;
}
public String getReplaceExpression() {
return replaceExpression;
}
public boolean isPreserveCase() {
return preserveCase;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof RP)) {
return false;
}
RP sp = (RP) obj;
return (this.replaceExpression.equals(sp.getReplaceExpression())
&& this.preserveCase == sp.isPreserveCase());
}
@Override
public int hashCode() {
int result = 17;
result = 37 * result + (this.preserveCase ? 1 : 0);
result = 37 * result + this.replaceExpression.hashCode();
return result;
}
}
}