Esempio n. 1
0
  // {{{ invokeReadNextChar() method
  protected void invokeReadNextChar(char ch) {
    JEditBuffer buffer = view.getBuffer();

    /* if(buffer.insideCompoundEdit())
    buffer.endCompoundEdit(); */

    String charStr = StandardUtilities.charsToEscapes(String.valueOf(ch));

    // this might be a bit slow if __char__ occurs a lot
    int index;
    while ((index = readNextChar.indexOf("__char__")) != -1) {
      readNextChar =
          readNextChar.substring(0, index)
              + '\''
              + charStr
              + '\''
              + readNextChar.substring(index + 8);
    }

    Macros.Recorder recorder = view.getMacroRecorder();
    if (recorder != null) recorder.record(getRepeatCount(), readNextChar);

    view.getStatus().setMessage(null);

    if (getRepeatCount() != 1) {
      try {
        buffer.beginCompoundEdit();

        BeanShell.eval(
            view,
            BeanShell.getNameSpace(),
            "for(int i = 1; i < " + getRepeatCount() + "; i++)\n{\n" + readNextChar + "\n}");
      } finally {
        buffer.endCompoundEdit();
      }
    } else BeanShell.eval(view, BeanShell.getNameSpace(), readNextChar);

    readNextChar = null;
  } // }}}
Esempio n. 2
0
public class ActionBar extends JPanel {

  public ActionBar(View view, boolean temp) {
    setLayout(new BoxLayout(this, BoxLayout.X_AXIS));

    this.view = view;
    this.temp = temp;

    add(Box.createHorizontalStrut(2));

    JLabel label = new JLabel(jEdit.getProperty("view.action.prompt"));
    add(label);
    add(Box.createHorizontalStrut(12));
    add(action = new ActionTextField());
    action.setEnterAddsToHistory(false);
    Dimension max = action.getPreferredSize();
    max.width = Integer.MAX_VALUE;
    action.setMaximumSize(max);
    action.addActionListener(new ActionHandler());
    action.getDocument().addDocumentListener(new DocumentHandler());

    if (temp) {
      close = new RolloverButton(GUIUtilities.loadIcon("closebox.gif"));
      close.addActionListener(new ActionHandler());
      close.setToolTipText(jEdit.getProperty("view.action.close-tooltip"));
      add(close);
    }

    this.temp = temp;
  }

  public HistoryTextField getField() {
    return action;
  }

  public void goToActionBar() {
    repeatCount = view.getInputHandler().getRepeatCount();
    action.setText(null);
    action.requestFocus();
  }

  private static NameSpace namespace =
      new NameSpace(BeanShell.getNameSpace(), "action bar namespace");

  private View view;
  private boolean temp;
  private int repeatCount;
  private HistoryTextField action;
  private CompletionPopup popup;
  private RolloverButton close;

  private void invoke() {
    String cmd;
    if (popup != null) cmd = popup.list.getSelectedValue().toString();
    else {
      cmd = action.getText().trim();
      int index = cmd.indexOf('=');
      if (index != -1) {
        action.addCurrentToHistory();
        String propName = cmd.substring(0, index).trim();
        String propValue = cmd.substring(index + 1).trim();
        String code;

        if (propName.startsWith("buffer.")) {
          if (propName.equals("buffer.mode")) {
            code = "buffer.setMode(\"" + MiscUtilities.charsToEscapes(propValue) + "\");";
          } else {
            code =
                "buffer.setStringProperty(\""
                    + MiscUtilities.charsToEscapes(propName.substring("buffer.".length()))
                    + "\",\""
                    + MiscUtilities.charsToEscapes(propValue)
                    + "\");";
          }

          code += "\nbuffer.propertiesChanged();";
        } else if (propName.startsWith("!buffer.")) {
          code =
              "jEdit.setProperty(\""
                  + MiscUtilities.charsToEscapes(propName.substring(1))
                  + "\",\""
                  + MiscUtilities.charsToEscapes(propValue)
                  + "\");\n"
                  + "jEdit.propertiesChanged();";
        } else {
          code =
              "jEdit.setProperty(\""
                  + MiscUtilities.charsToEscapes(propName)
                  + "\",\""
                  + MiscUtilities.charsToEscapes(propValue)
                  + "\");\n"
                  + "jEdit.propertiesChanged();";
        }

        Macros.Recorder recorder = view.getMacroRecorder();
        if (recorder != null) recorder.record(code);
        BeanShell.eval(view, namespace, code);
        cmd = null;
      } else if (cmd.length() != 0) {
        String[] completions = getCompletions(cmd);
        if (completions.length != 0) {
          cmd = completions[0];
        }
      } else cmd = null;
    }

    if (popup != null) {
      popup.dispose();
      popup = null;
    }

    final String finalCmd = cmd;
    final EditAction act = (finalCmd == null ? null : jEdit.getAction(finalCmd));
    if (temp) view.removeToolBar(this);

    SwingUtilities.invokeLater(
        new Runnable() {
          public void run() {
            view.getTextArea().requestFocus();
            if (act == null) {
              if (finalCmd != null) {
                view.getStatus()
                    .setMessageAndClear(jEdit.getProperty("view.action.no-completions"));
              }
            } else {
              view.getInputHandler().setRepeatCount(repeatCount);
              view.getInputHandler().invokeAction(act);
            }
          }
        });
  }

  private String[] getCompletions(String str) {
    str = str.toLowerCase();
    String[] actions = jEdit.getActionNames();
    ArrayList<String> returnValue = new ArrayList<String>(actions.length);
    for (int i = 0; i < actions.length; i++) {
      if (actions[i].toLowerCase().contains(str)) returnValue.add(actions[i]);
    }

    return returnValue.toArray(new String[returnValue.size()]);
  }

