/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed 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 com.intellij.find;
import com.intellij.execution.impl.ConsoleViewUtil;
import com.intellij.find.editorHeaderActions.*;
import com.intellij.find.impl.livePreview.LivePreviewController;
import com.intellij.find.impl.livePreview.SearchResults;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
import com.intellij.openapi.actionSystem.ex.DefaultCustomComponentAction;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.EditorFactoryAdapter;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.editor.event.SelectionEvent;
import com.intellij.openapi.editor.event.SelectionListener;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.components.labels.LinkLabel;
import com.intellij.ui.components.labels.LinkListener;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ui.update.Activatable;
import com.intellij.util.ui.update.UiNotifyConnector;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.regex.Pattern;
/**
* @author max, andrey.zaytsev
*/
public class EditorSearchSession implements SearchSession,
DataProvider,
SelectionListener,
SearchResults.SearchResultsListener,
SearchReplaceComponent.Listener {
public static DataKey<EditorSearchSession> SESSION_KEY = DataKey.create("EditorSearchSession");
private final Editor myEditor;
private final LivePreviewController myLivePreviewController;
private final SearchResults mySearchResults;
private final FindModel myFindModel;
private final SearchReplaceComponent myComponent;
private final LinkLabel<Object> myClickToHighlightLabel = new LinkLabel<>("Click to highlight", null, new LinkListener<Object>() {
@Override
public void linkSelected(LinkLabel aSource, Object aLinkData) {
setMatchesLimit(Integer.MAX_VALUE);
updateResults(true);
}
});
private final Disposable myDisposable = Disposer.newDisposable(EditorSearchSession.class.getName());
public EditorSearchSession(@NotNull Editor editor, Project project) {
this(editor, project, createDefaultFindModel(project, editor));
}
public EditorSearchSession(@NotNull final Editor editor, Project project, FindModel findModel) {
assert !editor.isDisposed();
myClickToHighlightLabel.setVisible(false);
myFindModel = findModel;
myEditor = editor;
mySearchResults = new SearchResults(myEditor, project);
myLivePreviewController = new LivePreviewController(mySearchResults, this, myDisposable);
myComponent = SearchReplaceComponent
.buildFor(project, myEditor.getContentComponent())
.addPrimarySearchActions(new PrevOccurrenceAction(),
new NextOccurrenceAction(),
new FindAllAction(),
new AnSeparator(),
new AddOccurrenceAction(),
new RemoveOccurrenceAction(),
new SelectAllAction(),
new AnSeparator())
.addSecondarySearchActions(new ToggleInCommentsAction(),
new ToggleInLiteralsOnlyAction(),
new ToggleExceptCommentsAction(),
new ToggleExceptLiteralsAction(),
new ToggleExceptCommentsAndLiteralsAction())
.addExtraSearchActions(new ToggleMatchCase(),
new ToggleRegex(),
new ToggleWholeWordsOnlyAction(),
new StatusTextAction(),
new DefaultCustomComponentAction(myClickToHighlightLabel))
.addSearchFieldActions(new RestorePreviousSettingsAction())
.addPrimaryReplaceActions(new ReplaceAction(),
new ReplaceAllAction(),
new ExcludeAction())
.addExtraReplaceAction(new TogglePreserveCaseAction(),
new ToggleSelectionOnlyAction())
.addReplaceFieldActions(new PrevOccurrenceAction(false),
new NextOccurrenceAction(false))
.withDataProvider(this)
.withCloseAction(() -> close())
.withReplaceAction(() -> replaceCurrent())
.withSecondarySearchActionsIsModifiedGetter(() -> myFindModel.getSearchContext() != FindModel.SearchContext.ANY)
.build();
myComponent.addListener(this);
new UiNotifyConnector(myComponent, new Activatable() {
@Override
public void showNotify() {
initLivePreview();
}
@Override
public void hideNotify() {
myLivePreviewController.off();
mySearchResults.removeListener(EditorSearchSession.this);
}
});
new SwitchToFind(getComponent());
new SwitchToReplace(getComponent());
myFindModel.addObserver(new FindModel.FindModelObserver() {
@Override
public void findModelChanged(FindModel findModel) {
String stringToFind = myFindModel.getStringToFind();
if (!wholeWordsApplicable(stringToFind)) {
myFindModel.setWholeWordsOnly(false);
}
updateUIWithFindModel();
mySearchResults.clear();
updateResults(true);
FindUtil.updateFindInFileModel(getProject(), myFindModel, !ConsoleViewUtil.isConsoleViewEditor(editor));
}
});
updateUIWithFindModel();
if (ApplicationManager.getApplication().isUnitTestMode()) {
initLivePreview();
}
updateMultiLineStateIfNeed();
EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() {
@Override
public void editorReleased(@NotNull EditorFactoryEvent event) {
if (event.getEditor() == myEditor) {
Disposer.dispose(myDisposable);
myLivePreviewController.dispose();
}
}
}, myDisposable);
}
@Nullable
public static EditorSearchSession get(@Nullable Editor editor) {
JComponent headerComponent = editor != null ? editor.getHeaderComponent() : null;
SearchReplaceComponent searchReplaceComponent = ObjectUtils.tryCast(headerComponent, SearchReplaceComponent.class);
return searchReplaceComponent != null ? SESSION_KEY.getData(searchReplaceComponent) : null;
}
@NotNull
public static EditorSearchSession start(@NotNull Editor editor, @Nullable Project project) {
EditorSearchSession session = new EditorSearchSession(editor, project);
editor.setHeaderComponent(session.getComponent());
return session;
}
@NotNull
public static EditorSearchSession start(@NotNull Editor editor, @NotNull FindModel findModel, @Nullable Project project) {
EditorSearchSession session = new EditorSearchSession(editor, project, findModel);
editor.setHeaderComponent(session.getComponent());
return session;
}
@NotNull
@Override
public SearchReplaceComponent getComponent() {
return myComponent;
}
public Project getProject() {
return myComponent.getProject();
}
private static FindModel createDefaultFindModel(Project project, Editor editor) {
FindModel findModel = new FindModel();
findModel.copyFrom(FindManager.getInstance(project).getFindInFileModel());
if (editor.getSelectionModel().hasSelection()) {
String selectedText = editor.getSelectionModel().getSelectedText();
if (selectedText != null) {
findModel.setStringToFind(selectedText);
}
}
findModel.setPromptOnReplace(false);
return findModel;
}
@Override
@Nullable
public Object getData(@NonNls final String dataId) {
if (SearchSession.KEY.is(dataId)) {
return this;
}
if (SESSION_KEY.is(dataId)) {
return this;
}
if (CommonDataKeys.EDITOR_EVEN_IF_INACTIVE.is(dataId)) {
return myEditor;
}
return null;
}
@Override
public void searchResultsUpdated(SearchResults sr) {
if (sr.getFindModel() == null) return;
if (myComponent.getSearchTextComponent().getText().isEmpty()) {
updateUIWithEmptyResults();
} else {
int matches = sr.getMatchesCount();
boolean tooManyMatches = matches > mySearchResults.getMatchesLimit();
myComponent.setStatusText(tooManyMatches
? ApplicationBundle.message("editorsearch.toomuch", mySearchResults.getMatchesLimit())
: ApplicationBundle.message("editorsearch.matches", matches));
myClickToHighlightLabel.setVisible(tooManyMatches);
if (!tooManyMatches && matches <= 0) {
myComponent.setNotFoundBackground();
}
else {
myComponent.setRegularBackground();
}
}
myComponent.updateActions();
}
@Override
public void cursorMoved() {
myComponent.updateActions();
}
@Override
public void updateFinished() {
}
@Override
public void searchFieldDocumentChanged() {
setMatchesLimit(LivePreviewController.MATCHES_LIMIT);
String text = myComponent.getSearchTextComponent().getText();
myFindModel.setStringToFind(text);
if (!StringUtil.isEmpty(text)) {
updateResults(true);
}
else {
nothingToSearchFor();
}
updateMultiLineStateIfNeed();
}
private void updateMultiLineStateIfNeed() {
myFindModel.setMultiline(myComponent.getSearchTextComponent().getText().contains("\n") ||
myComponent.getReplaceTextComponent().getText().contains("\n"));
}
@Override
public void replaceFieldDocumentChanged() {
setMatchesLimit(LivePreviewController.MATCHES_LIMIT);
myFindModel.setStringToReplace(myComponent.getReplaceTextComponent().getText());
updateMultiLineStateIfNeed();
}
@Override
public void multilineStateChanged() {
myFindModel.setMultiline(myComponent.isMultiline());
}
@NotNull
@Override
public FindModel getFindModel() {
return myFindModel;
}
@Override
public boolean hasMatches() {
return mySearchResults != null && mySearchResults.hasMatches();
}
@Override
public void searchForward() {
moveCursor(SearchResults.Direction.DOWN);
addTextToRecent(myComponent.getSearchTextComponent());
}
@Override
public void searchBackward() {
moveCursor(SearchResults.Direction.UP);
addTextToRecent(myComponent.getSearchTextComponent());
}
public void updateUIWithFindModel() {
myComponent.update(myFindModel.getStringToFind(),
myFindModel.getStringToReplace(),
myFindModel.isReplaceState(),
myFindModel.isMultiline());
myLivePreviewController.setTrackingSelection(!myFindModel.isGlobal());
}
private static boolean wholeWordsApplicable(String stringToFind) {
return !stringToFind.startsWith(" ") &&
!stringToFind.startsWith("\t") &&
!stringToFind.endsWith(" ") &&
!stringToFind.endsWith("\t");
}
private void setMatchesLimit(int value) {
mySearchResults.setMatchesLimit(value);
}
public void replaceCurrent() {
if (mySearchResults.getCursor() != null) {
myLivePreviewController.performReplace();
}
}
public void addTextToRecent(JTextComponent textField) {
myComponent.addTextToRecent(textField);
}
@Override
public void selectionChanged(SelectionEvent e) {
updateResults(false);
}
private void moveCursor(SearchResults.Direction direction) {
myLivePreviewController.moveCursor(direction);
}
@Override
public void close() {
IdeFocusManager.getInstance(getProject()).requestFocus(myEditor.getContentComponent(), false);
myLivePreviewController.dispose();
myEditor.setHeaderComponent(null);
}
private void initLivePreview() {
myLivePreviewController.on();
myLivePreviewController.setUserActivityDelay(0);
updateResults(false);
myLivePreviewController.setUserActivityDelay(LivePreviewController.USER_ACTIVITY_TRIGGERING_DELAY);
mySearchResults.addListener(this);
}
private void updateResults(final boolean allowedToChangedEditorSelection) {
final String text = myFindModel.getStringToFind();
if (text.length() == 0) {
nothingToSearchFor();
}
else {
if (myFindModel.isRegularExpressions()) {
try {
//noinspection ResultOfMethodCallIgnored
Pattern.compile(text);
}
catch (Exception e) {
myComponent.setNotFoundBackground();
myClickToHighlightLabel.setVisible(false);
mySearchResults.clear();
myComponent.setStatusText(INCORRECT_REGEX_MESSAGE);
return;
}
}
final FindManager findManager = FindManager.getInstance(getProject());
if (allowedToChangedEditorSelection) {
findManager.setFindWasPerformed();
FindModel copy = new FindModel();
copy.copyFrom(myFindModel);
copy.setReplaceState(false);
findManager.setFindNextModel(copy);
}
if (myLivePreviewController != null) {
myLivePreviewController.updateInBackground(myFindModel, allowedToChangedEditorSelection);
}
}
}
private void nothingToSearchFor() {
updateUIWithEmptyResults();
if (mySearchResults != null) {
mySearchResults.clear();
}
}
private void updateUIWithEmptyResults() {
myComponent.setRegularBackground();
myComponent.setStatusText("");
myClickToHighlightLabel.setVisible(false);
}
public String getTextInField() {
return myComponent.getSearchTextComponent().getText();
}
public void setTextInField(final String text) {
myComponent.getSearchTextComponent().setText(text);
myFindModel.setStringToFind(text);
}
public void selectAllOccurrences() {
FindUtil.selectSearchResultsInEditor(myEditor, mySearchResults.getOccurrences().iterator(), -1);
}
public void removeOccurrence() {
mySearchResults.prevOccurrence(true);
}
public void addNextOccurrence() {
mySearchResults.nextOccurrence(true);
}
public void clearUndoInTextFields() {
myComponent.resetUndoRedoActions();
}
private static abstract class ButtonAction extends DumbAwareAction implements CustomComponentAction, ActionListener {
private final String myTitle;
private final char myMnemonic;
protected ButtonAction(@NotNull String title, char mnemonic) {
myTitle = title;
myMnemonic = mnemonic;
}
@Override
public JComponent createCustomComponent(Presentation presentation) {
JButton button = new JButton(myTitle);
button.setFocusable(false);
if (!UISettings.getInstance().DISABLE_MNEMONICS_IN_CONTROLS) {
button.setMnemonic(myMnemonic);
}
button.addActionListener(this);
return button;
}
@Override
public final void update(AnActionEvent e) {
JButton button = (JButton)e.getPresentation().getClientProperty(CUSTOM_COMPONENT_PROPERTY);
if (button != null) {
update(button);
}
}
@Override
public final void actionPerformed(AnActionEvent e) {
onClick();
}
@Override
public final void actionPerformed(ActionEvent e) {
onClick();
}
protected abstract void update(@NotNull JButton button);
protected abstract void onClick();
}
private class ReplaceAction extends ButtonAction {
public ReplaceAction() {
super("Replace", 'p');
}
@Override
protected void update(@NotNull JButton button) {
button.setEnabled(mySearchResults.hasMatches());
}
@Override
protected void onClick() {
replaceCurrent();
}
}
private class ReplaceAllAction extends ButtonAction {
public ReplaceAllAction() {
super("Replace all", 'a');
}
@Override
protected void update(@NotNull JButton button) {
button.setEnabled(mySearchResults.hasMatches());
}
@Override
protected void onClick() {
myLivePreviewController.performReplaceAll();
}
}
private class ExcludeAction extends ButtonAction {
public ExcludeAction() {
super("", 'l');
}
@Override
protected void update(@NotNull JButton button) {
FindResult cursor = mySearchResults.getCursor();
button.setEnabled(cursor != null);
button.setText(cursor != null && mySearchResults.isExcluded(cursor) ? "Include" : "Exclude");
}
@Override
protected void onClick() {
myLivePreviewController.exclude();
moveCursor(SearchResults.Direction.DOWN);
}
}
}