/* * 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 synchronized static 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 final static 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 final static 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; } } }