  private void complete(boolean insertLongestPrefix) {
    String text = action.getText().trim();
    String[] completions = getCompletions(text);
    if (completions.length == 1) {
      if (insertLongestPrefix) action.setText(completions[0]);
    } else if (completions.length != 0) {
      if (insertLongestPrefix) {
        String prefix = MiscUtilities.getLongestPrefix(completions, true);
        if (prefix.contains(text)) action.setText(prefix);
      }

      if (popup != null) popup.setModel(completions);
      else popup = new CompletionPopup(completions);
      return;
    }

    if (popup != null) {
      popup.dispose();
      popup = null;
    }
  }

  class ActionHandler implements ActionListener {
    public void actionPerformed(ActionEvent evt) {
      if (evt.getSource() == close) view.removeToolBar(ActionBar.this);
      else invoke();
    }
  }

  class DocumentHandler implements DocumentListener {

    public void insertUpdate(DocumentEvent evt) {
      if (popup != null) complete(false);
    }

    public void removeUpdate(DocumentEvent evt) {
      if (popup != null) complete(false);
    }

    public void changedUpdate(DocumentEvent evt) {}
  }

  class ActionTextField extends HistoryTextField {
    boolean repeat;
    boolean nonDigit;

    ActionTextField() {
      super("action");
      setSelectAllOnFocus(true);
    }

    public boolean isManagingFocus() {
      return false;
    }

    public boolean getFocusTraversalKeysEnabled() {
      return false;
    }

    public void processKeyEvent(KeyEvent evt) {
      evt = KeyEventWorkaround.processKeyEvent(evt);
      if (evt == null) return;

      switch (evt.getID()) {
        case KeyEvent.KEY_TYPED:
          char ch = evt.getKeyChar();
          if (!nonDigit && Character.isDigit(ch)) {
            super.processKeyEvent(evt);
            repeat = true;
            repeatCount = Integer.parseInt(action.getText());
          } else {
            nonDigit = true;
            if (repeat) {
              passToView(evt);
            } else super.processKeyEvent(evt);
          }
          break;
        case KeyEvent.KEY_PRESSED:
          int keyCode = evt.getKeyCode();
          if (evt.isActionKey()
              || evt.isControlDown()
              || evt.isAltDown()
              || evt.isMetaDown()
              || keyCode == KeyEvent.VK_BACK_SPACE
              || keyCode == KeyEvent.VK_DELETE
              || keyCode == KeyEvent.VK_ENTER
              || keyCode == KeyEvent.VK_TAB
              || keyCode == KeyEvent.VK_ESCAPE) {
            nonDigit = true;
            if (repeat) {
              passToView(evt);
              break;
            } else if (keyCode == KeyEvent.VK_TAB) {
              complete(true);
              evt.consume();
            } else if (keyCode == KeyEvent.VK_ESCAPE) {
              evt.consume();
              if (popup != null) {
                popup.dispose();
                popup = null;
                action.requestFocus();
              } else {
                if (temp) view.removeToolBar(ActionBar.this);
                view.getEditPane().focusOnTextArea();
              }
              break;
            } else if ((keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN)
                && popup != null) {
              popup.list.processKeyEvent(evt);
              break;
            }
          }
          super.processKeyEvent(evt);
          break;
      }
    }

    private void passToView(final KeyEvent evt) {
      if (temp) view.removeToolBar(ActionBar.this);
      view.getTextArea().requestFocus();
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              view.getTextArea().requestFocus();
              view.getInputHandler().setRepeatCount(repeatCount);
              view.getInputHandler().processKeyEvent(evt, View.ACTION_BAR, false);
            }
          });
    }

    public void addNotify() {
      super.addNotify();
      repeat = nonDigit = false;
    }
  }

  class CompletionPopup extends JWindow {
    CompletionList list;

    CompletionPopup(String[] actions) {
      super(view);

      setContentPane(
          new JPanel(new BorderLayout()) {

            public boolean isManagingFocus() {
              return false;
            }

            public boolean getFocusTraversalKeysEnabled() {
              return false;
            }
          });

      list = new CompletionList(actions);
      list.setVisibleRowCount(8);
      list.addMouseListener(new MouseHandler());
      list.setSelectedIndex(0);
      list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

      JScrollPane scroller =
          new JScrollPane(
              list,
              ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
              ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

      getContentPane().add(scroller, BorderLayout.CENTER);

      GUIUtilities.requestFocus(this, list);

      pack();
      Point p = new Point(0, -getHeight());
      SwingUtilities.convertPointToScreen(p, action);
      setLocation(p);
      setVisible(true);

      KeyHandler keyHandler = new KeyHandler();
      addKeyListener(keyHandler);
      list.addKeyListener(keyHandler);
    }

    void setModel(String[] actions) {
      list.setListData(actions);
      list.setSelectedIndex(0);
    }

    class MouseHandler extends MouseAdapter {
      public void mouseClicked(MouseEvent evt) {
        invoke();
      }
    }

    class CompletionList extends JList {
      CompletionList(Object[] data) {
        super(data);
      }

      public void processKeyEvent(KeyEvent evt) {
        super.processKeyEvent(evt);
      }
    }

    class KeyHandler extends KeyAdapter {
      public void keyTyped(KeyEvent evt) {
        action.processKeyEvent(evt);
      }

      public void keyPressed(KeyEvent evt) {
        int keyCode = evt.getKeyCode();
        if (keyCode == KeyEvent.VK_ESCAPE) action.processKeyEvent(evt);
        else if (keyCode == KeyEvent.VK_ENTER) invoke();
        else if (keyCode == KeyEvent.VK_UP) {
          int selected = list.getSelectedIndex();
          if (selected == 0) {
            list.setSelectedIndex(list.getModel().getSize() - 1);
            evt.consume();
          }
        } else if (keyCode == KeyEvent.VK_DOWN) {
          int selected = list.getSelectedIndex();
          if (selected == list.getModel().getSize() - 1) {
            list.setSelectedIndex(0);
            evt.consume();
          }
        }
      }
    }
  }
}
/**
 * Class that implements regular expression and literal search within jEdit buffers.
 *
 * <p>There are two main groups of methods in this class:
 *
 * <ul>
 *   <li>Property accessors - for changing search and replace settings.
 *   <li>Actions - for performing search and replace.
 * </ul>
 *
 * The "HyperSearch" and "Keep dialog" features, as reflected in checkbox options in the search
 * dialog, are not handled from within this class. If you wish to have these options set before the
 * search dialog appears, make a prior call to either or both of the following:
 *
 * <pre> jEdit.setBooleanProperty("search.hypersearch.toggle",true);
 * jEdit.setBooleanProperty("search.keepDialog.toggle",true);</pre>
 *
 * If you are not using the dialog to undertake a search or replace, you may call any of the search
 * and replace methods (including {@link #hyperSearch(View)}) without concern for the value of these
 * properties.
 *
 * @author Slava Pestov
 * @author John Gellene (API documentation)
 * @version $Id$
 */
public class SearchAndReplace {
  // {{{ Getters and setters

  // {{{ setSearchString() method
  /**
   * Sets the current search string.
   *
   * @param search The new search string
   */
  public static void setSearchString(String search) {
    if (search.equals(SearchAndReplace.search)) return;

    SearchAndReplace.search = search;
    matcher = null;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getSearchString() method
  /** Returns the current search string. */
  public static String getSearchString() {
    return search;
  } // }}}

  // {{{ setReplaceString() method
  /**
   * Sets the current replacement string.
   *
   * @param replace The new replacement string
   */
  public static void setReplaceString(String replace) {
    if (replace.equals(SearchAndReplace.replace)) return;

    SearchAndReplace.replace = replace;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getReplaceString() method
  /** Returns the current replacement string. */
  public static String getReplaceString() {
    return replace;
  } // }}}

  // {{{ setWholeWord() method
  /**
   * Sets the whole word flag.
   *
   * @param wholeWord True if only whole words should be searched, false otherwise
   * @since 4.5pre1
   */
  public static void setWholeWord(boolean wholeWord) {
    if (wholeWord == SearchAndReplace.wholeWord) return;

    SearchAndReplace.wholeWord = wholeWord;
    matcher = null;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ setIgnoreCase() method
  /**
   * Sets the ignore case flag.
   *
   * @param ignoreCase True if searches should be case insensitive, false otherwise
   */
  public static void setIgnoreCase(boolean ignoreCase) {
    if (ignoreCase == SearchAndReplace.ignoreCase) return;

    SearchAndReplace.ignoreCase = ignoreCase;
    matcher = null;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getWholeWord() method
  /**
   * Returns the state of the whole word flag.
   *
   * @return True if only whole words should be searched, false otherwise
   * @since 4.5pre1
   */
  public static boolean getWholeWord() {
    return wholeWord;
  } // }}}

  // {{{ getIgnoreCase() method
  /**
   * Returns the state of the ignore case flag.
   *
   * @return True if searches should be case insensitive, false otherwise
   */
  public static boolean getIgnoreCase() {
    return ignoreCase;
  } // }}}

  // {{{ setRegexp() method
  /**
   * Sets the state of the regular expression flag.
   *
   * @param regexp True if regular expression searches should be performed
   */
  public static void setRegexp(boolean regexp) {
    if (regexp == SearchAndReplace.regexp) return;

    SearchAndReplace.regexp = regexp;
    if (regexp && reverse) reverse = false;

    matcher = null;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getRegexp() method
  /**
   * Returns the state of the regular expression flag.
   *
   * @return True if regular expression searches should be performed
   */
  public static boolean getRegexp() {
    return regexp;
  } // }}}

  // {{{ setReverseSearch() method
  /**
   * Determines whether a reverse search will conducted from the current position to the beginning
   * of a buffer. Note that reverse search and regular expression search is mutually exclusive;
   * enabling one will disable the other.
   *
   * @param reverse True if searches should go backwards, false otherwise
   */
  public static void setReverseSearch(boolean reverse) {
    if (reverse == SearchAndReplace.reverse) return;

    SearchAndReplace.reverse = reverse;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getReverseSearch() method
  /**
   * Returns the state of the reverse search flag.
   *
   * @return True if searches should go backwards, false otherwise
   */
  public static boolean getReverseSearch() {
    return reverse;
  } // }}}

  // {{{ setBeanShellReplace() method
  /**
   * Sets the state of the BeanShell replace flag.
   *
   * @param beanshell True if the replace string is a BeanShell expression
   * @since jEdit 3.2pre2
   */
  public static void setBeanShellReplace(boolean beanshell) {
    if (beanshell == SearchAndReplace.beanshell) return;

    SearchAndReplace.beanshell = beanshell;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getBeanShellReplace() method
  /**
   * Returns the state of the BeanShell replace flag.
   *
   * @return True if the replace string is a BeanShell expression
   * @since jEdit 3.2pre2
   */
  public static boolean getBeanShellReplace() {
    return beanshell;
  } // }}}

  // {{{ setAutoWrap() method
  /**
   * Sets the state of the auto wrap around flag.
   *
   * @param wrap If true, the 'continue search from start' dialog will not be displayed
   * @since jEdit 3.2pre2
   */
  public static void setAutoWrapAround(boolean wrap) {
    if (wrap == SearchAndReplace.wrap) return;

    SearchAndReplace.wrap = wrap;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getAutoWrap() method
  /**
   * Returns the state of the auto wrap around flag.
   *
   * @since jEdit 3.2pre2
   */
  public static boolean getAutoWrapAround() {
    return wrap;
  } // }}}

  // {{{ setSearchMatcher() method
  /**
   * Sets a custom search string matcher. Note that calling {@link #setSearchString(String)}, {@link
   * #setWholeWord(boolean)}, {@link #setIgnoreCase(boolean)}, or {@link #setRegexp(boolean)} will
   * reset the matcher to the default.
   */
  public static void setSearchMatcher(SearchMatcher matcher) {
    SearchAndReplace.matcher = matcher;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getSearchMatcher() method
  /**
   * Returns the current search string matcher.
   *
   * @return a SearchMatcher or null if there is no search or if the matcher can match empty String
   * @exception IllegalArgumentException if regular expression search is enabled, the search string
   *     or replacement string is invalid
   * @since jEdit 4.1pre7
   */
  public static SearchMatcher getSearchMatcher() throws Exception {
    if (matcher != null) return matcher;

    if (search == null || "".equals(search)) return null;

    if (regexp) {
      Pattern re = Pattern.compile(search, PatternSearchMatcher.getFlag(ignoreCase));
      matcher = new PatternSearchMatcher(re, ignoreCase, wholeWord);
    } else matcher = new BoyerMooreSearchMatcher(search, ignoreCase, wholeWord);

    return matcher;
  } // }}}

  // {{{ setSearchFileSet() method
  /**
   * Sets the current search file set.
   *
   * @param fileset The file set to perform searches in
   * @see AllBufferSet
   * @see CurrentBufferSet
   * @see DirectoryListSet
   */
  public static void setSearchFileSet(SearchFileSet fileset) {
    SearchAndReplace.fileset = fileset;

    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ getSearchFileSet() method
  /** Returns the current search file set. */
  public static SearchFileSet getSearchFileSet() {
    return fileset;
  } // }}}

  // {{{ getSmartCaseReplace() method
  /**
   * Returns if the replacement string will assume the same case as each specific occurrence of the
   * search string.
   *
   * @since jEdit 4.2pre10
   */
  public static boolean getSmartCaseReplace() {
    return (replace != null && TextUtilities.getStringCase(replace) == TextUtilities.LOWER_CASE);
  } // }}}

  // }}}

  // {{{ Actions

  // {{{ hyperSearch() method
  /**
   * Performs a HyperSearch.
   *
   * @param view The view
   * @since jEdit 2.7pre3
   */
  public static boolean hyperSearch(View view) {
    return hyperSearch(view, false);
  } // }}}

  // {{{ hyperSearch() method
  /**
   * Performs a HyperSearch.
   *
   * @param view The view
   * @param selection If true, will only search in the current selection. Note that the file set
   *     must be the current buffer file set for this to work.
   * @since jEdit 4.0pre1
   */
  public static boolean hyperSearch(View view, boolean selection) {
    // component that will parent any dialog boxes
    Component comp = SearchDialog.getSearchDialog(view);
    if (comp == null) comp = view;

    record(view, "hyperSearch(view," + selection + ')', false, !selection);

    view.getDockableWindowManager().addDockableWindow(HyperSearchResults.NAME);
    HyperSearchResults results =
        (HyperSearchResults) view.getDockableWindowManager().getDockable(HyperSearchResults.NAME);
    results.searchStarted();

    try {
      SearchMatcher matcher = getSearchMatcher();
      if (matcher == null) {
        view.getToolkit().beep();
        results.searchFailed();
        return false;
      }

      Selection[] s;
      if (selection) {
        s = view.getTextArea().getSelection();
        if (s == null) {
          results.searchFailed();
          return false;
        }
      } else s = null;
      ThreadUtilities.runInBackground(new HyperSearchRequest(view, matcher, results, s));
      return true;
    } catch (Exception e) {
      results.searchFailed();
      handleError(comp, e);
      return false;
    }
  } // }}}

  // {{{ find() method
  /**
   * Finds the next occurrence of the search string.
   *
   * @param view The view
   * @return True if the operation was successful, false otherwise
   */
  public static boolean find(View view) {
    // component that will parent any dialog boxes
    Component comp = SearchDialog.getSearchDialog(view);
    if (comp == null || !comp.isShowing()) comp = view;

    String path = fileset.getNextFile(view, null);
    if (path == null) {
      GUIUtilities.error(comp, "empty-fileset", null);
      return false;
    }

    try {
      view.showWaitCursor();

      SearchMatcher matcher = getSearchMatcher();
      if (matcher == null) {
        view.getToolkit().beep();
        return false;
      }

      record(view, "find(view)", false, true);

      boolean repeat = false;
      loop:
      for (; ; ) {
        while (path != null) {
          Buffer buffer = jEdit.openTemporary(view, null, path, false);

          /* this is stupid and misleading.
           * but 'path' is not used anywhere except
           * the above line, and if this is done
           * after the 'continue', then we will
           * either hang, or be forced to duplicate
           * it inside the buffer == null, or add
           * a 'finally' clause. you decide which one's
           * worse. */
          if (reverse) {
            path = fileset.getPrevFile(view, path);
          } else {
            path = fileset.getNextFile(view, path);
          }

          if (buffer == null) continue loop;

          // Wait for the buffer to load
          if (!buffer.isLoaded()) VFSManager.waitForRequests();

          int start;

          if (view.getBuffer() == buffer && !repeat) {
            JEditTextArea textArea = view.getTextArea();
            Selection s = textArea.getSelectionAtOffset(textArea.getCaretPosition());
            if (s == null) start = textArea.getCaretPosition();
            else if (reverse) start = s.getStart();
            else start = s.getEnd();
          } else if (reverse) start = buffer.getLength();
          else start = 0;

          if (find(view, buffer, start, repeat, reverse)) return true;
        }

        if (repeat) {
          if (!BeanShell.isScriptRunning()) {
            view.getStatus().setMessageAndClear(jEdit.getProperty("view.status.search-not-found"));

            view.getToolkit().beep();
          }
          return false;
        }

        boolean restart;

        // if auto wrap is on, always restart search.
        // if auto wrap is off, and we're called from
        // a macro, stop search. If we're called
        // interactively, ask the user what to do.
        if (wrap) {
          if (!BeanShell.isScriptRunning()) {
            view.getStatus().setMessageAndClear(jEdit.getProperty("view.status.auto-wrap"));
            // beep if beep property set
            if (jEdit.getBooleanProperty("search.beepOnSearchAutoWrap")) {
              view.getToolkit().beep();
            }
          }
          restart = true;
        } else if (BeanShell.isScriptRunning()) {
          restart = false;
        } else {
          Integer[] args = {Integer.valueOf(reverse ? 1 : 0)};
          int result =
              GUIUtilities.confirm(
                  comp,
                  "keepsearching",
                  args,
                  JOptionPane.YES_NO_OPTION,
                  JOptionPane.QUESTION_MESSAGE);
          restart = (result == JOptionPane.YES_OPTION);
        }

        if (restart) {
          // start search from beginning
          path = fileset.getFirstFile(view);
          repeat = true;
        } else break loop;
      }
    } catch (Exception e) {
      handleError(comp, e);
    } finally {
      view.hideWaitCursor();
    }

    return false;
  } // }}}

  // {{{ find() method
  /**
   * Finds the next instance of the search string in the specified buffer.
   *
   * @param view The view
   * @param buffer The buffer
   * @param start Location where to start the search
   */
  public static boolean find(View view, Buffer buffer, int start) throws Exception {
    return find(view, buffer, start, false, false);
  } // }}}

  // {{{ find() method
  /**
   * Finds the next instance of the search string in the specified buffer.
   *
   * @param view The view
   * @param buffer The buffer
   * @param start Location where to start the search
   * @param firstTime See {@link
   *     SearchMatcher#nextMatch(CharSequence,boolean,boolean,boolean,boolean)}.
   * @since jEdit 4.1pre7
   */
  public static boolean find(
      View view, Buffer buffer, int start, boolean firstTime, boolean reverse) throws Exception {

    EditBus.send(new PositionChanging(view.getEditPane()));

    SearchMatcher matcher = getSearchMatcher();
    if (matcher == null) {
      view.getToolkit().beep();
      return false;
    }

    CharSequence text;
    boolean startOfLine;
    boolean endOfLine;
    if (reverse) {
      text = new ReverseCharSequence(buffer.getSegment(0, start));
      startOfLine = true;
      endOfLine = (buffer.getLineEndOffset(buffer.getLineOfOffset(start)) - 1 == start);
    } else {
      text = buffer.getSegment(start, buffer.getLength() - start);
      startOfLine = (buffer.getLineStartOffset(buffer.getLineOfOffset(start)) == start);
      endOfLine = true;
    }

    String noWordSep = buffer.getStringProperty("noWordSep");
    matcher.setNoWordSep(noWordSep);
    SearchMatcher.Match match = matcher.nextMatch(text, startOfLine, endOfLine, firstTime, reverse);
    if (match != null) {
      jEdit.commitTemporary(buffer);
      view.setBuffer(buffer, true);
      JEditTextArea textArea = view.getTextArea();

      if (reverse) {
        textArea.setSelection(new Selection.Range(start - match.end, start - match.start));
        // make sure end of match is visible
        textArea.scrollTo(start - match.start, false);
        textArea.moveCaretPosition(start - match.end);
      } else {
        textArea.setSelection(new Selection.Range(start + match.start, start + match.end));
        textArea.moveCaretPosition(start + match.end);
        // make sure start of match is visible
        textArea.scrollTo(start + match.start, false);
      }

      return true;
    } else return false;
  } // }}}

  // {{{ replace() method
  /**
   * Replaces the current selection with the replacement string.
   *
   * @param view The view
   * @return True if the operation was successful, false otherwise
   */
  public static boolean replace(View view) {
    // component that will parent any dialog boxes
    Component comp = SearchDialog.getSearchDialog(view);
    if (comp == null) comp = view;

    JEditTextArea textArea = view.getTextArea();

    Buffer buffer = view.getBuffer();
    if (!buffer.isEditable()) return false;

    boolean smartCaseReplace = getSmartCaseReplace();

    Selection[] selection = textArea.getSelection();
    if (selection.length == 0) {
      view.getToolkit().beep();
      return false;
    }

    record(view, "replace(view)", true, false);

    // a little hack for reverse replace and find
    int caret = textArea.getCaretPosition();
    Selection s = textArea.getSelectionAtOffset(caret);
    if (s != null) caret = s.getStart();

    try {
      buffer.beginCompoundEdit();

      SearchMatcher matcher = getSearchMatcher();
      if (matcher == null) return false;

      initReplace();

      int retVal = 0;

      for (int i = 0; i < selection.length; i++) {
        s = selection[i];

        retVal += replaceInSelection(view, textArea, buffer, matcher, smartCaseReplace, s);
      }

      if (reverse) {
        // so that Replace and Find continues from
        // the right location
        textArea.moveCaretPosition(caret);
      } else {
        s = textArea.getSelectionAtOffset(textArea.getCaretPosition());
        if (s != null) textArea.moveCaretPosition(s.getEnd());
      }

      if (!BeanShell.isScriptRunning()) {
        Object[] args = {Integer.valueOf(retVal), Integer.valueOf(1)};
        view.getStatus().setMessageAndClear(jEdit.getProperty("view.status.replace-all", args));
      }

      if (retVal == 0) {
        view.getToolkit().beep();
        return false;
      }

      return true;
    } catch (Exception e) {
      handleError(comp, e);
    } finally {
      buffer.endCompoundEdit();
    }

    return false;
  } // }}}

  // {{{ replace() method
  /**
   * Replaces text in the specified range with the replacement string.
   *
   * @param view The view
   * @param buffer The buffer
   * @param start The start offset
   * @param end The end offset
   * @return True if the operation was successful, false otherwise
   */
  public static boolean replace(View view, Buffer buffer, int start, int end) {
    if (!buffer.isEditable()) return false;

    // component that will parent any dialog boxes
    Component comp = SearchDialog.getSearchDialog(view);
    if (comp == null) comp = view;

    boolean smartCaseReplace = getSmartCaseReplace();

    try {
      buffer.beginCompoundEdit();

      SearchMatcher matcher = getSearchMatcher();
      if (matcher == null) return false;

      initReplace();

      int retVal = 0;

      retVal += _replace(view, buffer, matcher, start, end, smartCaseReplace);

      if (retVal != 0) return true;
    } catch (Exception e) {
      handleError(comp, e);
    } finally {
      buffer.endCompoundEdit();
    }

    return false;
  } // }}}

  // {{{ replaceAll() method
  /**
   * Replaces all occurrences of the search string with the replacement string.
   *
   * @param view The view
   * @return the number of modified files
   */
  public static boolean replaceAll(View view) {
    return replaceAll(view, false);
  } // }}}

  // {{{ replaceAll() method
  /**
   * Replaces all occurrences of the search string with the replacement string.
   *
   * @param view The view
   * @param dontOpenChangedFiles Whether to open changed files or to autosave them quietly
   * @return the number of modified files
   */
  public static boolean replaceAll(View view, boolean dontOpenChangedFiles) {
    // component that will parent any dialog boxes
    Component comp = SearchDialog.getSearchDialog(view);
    if (comp == null) comp = view;

    if (fileset.getFileCount(view) == 0) {
      GUIUtilities.error(comp, "empty-fileset", null);
      return false;
    }

    record(view, "replaceAll(view)", true, true);

    view.showWaitCursor();

    boolean smartCaseReplace = getSmartCaseReplace();

    int fileCount = 0;
    int occurCount = 0;
    try {
      SearchMatcher matcher = getSearchMatcher();
      if (matcher == null) return false;

      initReplace();

      String path = fileset.getFirstFile(view);
      loop:
      while (path != null) {
        Buffer buffer = jEdit.openTemporary(view, null, path, false);

        /* this is stupid and misleading.
         * but 'path' is not used anywhere except
         * the above line, and if this is done
         * after the 'continue', then we will
         * either hang, or be forced to duplicate
         * it inside the buffer == null, or add
         * a 'finally' clause. you decide which one's
         * worse. */
        path = fileset.getNextFile(view, path);

        if (buffer == null) continue loop;

        // Wait for buffer to finish loading
        if (buffer.isPerformingIO()) VFSManager.waitForRequests();

        if (!buffer.isEditable()) continue loop;

        // Leave buffer in a consistent state if
        // an error occurs
        int retVal = 0;

        try {
          buffer.beginCompoundEdit();
          retVal = _replace(view, buffer, matcher, 0, buffer.getLength(), smartCaseReplace);
        } finally {
          buffer.endCompoundEdit();
        }

        if (retVal != 0) {
          fileCount++;
          occurCount += retVal;
          if (dontOpenChangedFiles) {
            buffer.save(null, null);
          } else {
            jEdit.commitTemporary(buffer);
            jEdit.getBufferSetManager().addBuffer(view, buffer);
          }
        }
      }
    } catch (Exception e) {
      handleError(comp, e);
    } finally {
      view.hideWaitCursor();
    }

    /* Don't do this when playing a macro, cos it's annoying */
    if (!BeanShell.isScriptRunning()) {
      Object[] args = {Integer.valueOf(occurCount), Integer.valueOf(fileCount)};
      view.getStatus().setMessageAndClear(jEdit.getProperty("view.status.replace-all", args));
      if (occurCount == 0) view.getToolkit().beep();
    }

    return (fileCount != 0);
  } // }}}

  // }}}

  // {{{ escapeRegexp() method
  /**
   * Escapes characters with special meaning in a regexp.
   *
   * @param str the string to escape
   * @param multiline Should \n be escaped?
   * @return the string with escaped characters
   * @since jEdit 4.3pre1
   */
  public static String escapeRegexp(String str, boolean multiline) {
    return StandardUtilities.charsToEscapes(str, "\r\t\\()[]{}$^*+?|." + (multiline ? "" : "\n"));
  } // }}}

  // {{{ load() method
  /** Loads search and replace state from the properties. */
  public static void load() {
    search = jEdit.getProperty("search.find.value");
    replace = jEdit.getProperty("search.replace.value");
    wholeWord = jEdit.getBooleanProperty("search.wholeWord.toggle");
    ignoreCase = jEdit.getBooleanProperty("search.ignoreCase.toggle");
    regexp = jEdit.getBooleanProperty("search.regexp.toggle");
    beanshell = jEdit.getBooleanProperty("search.beanshell.toggle");
    wrap = jEdit.getBooleanProperty("search.wrap.toggle");

    fileset = new CurrentBufferSet();

    // Tags plugin likes to call this method at times other than
    // startup; so we need to fire a SearchSettingsChanged to
    // notify the search bar and so on.
    matcher = null;
    EditBus.send(new SearchSettingsChanged(null));
  } // }}}

  // {{{ save() method
  /** Saves search and replace state to the properties. */
  public static void save() {
    jEdit.setProperty("search.find.value", search);
    jEdit.setProperty("search.replace.value", replace);
    jEdit.setBooleanProperty("search.wholeWord.toggle", wholeWord);
    jEdit.setBooleanProperty("search.ignoreCase.toggle", ignoreCase);
    jEdit.setBooleanProperty("search.regexp.toggle", regexp);
    jEdit.setBooleanProperty("search.beanshell.toggle", beanshell);
    jEdit.setBooleanProperty("search.wrap.toggle", wrap);
  } // }}}

  // {{{ handleError() method
  static void handleError(Component comp, Exception e) {
    Log.log(Log.ERROR, SearchAndReplace.class, e);
    if (comp instanceof Dialog) {
      new TextAreaDialog((Dialog) comp, beanshell ? "searcherror-bsh" : "searcherror", e);
    } else {
      new TextAreaDialog((Frame) comp, beanshell ? "searcherror-bsh" : "searcherror", e);
    }
  } // }}}

  // {{{ Private members

  // {{{ Instance variables
  private static String search;
  private static String replace;
  private static BshMethod replaceMethod;
  private static NameSpace replaceNS =
      new NameSpace(
          BeanShell.getNameSpace(),
          BeanShell.getNameSpace().getClassManager(),
          "search and replace");
  private static boolean regexp;
  private static boolean wholeWord;
  private static boolean ignoreCase;
  private static boolean reverse;
  private static boolean beanshell;
  private static boolean wrap;
  private static SearchMatcher matcher;
  private static SearchFileSet fileset;
  // }}}

  // {{{ initReplace() method
  /** Set up BeanShell replace if necessary. */
  private static void initReplace() throws Exception {
    if (beanshell && replace.length() != 0) {
      String text;
      if (replace.trim().startsWith("{")) text = replace;
      else text = "return (" + replace + ");";
      replaceMethod = BeanShell.cacheBlock("replace", text, true);
    } else replaceMethod = null;
  } // }}}

  // {{{ record() method
  private static void record(
      View view, String action, boolean replaceAction, boolean recordFileSet) {
    Macros.Recorder recorder = view.getMacroRecorder();

    if (recorder != null) {
      recorder.record(
          "SearchAndReplace.setSearchString(\""
              + StandardUtilities.charsToEscapes(search)
              + "\");");

      if (replaceAction) {
        recorder.record(
            "SearchAndReplace.setReplaceString(\""
                + StandardUtilities.charsToEscapes(replace)
                + "\");");
        recorder.record("SearchAndReplace.setBeanShellReplace(" + beanshell + ");");
      } else {
        // only record this if doing a find next
        recorder.record("SearchAndReplace.setAutoWrapAround(" + wrap + ");");
        recorder.record("SearchAndReplace.setReverseSearch(" + reverse + ");");
      }

      recorder.record("SearchAndReplace.setWholeWord(" + wholeWord + ");");
      recorder.record("SearchAndReplace.setIgnoreCase(" + ignoreCase + ");");
      recorder.record("SearchAndReplace.setRegexp(" + regexp + ");");

      if (recordFileSet) {
        recorder.record("SearchAndReplace.setSearchFileSet(" + fileset.getCode() + ");");
      }

      recorder.record("SearchAndReplace." + action + ';');
    }
  } // }}}

  // {{{ replaceInSelection() method
  private static int replaceInSelection(
      View view,
      TextArea textArea,
      Buffer buffer,
      SearchMatcher matcher,
      boolean smartCaseReplace,
      Selection s)
      throws Exception {
    /* if an occurence occurs at the
    beginning of the selection, the
    selection start will get moved.
    this sucks, so we hack to avoid it. */
    int start = s.getStart();

    int returnValue;

    if (s instanceof Selection.Range) {
      returnValue = _replace(view, buffer, matcher, s.getStart(), s.getEnd(), smartCaseReplace);

      textArea.removeFromSelection(s);
      textArea.addToSelection(new Selection.Range(start, s.getEnd()));
    } else if (s instanceof Selection.Rect) {
      Selection.Rect rect = (Selection.Rect) s;
      int startCol = rect.getStartColumn(buffer);
      int endCol = rect.getEndColumn(buffer);

      returnValue = 0;
      for (int j = s.getStartLine(); j <= s.getEndLine(); j++) {
        returnValue +=
            _replace(
                view,
                buffer,
                matcher,
                getColumnOnOtherLine(buffer, j, startCol),
                getColumnOnOtherLine(buffer, j, endCol),
                smartCaseReplace);
      }
      textArea.addToSelection(new Selection.Rect(start, s.getEnd()));
    } else throw new RuntimeException("Unsupported: " + s);

    return returnValue;
  } // }}}

  // {{{ _replace() method
  /**
   * Replaces all occurrences of the search string with the replacement string.
   *
   * @param view The view
   * @param buffer The buffer
   * @param start The start offset
   * @param end The end offset
   * @param matcher The search matcher to use
   * @param smartCaseReplace See user's guide
   * @return The number of occurrences replaced
   */
  private static int _replace(
      View view,
      JEditBuffer buffer,
      SearchMatcher matcher,
      int start,
      int end,
      boolean smartCaseReplace)
      throws Exception {
    String noWordSep = buffer.getStringProperty("noWordSep");
    matcher.setNoWordSep(noWordSep);
    int occurCount = 0;

    boolean endOfLine = (buffer.getLineEndOffset(buffer.getLineOfOffset(end)) - 1 == end);

    int offset = start;
    loop:
    for (int counter = 0; ; counter++) {
      boolean startOfLine = (buffer.getLineStartOffset(buffer.getLineOfOffset(offset)) == offset);

      CharSequence text = buffer.getSegment(offset, end - offset);
      SearchMatcher.Match occur =
          matcher.nextMatch(text, startOfLine, endOfLine, counter == 0, false);
      if (occur == null) break loop;

      CharSequence found = text.subSequence(occur.start, occur.end);

      int length = replaceOne(view, buffer, occur, offset, found, smartCaseReplace);
      if (length == -1) offset += occur.end;
      else {
        offset += occur.start + length;
        end += (length - found.length());
        occurCount++;
      }
    }

    return occurCount;
  } // }}}

  // {{{ replaceOne() method
  /** Replace one occurrence of the search string with the replacement string. */
  private static int replaceOne(
      View view,
      JEditBuffer buffer,
      SearchMatcher.Match occur,
      int offset,
      CharSequence found,
      boolean smartCaseReplace)
      throws Exception {
    String subst = replaceOne(view, buffer, occur, found);
    if (smartCaseReplace && ignoreCase) {
      int strCase = TextUtilities.getStringCase(found);
      if (strCase == TextUtilities.LOWER_CASE) subst = subst.toLowerCase();
      else if (strCase == TextUtilities.UPPER_CASE) subst = subst.toUpperCase();
      else if (strCase == TextUtilities.TITLE_CASE) subst = TextUtilities.toTitleCase(subst);
    }

    if (subst != null) {
      int start = offset + occur.start;
      int end = offset + occur.end;

      if (end - start > 0) buffer.remove(start, end - start);
      buffer.insert(start, subst);
      return subst.length();
    } else return -1;
  } // }}}

  // {{{ replaceOne() method
  private static String replaceOne(
      View view, JEditBuffer buffer, SearchMatcher.Match occur, CharSequence found)
      throws Exception {
    if (regexp) {
      if (replaceMethod != null) return regexpBeanShellReplace(view, buffer, occur);
      else return regexpReplace(occur, found);
    } else {
      if (replaceMethod != null) return literalBeanShellReplace(view, buffer, found);
      else return replace;
    }
  } // }}}

  // {{{ regexpBeanShellReplace() method
  private static String regexpBeanShellReplace(
      View view, JEditBuffer buffer, SearchMatcher.Match occur) throws Exception {
    replaceNS.setVariable("buffer", buffer, false);
    for (int i = 0; i < occur.substitutions.length; i++) {
      replaceNS.setVariable("_" + i, occur.substitutions[i]);
    }

    Object obj = BeanShell.runCachedBlock(replaceMethod, view, replaceNS);

    for (int i = 0; i < occur.substitutions.length; i++) {
      replaceNS.setVariable("_" + i, null, false);
    }
    // Not really necessary because it is already cleared in the end of
    // BeanShell.runCachedBlock()
    replaceNS.setVariable("buffer", null, false);

    if (obj == null) return "";
    else return obj.toString();
  } // }}}

  // {{{ regexpReplace() method
  private static String regexpReplace(SearchMatcher.Match occur, CharSequence found)
      throws Exception {
    StringBuilder buf = new StringBuilder();

    for (int i = 0; i < replace.length(); i++) {
      char ch = replace.charAt(i);
      switch (ch) {
        case '$':
          if (i == replace.length() - 1) {
            // last character of the replace string,
            // it is not a capturing group
            buf.append(ch);
            break;
          }

          ch = replace.charAt(++i);
          if (ch == '$') {
            // It was $$, so it is an escaped $
            buf.append('$');
          } else if (ch == '0') {
            // $0 meaning the first capturing group :
            // the found value
            buf.append(found);
          } else if (Character.isDigit(ch)) {
            int n = ch - '0';
            while (i < replace.length() - 1) {
              ch = replace.charAt(++i);
              if (Character.isDigit(ch)) {
                n = n * 10 + (ch - '0');
              } else {
                // The character is not
                // a digit, going back and
                // end loop
                i--;
                break;
              }
            }
            if (n < occur.substitutions.length) {
              String subs = occur.substitutions[n];
              if (subs != null) buf.append(subs);
            }
          }
          break;
        case '\\':
          if (i == replace.length() - 1) {
            buf.append('\\');
            break;
          }
          ch = replace.charAt(++i);
          switch (ch) {
            case 'n':
              buf.append('\n');
              break;
            case 't':
              buf.append('\t');
              break;
            default:
              buf.append(ch);
              break;
          }
          break;
        default:
          buf.append(ch);
          break;
      }
    }

    return buf.toString();
  } // }}}

  // {{{ literalBeanShellReplace() method
  private static String literalBeanShellReplace(View view, JEditBuffer buffer, CharSequence found)
      throws Exception {
    replaceNS.setVariable("buffer", buffer);
    replaceNS.setVariable("_0", found);
    Object obj = BeanShell.runCachedBlock(replaceMethod, view, replaceNS);

    replaceNS.setVariable("_0", null, false);
    // Not really necessary because it is already cleared in the end of
    // BeanShell.runCachedBlock()
    replaceNS.setVariable("buffer", null, false);

    if (obj == null) return "";
    else return obj.toString();
  } // }}}

  // {{{ getColumnOnOtherLine() method
  /** Should be somewhere else... */
  private static int getColumnOnOtherLine(Buffer buffer, int line, int col) {
    int returnValue = buffer.getOffsetOfVirtualColumn(line, col, null);
    if (returnValue == -1) return buffer.getLineEndOffset(line) - 1;
    else return buffer.getLineStartOffset(line) + returnValue;
  } // }}}

  // }}}
}