diff --git a/platform/lang-impl/src/com/intellij/find/EditorSearchComponent.java b/platform/lang-impl/src/com/intellij/find/EditorSearchComponent.java index ba6491c7c18b..bfd283667e94 100644 --- a/platform/lang-impl/src/com/intellij/find/EditorSearchComponent.java +++ b/platform/lang-impl/src/com/intellij/find/EditorSearchComponent.java @@ -21,9 +21,10 @@ package com.intellij.find; import com.intellij.featureStatistics.FeatureUsageTracker; import com.intellij.find.impl.FindManagerImpl; -import com.intellij.find.impl.LiveOccurrence; -import com.intellij.find.impl.LivePreview; -import com.intellij.find.impl.LivePreviewControllerBase; +import com.intellij.find.impl.livePreview.LiveOccurrence; +import com.intellij.find.impl.livePreview.LivePreview; +import com.intellij.find.impl.livePreview.LivePreviewControllerBase; +import com.intellij.find.impl.livePreview.SearchResults; import com.intellij.ide.ui.UISettings; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; @@ -38,7 +39,6 @@ import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.PopupChooserBuilder; import com.intellij.openapi.util.IconLoader; -import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; @@ -69,8 +69,8 @@ import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; -public class EditorSearchComponent extends JPanel implements DataProvider, LivePreview.CursorListener, SelectionListener { - private static final int MATCHES_LIMIT = 100; +public class EditorSearchComponent extends JPanel implements DataProvider, SelectionListener, SearchResults.SearchResultsListener { + private static final int MATCHES_LIMIT = 10000; private final JLabel myMatchInfoLabel; private final LinkLabel myClickToHighlightLabel; private final Project myProject; @@ -95,51 +95,14 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP private final JCheckBox myCbInComments; private final JCheckBox myCbInLiterals; - private final LivePreviewControllerBase myLivePreviewController = new LivePreviewControllerBase() { - @Override - public void getFocusBack() { - mySearchField.requestFocus(); - } - - @Override - public Editor getEditor(Ref needToUpdate) { - needToUpdate.set(true); - return myEditor; - } - - @Override - public void searchEndsWith(int count) { - if (count <= getMatchesLimit()) { - myClickToHighlightLabel.setVisible(false); - - if (count > 0) { - setRegularBackground(); - if (count > 1) { - myMatchInfoLabel.setText(count + " matches"); - } - else { - myMatchInfoLabel.setText("1 match"); - } - } - else { - setNotFoundBackground(); - myMatchInfoLabel.setText("No matches"); - } - } - else { - setRegularBackground(); - myMatchInfoLabel.setText("More than 100 matches"); - myClickToHighlightLabel.setVisible(true); - boldMatchInfo(); - } - } - }; + private final LivePreviewControllerBase myLivePreviewController; private final LivePreview myLivePreview; private boolean myIsReplace; private boolean myListeningSelection = false; private boolean myToChangeSelection = true; + private SearchResults mySearchResults; @Nullable public Object getData(@NonNls final String dataId) { @@ -153,12 +116,71 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP this(e, p, false); } + @Override + public void searchResultsUpdated(SearchResults sr) { + int count = sr.getActualFound(); + if (mySearchField.getText().isEmpty()) { + nothingToSearchFor(); + return; + } + + if (count <= mySearchResults.getMatchesLimit()) { + myClickToHighlightLabel.setVisible(false); + + if (count > 0) { + setRegularBackground(); + if (count > 1) { + myMatchInfoLabel.setText(count + " matches"); + } + else { + myMatchInfoLabel.setText("1 match"); + } + } + else { + setNotFoundBackground(); + myMatchInfoLabel.setText("No matches"); + } + } + else { + setRegularBackground(); + myMatchInfoLabel.setText("More than " + mySearchResults.getMatchesLimit() + " matches"); + myClickToHighlightLabel.setVisible(true); + boldMatchInfo(); + } + + updateSelection(); + } + + @Override + public void cursorMoved() { + updateSelection(); + } + + private void updateSelection() { + if (myToChangeSelection && (mySelectionOnly == null || !mySelectionOnly.isSelected())) { + LiveOccurrence cursor = mySearchResults.getCursor(); + if (cursor != null) { + TextRange range = cursor.getPrimaryRange(); + myEditor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + + myEditor.getCaretModel().moveToOffset(range.getEndOffset()); + myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + myEditor.getCaretModel().moveToOffset(range.getStartOffset()); + myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + myToChangeSelection = false; + } + } + + @Override + public void editorChanged(SearchResults sr, Editor oldEditor) { } + public EditorSearchComponent(final Editor editor, final Project project, boolean isReplace) { super(new BorderLayout(0, 0)); myIsReplace = isReplace; - setMatchesLimit(MATCHES_LIMIT); GRADIENT_C1 = getBackground(); GRADIENT_C2 = new Color(Math.max(0, GRADIENT_C1.getRed() - 0x18), Math.max(0, GRADIENT_C1.getGreen() - 0x18), Math.max(0, GRADIENT_C1.getBlue() - 0x18)); @@ -166,9 +188,18 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP myProject = project; myEditor = editor; - myLivePreview = new LivePreview(myEditor.getProject(), 0); - myLivePreview.addCursorListener(this); - myLivePreview.setDelegate(myLivePreviewController); + mySearchResults = new SearchResults(myEditor); + myLivePreview = new LivePreview(mySearchResults); + + myLivePreviewController = new LivePreviewControllerBase(mySearchResults, myLivePreview) { + @Override + public void getFocusBack() { + mySearchField.requestFocus(); + } + }; + mySearchResults.addListener(this); + setMatchesLimit(MATCHES_LIMIT); + JPanel leadPanel = createLeadPane(); add(leadPanel, BorderLayout.WEST); @@ -338,7 +369,7 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP } private void setMatchesLimit(int value) { - myLivePreviewController.setMatchesLimit(value); + mySearchResults.setMatchesLimit(value); } private void configureReplacementPane() { @@ -483,24 +514,6 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP } } - @Override - public void cursorMoved() { - if (myToChangeSelection && (mySelectionOnly == null || !mySelectionOnly.isSelected())) { - LiveOccurrence cursor = myLivePreview.getCursor(); - if (cursor != null) { - TextRange range = cursor.getPrimaryRange(); - myEditor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); - - myEditor.getCaretModel().moveToOffset(range.getEndOffset()); - myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); - - myEditor.getCaretModel().moveToOffset(range.getStartOffset()); - myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); - } - myToChangeSelection = false; - } - } - @Override public void selectionChanged(SelectionEvent e) { updateResults(true); @@ -509,9 +522,9 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP public void moveCursor(boolean forwardOrBackward) { myToChangeSelection = true; if (forwardOrBackward) { - myLivePreview.nextOccurrence(); + mySearchResults.nextOccurrence(); } else { - myLivePreview.prevOccurrence(); + mySearchResults.prevOccurrence(); } } @@ -555,7 +568,7 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP myEditor.getDocument().addDocumentListener(myDocumentListener); if (myLivePreview != null) { - myLivePreview.update(); + myLivePreviewController.updateInBackground(mySearchResults.getFindModel()); } } @@ -576,12 +589,7 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP myMatchInfoLabel.setFont(myMatchInfoLabel.getFont().deriveFont(Font.PLAIN)); final String text = mySearchField.getText(); if (text.length() == 0) { - setRegularBackground(); - myMatchInfoLabel.setText(""); - myClickToHighlightLabel.setVisible(false); - if (myLivePreview != null) { - myLivePreview.cleanUp(); - } + nothingToSearchFor(); } else { final FindModel model = new FindModel(); @@ -628,9 +636,17 @@ public class EditorSearchComponent extends JPanel implements DataProvider, LiveP model.setPreserveCase(myPreserveCase.isEnabled() && myPreserveCase.isSelected()); } myToChangeSelection = allowedToChangedEditorSelection; - myLivePreviewController.setFindModel(model); - myLivePreview.update(); + myLivePreviewController.updateInBackground(model); + } + } + + private void nothingToSearchFor() { + setRegularBackground(); + myMatchInfoLabel.setText(""); + myClickToHighlightLabel.setVisible(false); + if (myLivePreview != null) { + myLivePreview.cleanUp(); } } diff --git a/platform/lang-impl/src/com/intellij/find/FindUtil.java b/platform/lang-impl/src/com/intellij/find/FindUtil.java index 1e0808379486..03d0f56d5cbe 100644 --- a/platform/lang-impl/src/com/intellij/find/FindUtil.java +++ b/platform/lang-impl/src/com/intellij/find/FindUtil.java @@ -410,6 +410,15 @@ public class FindUtil { } public static boolean replace(Project project, Editor editor, int offset, FindModel model) { + return replace(project, editor, offset, model, new ReplaceDelegate() { + @Override + public boolean shouldReplace(TextRange range, String replace) { + return true; + } + }); + } + + public static boolean replace(Project project, Editor editor, int offset, FindModel model, ReplaceDelegate delegate) { Document document = editor.getDocument(); if (!FileDocumentManager.getInstance().requestWriting(document, project)) { @@ -423,7 +432,7 @@ public class FindUtil { ((DocumentEx) document).setInBulkUpdate(true); } try { - toPrompt = doReplace(project, editor, model, document, offset, toPrompt); + toPrompt = doReplace(project, editor, model, document, offset, toPrompt, delegate); } catch (ReadOnlyFragmentModificationException e) { EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(document).handle(e); @@ -438,7 +447,8 @@ public class FindUtil { return true; } - private static boolean doReplace(Project project, Editor editor, FindModel model, final Document document, int caretOffset, boolean toPrompt) { + private static boolean doReplace(Project project, Editor editor, FindModel model, final Document document, int caretOffset, + boolean toPrompt, ReplaceDelegate delegate) { FindManager findManager = FindManager.getInstance(project); model = (FindModel)model.clone(); int occurrences = 0; @@ -517,7 +527,11 @@ public class FindUtil { TextRange range = pair.getFirst(); String replace = pair.getSecond(); newText.append(text, offsetBefore, range.getStartOffset()); //before change - newText.append(replace); + if (delegate.shouldReplace(range, replace)) { + newText.append(replace); + } else { + newText.append(text.subSequence(range.getStartOffset(), range.getEndOffset())); + } offsetBefore = range.getEndOffset(); if (offsetBefore < caretOffset) { caretOffset += replace.length() - range.getLength(); @@ -761,4 +775,8 @@ public class FindUtil { editor.getCaretModel().moveToLogicalPosition(pos); editor.getScrollingModel().scrollToCaret(scrollType); } + + public interface ReplaceDelegate { + boolean shouldReplace(TextRange range, String replace) ; + } } diff --git a/platform/lang-impl/src/com/intellij/find/impl/FindDialog.java b/platform/lang-impl/src/com/intellij/find/impl/FindDialog.java index 2bcee259eec9..0f818f3e59f6 100644 --- a/platform/lang-impl/src/com/intellij/find/impl/FindDialog.java +++ b/platform/lang-impl/src/com/intellij/find/impl/FindDialog.java @@ -19,7 +19,6 @@ package com.intellij.find.impl; import com.intellij.CommonBundle; import com.intellij.find.FindBundle; -import com.intellij.find.FindManager; import com.intellij.find.FindModel; import com.intellij.find.FindSettings; import com.intellij.ide.util.scopeChooser.ScopeChooserCombo; @@ -108,9 +107,6 @@ class FindDialog extends DialogWrapper { private JRadioButton myRbCustomScope; private ScopeChooserCombo myScopeCombo; - private LivePreviewController myLivePreviewController; - - public FindDialog(Project project, FindModel model, Runnable myOkHandler){ super(project, true); myProject = project; @@ -139,7 +135,7 @@ class FindDialog extends DialogWrapper { setOKButtonIcon(IconLoader.getIcon("/actions/find.png")); init(); initByModel(); - myLivePreviewController = new LivePreviewController(this, new LivePreview(myProject), getContentPane()); + //myLivePreviewController = new LivePreviewController(this, new LivePreview(myProject), getContentPane()); } @Override @@ -148,9 +144,9 @@ class FindDialog extends DialogWrapper { e.getKey().removeDocumentListener(e.getValue()); } myComboBoxListeners.clear(); - if (myLivePreviewController != null) { - myLivePreviewController.cleanUp(); - } + //if (myLivePreviewController != null) { + // myLivePreviewController.cleanUp(); + //} super.dispose(); } diff --git a/platform/lang-impl/src/com/intellij/find/impl/LivePreview.java b/platform/lang-impl/src/com/intellij/find/impl/LivePreview.java deleted file mode 100644 index e092ac5293fe..000000000000 --- a/platform/lang-impl/src/com/intellij/find/impl/LivePreview.java +++ /dev/null @@ -1,570 +0,0 @@ -/* - * Copyright 2000-2011 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.impl; - - -import com.intellij.codeInsight.highlighting.HighlightManager; -import com.intellij.ide.IdeTooltipManager; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.ScrollType; -import com.intellij.openapi.editor.VisualPosition; -import com.intellij.openapi.editor.event.DocumentAdapter; -import com.intellij.openapi.editor.event.DocumentEvent; -import com.intellij.openapi.editor.event.VisibleAreaEvent; -import com.intellij.openapi.editor.event.VisibleAreaListener; -import com.intellij.openapi.editor.markup.CustomHighlighterRenderer; -import com.intellij.openapi.editor.markup.RangeHighlighter; -import com.intellij.openapi.editor.markup.TextAttributes; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.popup.Balloon; -import com.intellij.openapi.ui.popup.BalloonBuilder; -import com.intellij.openapi.ui.popup.JBPopupFactory; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.TextRange; -import com.intellij.ui.awt.RelativePoint; -import com.intellij.util.Alarm; -import com.intellij.util.containers.HashSet; -import com.intellij.util.ui.PositionTracker; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.awt.*; -import java.util.*; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -public class LivePreview extends DocumentAdapter implements ReplacementView.Delegate { - - private final Collection myHighlighters = new HashSet(); - private RangeHighlighter myCursorHighlighter; - private final List myVisibleAreaListenersToRemove = new ArrayList(); - private boolean myShouldStop; - - @Override - public void performReplacement(LiveOccurrence occurrence, String replacement) { - if (myDelegate != null) { - final TextRange textRange = myDelegate.performReplace(occurrence, replacement, myEditor); - if (textRange != null) { - updateInBackground(); - setContinuation(new Runnable() { - @Override - public void run() { - if (mySearchResults != null) { - LiveOccurrence nearest = null; - int minDist = Integer.MAX_VALUE; - for (LiveOccurrence o : mySearchResults) { - if (nearest == null) { - nearest = o; - } - int dist = Math.abs(o.getPrimaryRange().getStartOffset() - textRange.getStartOffset()); - if (dist < minDist) { - minDist = dist; - nearest = o; - } - } - if (nearest != null) { - moveCursorTo(nearest); - } - } - } - }); - myDelegate.getFocusBack(); - } - } - - } - - @Override - public void performReplaceAll() { - myDelegate.performReplaceAll(myEditor); - } - - public boolean hasMatches() { - return mySearchResults != null && !mySearchResults.isEmpty(); - } - - public LiveOccurrence getCursor() { - return myCursor; - } - - public Project getProject() { - return myProject; - } - - public Editor getEditor() { - return myEditor; - } - - public interface CursorListener { - void cursorMoved(); - } - - private final List myListeners = new ArrayList(); - - public void addCursorListener(CursorListener listener) { - myListeners.add(listener); - } - - public void removeCursorListener(CursorListener listener) { - myListeners.remove(listener); - } - - public interface Delegate { - @NotNull - List performSearchInBackgroundInReadAction(Editor editor); - - @Nullable - String getReplacementPreviewText(Editor editor, LiveOccurrence liveOccurrence); - - TextRange performReplace(LiveOccurrence occurrence, String replacement, Editor editor); - - void performReplaceAll(Editor e); - - void getFocusBack(); - - Editor getEditor(Ref needToUpdate); - } - - private static final int USER_ACTIVITY_TRIGGERING_DELAY = 300; - - private static final TextAttributes OTHER_TARGETS_ATTRIBUTES = new TextAttributes(Color.BLACK, Color.GREEN, null, null, 0); - private static final TextAttributes MAIN_TARGET_ATTRIBUTES = new TextAttributes(Color.BLACK, Color.YELLOW, null, null, 0); - - private final Alarm myLivePreviewAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD); - - private final Project myProject; - private Editor myEditor; - - private Delegate myDelegate; - - private LiveOccurrence myCursor; - - private Runnable myContinuation; - - private int myUserActivityDelay = USER_ACTIVITY_TRIGGERING_DELAY; - - public Runnable getContinuation() { - return myContinuation; - } - - public void setContinuation(Runnable continuation) { - myContinuation = continuation; - } - - public List getSearchResults() { - return mySearchResults; - } - - private List mySearchResults; - - private Balloon myReplacementBalloon; - - public LivePreview(Project project) { - myProject = project; - } - - public LivePreview(Project project, int delay) { - this(project); - myUserActivityDelay = delay; - } - - public Delegate getDelegate() { - return myDelegate; - } - - public void setDelegate(Delegate delegate) { - myDelegate = delegate; - } - - @Nullable - private Editor updateEditorReference() { - if (myProject == null) return null; - Ref needToUpdate = new Ref(); - Editor result = myDelegate.getEditor(needToUpdate); - if (!needToUpdate.isNull() && needToUpdate.get()) { - setEditor(result); - } - return myEditor; - } - - private void setEditor(Editor editor) { - if (editor != myEditor) { - doInternalCleanUp(); - } - if (myEditor != null) { - myEditor.getDocument().removeDocumentListener(this); - } - myEditor = editor; - if (myEditor != null) { - myEditor.getDocument().addDocumentListener(this); - } - } - - @Override - public void documentChanged(DocumentEvent e) { - doInternalCleanUp(); - } - - public void update() { - myShouldStop = false; - myLivePreviewAlarm.cancelAllRequests(); - if (updateEditorReference() != null) { - Runnable request = new Runnable() { - @Override - public void run() { - updateInBackground(); - } - }; - if (ApplicationManager.getApplication().isUnitTestMode()) { - request.run(); - } else { - myLivePreviewAlarm.addRequest(request, myUserActivityDelay); - } - } - } - - public void cleanUp() { - myShouldStop = true; - myLivePreviewAlarm.cancelAllRequests(); - doInternalCleanUp(); - } - - private void doInternalCleanUp() { - if (myReplacementBalloon != null) { - myReplacementBalloon.hide(); - } - if (myEditor != null) { - - for (VisibleAreaListener visibleAreaListener : myVisibleAreaListenersToRemove) { - myEditor.getScrollingModel().removeVisibleAreaListener(visibleAreaListener); - } - myVisibleAreaListenersToRemove.clear(); - for (RangeHighlighter h : myHighlighters) { - HighlightManager.getInstance(myProject).removeSegmentHighlighter(myEditor, h); - } - if (myCursorHighlighter != null) { - HighlightManager.getInstance(myProject).removeSegmentHighlighter(myEditor, myCursorHighlighter); - myCursorHighlighter = null; - } - } - } - - private void updateInBackground() { - if (myDelegate == null) return; - final TextRange oldCursorRange = myCursor != null ? myCursor.getPrimaryRange() : null; - mySearchResults = performSearchInBackground(); - Collections.sort(mySearchResults, new Comparator() { - @Override - public int compare(LiveOccurrence liveOccurrence, LiveOccurrence liveOccurence1) { - return liveOccurrence.getPrimaryRange().getStartOffset() - liveOccurence1.getPrimaryRange().getStartOffset(); - } - }); - if (mySearchResults != null) { - Runnable highlightUsagesBlock = new Runnable() { - @Override - public void run() { - doInternalCleanUp(); - highlightUsages(oldCursorRange); - if (myContinuation != null) { - myContinuation.run(); - myContinuation = null; - } - } - }; - if (ApplicationManager.getApplication().isUnitTestMode()) { - highlightUsagesBlock.run(); - } else { - ApplicationManager.getApplication().invokeLater(highlightUsagesBlock); - } - } - } - - private List performSearchInBackground() { - final AtomicReference> occurrences = new AtomicReference>(); - ApplicationManager.getApplication().runReadAction(new Runnable() { - @Override - public void run() { - occurrences.set(myDelegate.performSearchInBackgroundInReadAction(myEditor)); - } - }); - return occurrences.get(); - } - - private void highlightUsages(TextRange oldCursorRange) { - if (myEditor == null || myShouldStop) return; - for (LiveOccurrence o : mySearchResults) { - for (TextRange textRange : o.getSecondaryRanges()) { - highlightRange(textRange, OTHER_TARGETS_ATTRIBUTES, myHighlighters); - } - highlightRange(o.getPrimaryRange(), MAIN_TARGET_ATTRIBUTES, myHighlighters); - } - - if (!tryToRepairOldCursor(oldCursorRange)) { - LiveOccurrence afterCaret = firstOccurrenceAfterCaret(); - if (afterCaret != null) { - setCursor(afterCaret); - } else { - LiveOccurrence occurrence = firstVisibleOccurrence(); - if (occurrence != null) { - setCursor(occurrence); - } - } - } - } - - @Nullable - private LiveOccurrence firstVisibleOccurrence() { - int offset = Integer.MAX_VALUE; - LiveOccurrence firstOccurrence = null; - LiveOccurrence firstVisibleOccurrence = null; - for (LiveOccurrence o : mySearchResults) { - if (insideVisibleArea(myEditor, o.getPrimaryRange())) { - if (firstVisibleOccurrence == null || o.getPrimaryRange().getStartOffset() < firstVisibleOccurrence.getPrimaryRange().getStartOffset()) { - firstVisibleOccurrence = o; - } - } - if (o.getPrimaryRange().getStartOffset() < offset) { - offset = o.getPrimaryRange().getStartOffset(); - firstOccurrence = o; - } - } - return firstVisibleOccurrence != null ? firstVisibleOccurrence : firstOccurrence; - } - - @Nullable - private LiveOccurrence firstOccurrenceAfterCaret() { - LiveOccurrence afterCaret = null; - int caret = myEditor.getCaretModel().getOffset(); - for (LiveOccurrence occurrence : mySearchResults) { - if (occurrence.getPrimaryRange().getStartOffset() >= caret) { - if (afterCaret == null || occurrence.getPrimaryRange().getStartOffset() < afterCaret.getPrimaryRange().getStartOffset() ) { - afterCaret = occurrence; - } - } - } - return afterCaret; - } - - private boolean tryToRepairOldCursor(TextRange oldCursorRange) { - if (oldCursorRange == null) return false; - LiveOccurrence mayBeOldCursor = null; - for (LiveOccurrence searchResult : mySearchResults) { - if (searchResult.getPrimaryRange().intersects(oldCursorRange)) { - mayBeOldCursor = searchResult; - break; - } - } - if (mayBeOldCursor != null && insideVisibleArea(myEditor, mayBeOldCursor.getPrimaryRange())) { - setCursor(mayBeOldCursor); - return true; - } - return false; - } - - @Nullable - private LiveOccurrence prevOccurrence(LiveOccurrence o) { - if (mySearchResults == null) return null; - for (int i = 0; i < mySearchResults.size(); ++i) { - if (o == mySearchResults.get(i)) { - if (i > 0) { - return mySearchResults.get(i-1); - } - } - } - return null; - } - - @Nullable - private LiveOccurrence nextOccurrence(LiveOccurrence o) { - if (mySearchResults == null) return null; - boolean found = false; - for (LiveOccurrence occurrence : mySearchResults) { - if (found) { - return occurrence; - } - if (o == occurrence) { - found = true; - } - } - return null; - } - - public void prevOccurrence() { - LiveOccurrence prev = prevOccurrence(myCursor); - if (prev == null && !mySearchResults.isEmpty()) { - prev = mySearchResults.get(mySearchResults.size()-1); - } - moveCursorTo(prev); - } - - public void nextOccurrence() { - LiveOccurrence next = nextOccurrence(myCursor); - if (next == null && !mySearchResults.isEmpty()) { - next = mySearchResults.get(0); - } - moveCursorTo(next); - } - - public void moveCursorTo(LiveOccurrence next) { - if (next != null) { - setCursor(next); - } else { - showReplacementPreview(); - } - } - - private void setCursor(LiveOccurrence liveOccurrence) { - hideBalloon(); - boolean toNotify = myCursor == null || !myCursor.equals(liveOccurrence); - - myCursor = liveOccurrence; - - if (myCursorHighlighter != null) { - HighlightManager.getInstance(myProject).removeSegmentHighlighter(myEditor, myCursorHighlighter); - myCursorHighlighter = null; - } - if (myCursor != null) { - ArrayList dummy = new ArrayList(); - highlightRange(myCursor.getPrimaryRange(), new TextAttributes(null, null, null, null, 0), dummy); - if (!dummy.isEmpty()) { - myCursorHighlighter = dummy.get(0); - myCursorHighlighter.setCustomRenderer(new MyCustomCursorRenderer()); - } - - if (!insideVisibleArea(myEditor, myCursor.getPrimaryRange())) { - myEditor.getScrollingModel().scrollTo(myEditor.offsetToLogicalPosition(myCursor.getPrimaryRange().getStartOffset()), - ScrollType.CENTER); - myEditor.getScrollingModel().runActionOnScrollingFinished(new Runnable() { - @Override - public void run() { - showReplacementPreview(); - } - }); - } else { - showReplacementPreview(); - } - } - if (toNotify) { - for (CursorListener l : myListeners) { - if (l!= null) { - l.cursorMoved(); - } - } - } - } - - private void showReplacementPreview() { - hideBalloon(); - if (myDelegate != null && myCursor != null) { - String replacementPreviewText = myDelegate.getReplacementPreviewText(myEditor, myCursor); - if (replacementPreviewText != null) { - - //JLabel balloonContent = new JLabel(replacementPreviewText); - //balloonContent.setForeground(Color.WHITE); - - ReplacementView replacementView = new ReplacementView(replacementPreviewText, myCursor); - replacementView.setDelegate(this); - - BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createBalloonBuilder(replacementView); - balloonBuilder.setFadeoutTime(0); - balloonBuilder.setFillColor(IdeTooltipManager.GRAPHITE_COLOR); - balloonBuilder.setAnimationCycle(0); - balloonBuilder.setHideOnClickOutside(false); - balloonBuilder.setHideOnKeyOutside(false); - balloonBuilder.setHideOnAction(false); - balloonBuilder.setCloseButtonEnabled(true); - myReplacementBalloon = balloonBuilder.createBalloon(); - final int startOffset = myCursor.getPrimaryRange().getStartOffset(); - final int endOffset = myCursor.getPrimaryRange().getEndOffset(); - - myReplacementBalloon.show(new PositionTracker(myEditor.getContentComponent()) { - @Override - public RelativePoint recalculateLocation(final Balloon object) { - Point startPoint = myEditor.visualPositionToXY(myEditor.offsetToVisualPosition(startOffset)); - Point endPoint = myEditor.visualPositionToXY(myEditor.offsetToVisualPosition(endOffset)); - Point point = new Point((startPoint.x + endPoint.x)/2, startPoint.y); - if (!insideVisibleArea(myEditor, myCursor.getPrimaryRange())) { - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - object.hide(); - } - }); - - VisibleAreaListener visibleAreaListener = new VisibleAreaListener() { - @Override - public void visibleAreaChanged(VisibleAreaEvent e) { - if (insideVisibleArea(myEditor, myCursor.getPrimaryRange())) { - showReplacementPreview(); - final VisibleAreaListener visibleAreaListener = this; - myEditor.getScrollingModel().removeVisibleAreaListener(visibleAreaListener); - myVisibleAreaListenersToRemove.remove(visibleAreaListener); - } - } - }; - myEditor.getScrollingModel().addVisibleAreaListener(visibleAreaListener); - myVisibleAreaListenersToRemove.add(visibleAreaListener); - - } - return new RelativePoint(myEditor.getContentComponent(), point); - } - }, Balloon.Position.above); - } - } - } - - private void hideBalloon() { - if (myReplacementBalloon != null) { - myReplacementBalloon.hide(); - myReplacementBalloon = null; - } - } - - private void highlightRange(TextRange textRange, TextAttributes attributes, Collection highlighters) { - HighlightManager highlightManager = HighlightManager.getInstance(myProject); - if (highlightManager != null) { - highlightManager.addRangeHighlight(myEditor, - textRange.getStartOffset(), textRange.getEndOffset(), - attributes, false, highlighters); - } - } - - private static boolean insideVisibleArea(Editor e, TextRange r) { - Rectangle visibleArea = e.getScrollingModel().getVisibleArea(); - Point point = e.logicalPositionToXY(e.offsetToLogicalPosition(r.getStartOffset())); - - return visibleArea.contains(point); - } - - private static class MyCustomCursorRenderer implements CustomHighlighterRenderer { - @Override - public void paint(Editor editor, RangeHighlighter highlighter, Graphics g) { - Graphics2D g2d = (Graphics2D)g; - VisualPosition startVp = editor.offsetToVisualPosition(highlighter.getStartOffset()); - VisualPosition endVp = editor.offsetToVisualPosition(highlighter.getEndOffset()); - Point start = editor.visualPositionToXY(startVp); - Point end = editor.visualPositionToXY(endVp); - g2d.setColor(new Color(50, 50, 50)); - g2d.translate(0, start.y - 4); - Color c1 = new Color(220, 200, 130); - Color c2 = new Color(220, 170, 30); - UIUtil.drawSearchMatch(g2d, start.x- 1, end.x+ 1, editor.getLineHeight() +2* 4, c1, c2); - g2d.translate(0, -start.y + 4); - } - } -} diff --git a/platform/lang-impl/src/com/intellij/find/impl/LivePreviewController.java b/platform/lang-impl/src/com/intellij/find/impl/LivePreviewController.java deleted file mode 100644 index 0cd76024bbc9..000000000000 --- a/platform/lang-impl/src/com/intellij/find/impl/LivePreviewController.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.intellij.find.impl; - -import com.intellij.find.FindModel; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.FileEditor; -import com.intellij.openapi.fileEditor.TextEditor; -import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.ui.UserActivityListener; -import com.intellij.ui.UserActivityWatcher; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.WindowEvent; -import java.awt.event.WindowFocusListener; -import java.util.List; - -public class LivePreviewController implements LivePreview.Delegate, UserActivityListener { - - - private final FindDialog myFindDialog; - private final LivePreview myLivePreview; - private final UserActivityWatcher myWatcher = new MomentoUserActivityWatcher(); - - LivePreviewControllerBase myDelegate = new LivePreviewControllerBase(); - - private void updateDelegatesFindModel() { - FindModel currentModel = myFindDialog.getCurrentModel(); - myDelegate.setFindModel(currentModel); - } - - public LivePreviewController(FindDialog findDialog, LivePreview livePreview, Component toWatch) { - myFindDialog = findDialog; - myLivePreview = livePreview; - myLivePreview.setDelegate(this); - myWatcher.register(toWatch); - myWatcher.addUserActivityListener(this); - myFindDialog.getWindow().addWindowFocusListener(new WindowFocusListener() { - @Override - public void windowGainedFocus(WindowEvent windowEvent) { - myLivePreview.update(); - } - - @Override - public void windowLostFocus(WindowEvent windowEvent) { - } - }); - - JComponent contentPane = (JComponent)findDialog.getContentPane(); - new AnAction() { - - @Override - public void actionPerformed(AnActionEvent e) { - myLivePreview.nextOccurrence(); - } - }.registerCustomShortcutSet(KeyEvent.VK_F3, 0, contentPane); - - new AnAction() { - - @Override - public void actionPerformed(AnActionEvent e) { - myLivePreview.prevOccurrence(); - } - }.registerCustomShortcutSet(KeyEvent.VK_F3, KeyEvent.SHIFT_MASK, contentPane); - } - - public void cleanUp() { - myWatcher.removeUserActivityListener(this); - myLivePreview.cleanUp(); - } - - @NotNull - @Override - public List performSearchInBackgroundInReadAction(Editor editor) { - updateDelegatesFindModel(); - return myDelegate.performSearchInBackgroundInReadAction(editor); - } - - @Override - public void performReplaceAll(Editor e) { - updateDelegatesFindModel(); - myDelegate.performReplaceAll(e); - } - - @Override - public void getFocusBack() { - myFindDialog.getContentPane().requestFocus(); - } - - @Override - public Editor getEditor(Ref needToUpdate) { - Editor result = null; - FileEditorManagerEx instanceEx = FileEditorManagerEx.getInstanceEx(myLivePreview.getProject()); - if (instanceEx != null) { - VirtualFile currentFile = instanceEx.getCurrentFile(); - if (currentFile != null) { - FileEditor[] editors = instanceEx.getEditors(currentFile); - if (editors.length > 0) { - FileEditor fileEditor = editors[0]; - boolean focusedFound = false; - for (FileEditor e : editors) { - if (e instanceof TextEditor && ((TextEditor)e).getEditor().getContentComponent().hasFocus()) { - fileEditor = e; - focusedFound = true; - } - } - needToUpdate.set(true); - if (!focusedFound) { - for (FileEditor e : editors) { - if (e instanceof TextEditor && ((TextEditor)e).getEditor() == myLivePreview.getEditor()) { - needToUpdate.set(false); - } - } - } - if(needToUpdate.get() && fileEditor instanceof TextEditor) { - result = ((TextEditor)fileEditor).getEditor(); - } - } - } - } - return result; - } - - @Override - public TextRange performReplace(final LiveOccurrence occurrence, final String replacement, final Editor editor) { - updateDelegatesFindModel(); - return myDelegate.performReplace(occurrence, replacement, editor); - } - - @Override - public String getReplacementPreviewText(Editor editor, LiveOccurrence liveOccurrence) { - updateDelegatesFindModel(); - return myDelegate.getReplacementPreviewText(editor, liveOccurrence); - } - - - @Override - public void stateChanged() { - myLivePreview.update(); - } -} diff --git a/platform/lang-impl/src/com/intellij/find/impl/LivePreviewControllerBase.java b/platform/lang-impl/src/com/intellij/find/impl/LivePreviewControllerBase.java deleted file mode 100644 index 82c93f288eec..000000000000 --- a/platform/lang-impl/src/com/intellij/find/impl/LivePreviewControllerBase.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.intellij.find.impl; - -import com.intellij.find.FindManager; -import com.intellij.find.FindModel; -import com.intellij.find.FindResult; -import com.intellij.find.FindUtil; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.vfs.VirtualFile; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class LivePreviewControllerBase implements LivePreview.Delegate { - - private static final String EMPTY_STRING_DISPLAY_TEXT = ""; - - private int myMatchesLimit = 100; - - public int getMatchesLimit() { - return myMatchesLimit; - } - - public void setMatchesLimit(int matchesLimit) { - myMatchesLimit = matchesLimit; - } - - private FindModel myFindModel; - - public FindModel getFindModel() { - return myFindModel; - } - - public void setFindModel(FindModel findModel) { - myFindModel = findModel; - } - - private static void findResultsToOccurrences(ArrayList results, Collection occurrences) { - for (FindResult r : results) { - LiveOccurrence occurrence = new LiveOccurrence(); - occurrence.setPrimaryRange(r); - occurrences.add(occurrence); - } - } - - @NotNull - @Override - public List performSearchInBackgroundInReadAction(Editor editor) { - ArrayList occurrences = new ArrayList(); - if (myFindModel != null) { - - TextRange r = myFindModel.isGlobal() ? new TextRange(0, Integer.MAX_VALUE) : - new TextRange(editor.getSelectionModel().getSelectionStart(), - editor.getSelectionModel().getSelectionEnd()); - if (r.getLength() == 0) { - r = new TextRange(0, Integer.MAX_VALUE); - } - int offset = r.getStartOffset(); - VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(editor.getDocument()); - final ArrayList results = new ArrayList(); - - while (true) { - FindManager findManager = FindManager.getInstance(editor.getProject()); - FindResult result = findManager.findString(editor.getDocument().getCharsSequence(), offset, myFindModel, virtualFile); - if (!result.isStringFound()) break; - int newOffset = result.getEndOffset(); - if (offset == newOffset || result.getEndOffset() > r.getEndOffset()) break; - offset = newOffset; - results.add(result); - - if (results.size() > myMatchesLimit) break; - } - if (results.size() < myMatchesLimit) { - - findResultsToOccurrences(results, occurrences); - } - - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - searchEndsWith(results.size()); - } - }); - } - return occurrences; - } - - protected void searchEndsWith(int size) { - - } - - - @Override - public String getReplacementPreviewText(Editor editor, LiveOccurrence liveOccurrence) { - String foundString = editor.getDocument().getText(liveOccurrence.getPrimaryRange()); - String documentText = editor.getDocument().getText(); - FindModel currentModel = myFindModel; - String stringToReplace = null; - - if (currentModel != null) { - if (currentModel.isReplaceState()) { - FindManager findManager = FindManager.getInstance(editor.getProject()); - try { - stringToReplace = findManager.getStringToReplace(foundString, currentModel, - liveOccurrence.getPrimaryRange().getStartOffset(), documentText); - } - catch (FindManager.MalformedReplacementStringException e) { - return null; - } - if (stringToReplace != null && stringToReplace.isEmpty()) { - stringToReplace = EMPTY_STRING_DISPLAY_TEXT; - } - } - } - return stringToReplace; - } - - @Override - public TextRange performReplace(final LiveOccurrence occurrence, final String replacement, final Editor editor) { - TextRange range = occurrence.getPrimaryRange(); - try { - return FindUtil.doReplace(editor.getProject(), editor.getDocument(), myFindModel, new FindResultImpl(range.getStartOffset(), range.getEndOffset()), - FindManager.getInstance(editor.getProject()).getStringToReplace(editor.getDocument().getText(range), myFindModel), true, new ArrayList>()); - } - catch (FindManager.MalformedReplacementStringException e) { - /**/ - } - return null; - } - - @Override - public void performReplaceAll(Editor e) { - FindUtil.replace(e.getProject(), e, 0, myFindModel); - } - - @Override - public void getFocusBack() {} - - @Override - public Editor getEditor(Ref needToUpdate) { - return null; - } - -} diff --git a/platform/lang-impl/src/com/intellij/find/impl/ReplacementView.java b/platform/lang-impl/src/com/intellij/find/impl/ReplacementView.java deleted file mode 100644 index 80d51ed6f8ea..000000000000 --- a/platform/lang-impl/src/com/intellij/find/impl/ReplacementView.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.intellij.find.impl; - -import com.intellij.ide.IdeTooltipManager; -import com.intellij.openapi.util.SystemInfo; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -public class ReplacementView extends JPanel { - - private String myReplacement; - private LiveOccurrence myOccurrence; - - public interface Delegate { - void performReplacement(LiveOccurrence occurrence, String replacement); - void performReplaceAll(); - } - - private Delegate delegate; - - public Delegate getDelegate() { - return delegate; - } - - public void setDelegate(Delegate delegate) { - this.delegate = delegate; - } - - public ReplacementView(final String replacement, final LiveOccurrence occurrence) { - myReplacement = replacement; - myOccurrence = occurrence; - JLabel jLabel = new JLabel(replacement); - jLabel.setForeground(Color.WHITE); - add(jLabel); - JButton replace = new JButton("Replace"); - replace.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent actionEvent) { - if (delegate != null) { - delegate.performReplacement(occurrence, replacement); - } - } - }); - replace.setMnemonic('R'); - add(replace); - JButton replaceAllButton = new JButton("Replace all"); - replaceAllButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent actionEvent) { - if (delegate != null) { - delegate.performReplaceAll(); - } - } - }); - replaceAllButton.setMnemonic('a'); - add(replaceAllButton); - setBackground(IdeTooltipManager.GRAPHITE_COLOR); - if (SystemInfo.isMac) { - Font f = getFont(); - setFont(f.deriveFont(f.getStyle(), f.getSize() - 4)); - } - } - -} diff --git a/platform/lang-impl/src/com/intellij/find/impl/LiveOccurrence.java b/platform/lang-impl/src/com/intellij/find/impl/livePreview/LiveOccurrence.java similarity index 87% rename from platform/lang-impl/src/com/intellij/find/impl/LiveOccurrence.java rename to platform/lang-impl/src/com/intellij/find/impl/livePreview/LiveOccurrence.java index bef2f3c6536c..a1ae6c3a4e60 100644 --- a/platform/lang-impl/src/com/intellij/find/impl/LiveOccurrence.java +++ b/platform/lang-impl/src/com/intellij/find/impl/livePreview/LiveOccurrence.java @@ -1,4 +1,4 @@ -package com.intellij.find.impl; +package com.intellij.find.impl.livePreview; import com.intellij.openapi.util.TextRange; @@ -34,4 +34,9 @@ public class LiveOccurrence { } return false; } + + @Override + public int hashCode() { + return myPrimaryRange.hashCode(); + } } diff --git a/platform/lang-impl/src/com/intellij/find/impl/livePreview/LivePreview.java b/platform/lang-impl/src/com/intellij/find/impl/livePreview/LivePreview.java new file mode 100644 index 000000000000..8deac659bc64 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/find/impl/livePreview/LivePreview.java @@ -0,0 +1,336 @@ +/* + * Copyright 2000-2011 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.impl.livePreview; + + +import com.intellij.codeInsight.highlighting.HighlightManager; +import com.intellij.ide.IdeTooltipManager; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.editor.VisualPosition; +import com.intellij.openapi.editor.event.DocumentAdapter; +import com.intellij.openapi.editor.event.VisibleAreaEvent; +import com.intellij.openapi.editor.event.VisibleAreaListener; +import com.intellij.openapi.editor.markup.CustomHighlighterRenderer; +import com.intellij.openapi.editor.markup.EffectType; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.ui.popup.BalloonBuilder; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.util.Segment; +import com.intellij.openapi.util.TextRange; +import com.intellij.ui.awt.RelativePoint; +import com.intellij.util.containers.HashSet; +import com.intellij.util.ui.PositionTracker; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +public class LivePreview extends DocumentAdapter implements ReplacementView.Delegate, SearchResults.SearchResultsListener { + + public interface Delegate { + + @Nullable + String getReplacementPreviewText(Editor editor, LiveOccurrence liveOccurrence); + + @Nullable + TextRange performReplace(LiveOccurrence occurrence, String replacement, Editor editor); + + void performReplaceAll(Editor e); + + void getFocusBack(); + + } + + private final Collection myHighlighters = new HashSet(); + + + private RangeHighlighter myCursorHighlighter; + private final List myVisibleAreaListenersToRemove = new ArrayList(); + + private static final TextAttributes EXCLUDED_TARGET_ATTRIBUTES = new TextAttributes(Color.BLACK, Color.YELLOW, + Color.BLACK, EffectType.STRIKEOUT, 0); + + private static final TextAttributes OTHER_TARGETS_ATTRIBUTES = new TextAttributes(Color.BLACK, Color.GREEN, null, null, 0); + + private static final TextAttributes MAIN_TARGET_ATTRIBUTES = new TextAttributes(Color.BLACK, Color.YELLOW, null, null, 0); + + private Delegate myDelegate; + + private SearchResults mySearchResults; + + private Balloon myReplacementBalloon; + + @Override + public void performReplacement(LiveOccurrence occurrence, String replacement) { + if (myDelegate != null) { + myDelegate.performReplace(occurrence, replacement, mySearchResults.getEditor()); + myDelegate.getFocusBack(); + } + } + + @Override + public void performReplaceAll() { + myDelegate.performReplaceAll(mySearchResults.getEditor()); + } + + public boolean isExcluded(LiveOccurrence occurrence) { + return mySearchResults.isExcluded(occurrence); + } + + public void exclude(LiveOccurrence occurrence) { + mySearchResults.exclude(occurrence); + myDelegate.getFocusBack(); + } + + public boolean hasMatches() { + return mySearchResults.hasMatches(); + } + + @Override + public void searchResultsUpdated(SearchResults sr) { + removeFromEditor(mySearchResults.getEditor()); + highlightUsages(); + updateCursorHighlighting(false); + } + + @Override + public void cursorMoved() { + updateCursorHighlighting(true); + } + + public void editorChanged(SearchResults sr, Editor oldEditor) { + removeFromEditor(mySearchResults.getEditor()); + oldEditor.getDocument().removeDocumentListener(this); + mySearchResults.getEditor().getDocument().addDocumentListener(this); + } + + private void updateCursorHighlighting(boolean scroll) { + hideBalloon(); + + if (myCursorHighlighter != null) { + HighlightManager.getInstance(mySearchResults.getProject()).removeSegmentHighlighter(mySearchResults.getEditor(), myCursorHighlighter); + myCursorHighlighter = null; + } + + LiveOccurrence cursor = mySearchResults.getCursor(); + Editor editor = mySearchResults.getEditor(); + if (cursor != null) { + ArrayList dummy = new ArrayList(); + highlightRange(cursor.getPrimaryRange(), new TextAttributes(null, null, null, null, 0), dummy); + if (!dummy.isEmpty()) { + myCursorHighlighter = dummy.get(0); + myCursorHighlighter.setCustomRenderer(new CursorRenderer()); + } + + if (!SearchResults.insideVisibleArea(editor, cursor.getPrimaryRange()) && scroll) { + editor.getScrollingModel().scrollTo(editor.offsetToLogicalPosition(cursor.getPrimaryRange().getStartOffset()), + ScrollType.CENTER); + editor.getScrollingModel().runActionOnScrollingFinished(new Runnable() { + @Override + public void run() { + showReplacementPreview(); + } + }); + } else { + showReplacementPreview(); + } + } + } + + public LivePreview(SearchResults searchResults) { + mySearchResults = searchResults; + searchResultsUpdated(searchResults); + searchResults.addListener(this); + } + + public Delegate getDelegate() { + return myDelegate; + } + + public void setDelegate(Delegate delegate) { + myDelegate = delegate; + } + + + public void cleanUp() { + removeFromEditor(mySearchResults.getEditor()); + } + + private void removeFromEditor(Editor editor) { + if (myReplacementBalloon != null) { + myReplacementBalloon.hide(); + } + + if (editor != null) { + + for (VisibleAreaListener visibleAreaListener : myVisibleAreaListenersToRemove) { + editor.getScrollingModel().removeVisibleAreaListener(visibleAreaListener); + } + myVisibleAreaListenersToRemove.clear(); + for (RangeHighlighter h : myHighlighters) { + HighlightManager.getInstance(mySearchResults.getProject()).removeSegmentHighlighter(editor, h); + } + if (myCursorHighlighter != null) { + HighlightManager.getInstance(mySearchResults.getProject()).removeSegmentHighlighter(editor, myCursorHighlighter); + myCursorHighlighter = null; + } + } + } + + private void highlightUsages() { + if (mySearchResults.getEditor() == null) return; + for (LiveOccurrence o : mySearchResults.getOccurrences()) { + for (TextRange textRange : o.getSecondaryRanges()) { + highlightRange(textRange, OTHER_TARGETS_ATTRIBUTES, myHighlighters); + } + if (mySearchResults.isExcluded(o)) { + highlightRange(o.getPrimaryRange(), EXCLUDED_TARGET_ATTRIBUTES, myHighlighters); + } else { + highlightRange(o.getPrimaryRange(), MAIN_TARGET_ATTRIBUTES, myHighlighters); + } + } + } + + private void showReplacementPreview() { + hideBalloon(); + final LiveOccurrence cursor = mySearchResults.getCursor(); + final Editor editor = mySearchResults.getEditor(); + if (myDelegate != null && cursor != null) { + String replacementPreviewText = myDelegate.getReplacementPreviewText(editor, cursor); + if (replacementPreviewText != null) { + + ReplacementView replacementView = new ReplacementView(replacementPreviewText, cursor); + replacementView.setDelegate(this); + + BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createBalloonBuilder(replacementView); + balloonBuilder.setFadeoutTime(0); + balloonBuilder.setFillColor(IdeTooltipManager.GRAPHITE_COLOR); + balloonBuilder.setAnimationCycle(0); + balloonBuilder.setHideOnClickOutside(false); + balloonBuilder.setHideOnKeyOutside(false); + balloonBuilder.setHideOnAction(false); + balloonBuilder.setCloseButtonEnabled(true); + myReplacementBalloon = balloonBuilder.createBalloon(); + final int startOffset = cursor.getPrimaryRange().getStartOffset(); + final int endOffset = cursor.getPrimaryRange().getEndOffset(); + + myReplacementBalloon.show(new ReplacementBalloonPositionTracker(editor, startOffset, endOffset, cursor), Balloon.Position.above); + } + } + } + + private void hideBalloon() { + if (myReplacementBalloon != null) { + myReplacementBalloon.hide(); + myReplacementBalloon = null; + } + } + + private void highlightRange(TextRange textRange, TextAttributes attributes, Collection highlighters) { + HighlightManager highlightManager = HighlightManager.getInstance(mySearchResults.getProject()); + if (highlightManager != null) { + highlightManager.addRangeHighlight(mySearchResults.getEditor(), + textRange.getStartOffset(), textRange.getEndOffset(), + attributes, false, highlighters); + } + } + + + + private static class CursorRenderer implements CustomHighlighterRenderer { + @Override + public void paint(Editor editor, RangeHighlighter highlighter, Graphics g) { + Document document = editor.getDocument(); + int offset = highlighter.getStartOffset(); + while (offset < highlighter.getEndOffset()) { + int line = document.getLineNumber(offset); + int newOffset = document.getLineEndOffset(line); + newOffset = Math.min(highlighter.getEndOffset(), newOffset); + drawSegment(editor, new TextRange(offset, newOffset), g); + offset = newOffset+1; + } + + } + + private static void drawSegment(Editor editor, Segment highlighter, Graphics g) { + Graphics2D g2d = (Graphics2D)g; + VisualPosition startVp = editor.offsetToVisualPosition(highlighter.getStartOffset()); + VisualPosition endVp = editor.offsetToVisualPosition(highlighter.getEndOffset()); + Point start = editor.visualPositionToXY(startVp); + Point end = editor.visualPositionToXY(endVp); + g2d.setColor(new Color(50, 50, 50)); + g2d.translate(0, start.y - 4); + Color c1 = new Color(220, 200, 130); + Color c2 = new Color(220, 170, 30); + int endX = start.x != end.x ? end.x : end.x + 2; + UIUtil.drawSearchMatch(g2d, start.x - 1, endX + 1, editor.getLineHeight() + 2 * 4, c1, c2); + g2d.translate(0, -start.y + 4); + } + } + + private class ReplacementBalloonPositionTracker extends PositionTracker { + private final Editor myEditor; + private final int myStartOffset; + private final int myEndOffset; + private final LiveOccurrence myCursor; + + public ReplacementBalloonPositionTracker(Editor editor, int startOffset, int endOffset, LiveOccurrence cursor) { + super(editor.getContentComponent()); + myEditor = editor; + myStartOffset = startOffset; + myEndOffset = endOffset; + myCursor = cursor; + } + + @Override + public RelativePoint recalculateLocation(final Balloon object) { + Point startPoint = myEditor.visualPositionToXY(myEditor.offsetToVisualPosition(myStartOffset)); + Point endPoint = myEditor.visualPositionToXY(myEditor.offsetToVisualPosition(myEndOffset)); + Point point = new Point((startPoint.x + endPoint.x)/2, startPoint.y); + if (!SearchResults.insideVisibleArea(myEditor, myCursor.getPrimaryRange())) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + object.hide(); + } + }); + + VisibleAreaListener visibleAreaListener = new VisibleAreaListener() { + @Override + public void visibleAreaChanged(VisibleAreaEvent e) { + if (SearchResults.insideVisibleArea(myEditor, myCursor.getPrimaryRange())) { + showReplacementPreview(); + final VisibleAreaListener visibleAreaListener = this; + myEditor.getScrollingModel().removeVisibleAreaListener(visibleAreaListener); + myVisibleAreaListenersToRemove.remove(visibleAreaListener); + } + } + }; + myEditor.getScrollingModel().addVisibleAreaListener(visibleAreaListener); + myVisibleAreaListenersToRemove.add(visibleAreaListener); + + } + return new RelativePoint(myEditor.getContentComponent(), point); + } + } +} diff --git a/platform/lang-impl/src/com/intellij/find/impl/livePreview/LivePreviewControllerBase.java b/platform/lang-impl/src/com/intellij/find/impl/livePreview/LivePreviewControllerBase.java new file mode 100644 index 000000000000..a94ec03b51a1 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/find/impl/livePreview/LivePreviewControllerBase.java @@ -0,0 +1,119 @@ +package com.intellij.find.impl.livePreview; + +import com.intellij.find.FindManager; +import com.intellij.find.FindModel; +import com.intellij.find.FindUtil; +import com.intellij.find.impl.FindResultImpl; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.util.Alarm; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +public class LivePreviewControllerBase implements LivePreview.Delegate, FindUtil.ReplaceDelegate { + + private static final String EMPTY_STRING_DISPLAY_TEXT = ""; + + private static final int USER_ACTIVITY_TRIGGERING_DELAY = 300; + + private int myUserActivityDelay = USER_ACTIVITY_TRIGGERING_DELAY; + + private final Alarm myLivePreviewAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD); + + private SearchResults mySearchResults; + private LivePreview myLivePreview; + + public LivePreviewControllerBase(SearchResults searchResults, LivePreview livePreview) { + mySearchResults = searchResults; + myLivePreview = livePreview; + myLivePreview.setDelegate(this); + } + + public int getUserActivityDelay() { + return myUserActivityDelay; + } + + public void setUserActivityDelay(int userActivityDelay) { + myUserActivityDelay = userActivityDelay; + } + + public void updateInBackground(final FindModel findModel) { + myLivePreviewAlarm.cancelAllRequests(); + if (findModel == null) return; + Runnable request = new Runnable() { + @Override + public void run() { + mySearchResults.updateThreadSafe(findModel); + } + }; + if (ApplicationManager.getApplication().isUnitTestMode()) { + request.run(); + } else { + myLivePreviewAlarm.addRequest(request, myUserActivityDelay); + } + } + + @Override + public String getReplacementPreviewText(Editor editor, LiveOccurrence liveOccurrence) { + String foundString = editor.getDocument().getText(liveOccurrence.getPrimaryRange()); + String documentText = editor.getDocument().getText(); + FindModel currentModel = mySearchResults.getFindModel(); + String stringToReplace = null; + + if (currentModel != null) { + if (currentModel.isReplaceState()) { + FindManager findManager = FindManager.getInstance(editor.getProject()); + try { + stringToReplace = findManager.getStringToReplace(foundString, currentModel, + liveOccurrence.getPrimaryRange().getStartOffset(), documentText); + } + catch (FindManager.MalformedReplacementStringException e) { + return null; + } + if (stringToReplace != null && stringToReplace.isEmpty()) { + stringToReplace = EMPTY_STRING_DISPLAY_TEXT; + } + } + } + return stringToReplace; + } + + @Nullable + @Override + public TextRange performReplace(final LiveOccurrence occurrence, final String replacement, final Editor editor) { + TextRange range = occurrence.getPrimaryRange(); + FindModel findModel = mySearchResults.getFindModel(); + try { + return FindUtil.doReplace(editor.getProject(), editor.getDocument(), findModel, new FindResultImpl(range.getStartOffset(), range.getEndOffset()), + FindManager.getInstance(editor.getProject()).getStringToReplace(editor.getDocument().getText(range), findModel), + true, new ArrayList>()); + } + catch (FindManager.MalformedReplacementStringException e) { + /**/ + } + mySearchResults.updateThreadSafe(findModel); + return null; + } + + @Override + public void performReplaceAll(Editor e) { + FindUtil.replace(e.getProject(), e, 0, mySearchResults.getFindModel(), this); + } + + @Override + public void getFocusBack() {} + + @Override + public boolean shouldReplace(TextRange range, String replace) { + for (LiveOccurrence o : mySearchResults.getExcluded()) { + TextRange primaryRange = o.getPrimaryRange(); + if (primaryRange.equals(range)) { + return false; + } + } + return true; + } +} diff --git a/platform/lang-impl/src/com/intellij/find/impl/livePreview/ReplacementView.java b/platform/lang-impl/src/com/intellij/find/impl/livePreview/ReplacementView.java new file mode 100644 index 000000000000..ea66fc678db6 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/find/impl/livePreview/ReplacementView.java @@ -0,0 +1,125 @@ +package com.intellij.find.impl.livePreview; + +import com.intellij.ide.IdeTooltipManager; +import com.intellij.openapi.util.SystemInfo; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class ReplacementView extends JPanel { + + private String myReplacement; + private LiveOccurrence myOccurrence; + private JButton myStatusButton; + + public interface Delegate { + void performReplacement(LiveOccurrence occurrence, String replacement); + void performReplaceAll(); + boolean isExcluded(LiveOccurrence occurrence); + void exclude(LiveOccurrence occurrence); + } + + private Delegate myDelegate; + + public Delegate getDelegate() { + return myDelegate; + } + + public void setDelegate(Delegate delegate) { + this.myDelegate = delegate; + updateStatusLabel(); + } + + @Override + protected void paintComponent(Graphics graphics) { + + } + + public ReplacementView(final String replacement, final LiveOccurrence occurrence) { + myReplacement = replacement; + myOccurrence = occurrence; + setLayout(new GridBagLayout()); + + GridBagConstraints c = new GridBagConstraints(); + + JLabel jLabel = new JLabel(replacement); + jLabel.setForeground(Color.WHITE); + JPanel labelPane = new JPanel(new FlowLayout()) { + @Override + protected void paintComponent(Graphics graphics) {} + }; + labelPane.setAlignmentX(Component.LEFT_ALIGNMENT); + labelPane.add(jLabel); + + c.fill = GridBagConstraints.HORIZONTAL; + c.gridx = c.gridy = 0; + c.gridwidth = 2; + c.gridheight = 1; + add(labelPane, c); + JButton replace = new JButton("Replace"); + + replace.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (myDelegate != null) { + myDelegate.performReplacement(occurrence, replacement); + } + } + }); + replace.setPreferredSize(new Dimension(80, 20)); + replace.setMnemonic('R'); + JPanel buttonsPane = new JPanel(){ + @Override + protected void paintComponent(Graphics graphics) { } + }; + JButton replaceAllButton = new JButton("Replace all"); + + replaceAllButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (myDelegate != null) { + myDelegate.performReplaceAll(); + } + } + }); + replaceAllButton.setPreferredSize(new Dimension(100, 20)); + replaceAllButton.setMnemonic('a'); + + myStatusButton = new JButton("Exclude"); + myStatusButton.setPreferredSize(new Dimension(80, 20)); + myStatusButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + myDelegate.exclude(myOccurrence); + updateStatusLabel(); + } + }); + myStatusButton.setMnemonic('l'); + buttonsPane.add(myStatusButton); + + setBackground(IdeTooltipManager.GRAPHITE_COLOR); + if (SystemInfo.isMac) { + Font f = getFont(); + setFont(f.deriveFont(f.getStyle(), f.getSize() - 4)); + } + + buttonsPane.add(replace); + buttonsPane.add(replaceAllButton); + + c.gridx = 0; + c.gridy = 1; + + c.gridwidth = 3; + c.gridheight = 1; + add(buttonsPane, c); + setAlignmentX(Component.LEFT_ALIGNMENT); + } + + private void updateStatusLabel() { + myStatusButton.setText(myDelegate.isExcluded(myOccurrence) ? "Include" : "Exclude"); + myStatusButton.repaint(); + } + +} diff --git a/platform/lang-impl/src/com/intellij/find/impl/livePreview/SearchResults.java b/platform/lang-impl/src/com/intellij/find/impl/livePreview/SearchResults.java new file mode 100644 index 000000000000..d6925ef5d915 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/find/impl/livePreview/SearchResults.java @@ -0,0 +1,328 @@ +package com.intellij.find.impl.livePreview; + + +import com.intellij.find.FindManager; +import com.intellij.find.FindModel; +import com.intellij.find.FindResult; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.*; +import java.util.List; + +public class SearchResults { + + private int myActualFound = 0; + + private List myListeners = new ArrayList(); + + private LiveOccurrence myCursor; + + private List myOccurrences = new ArrayList(); + private Set myExcluded = new HashSet(); + + private Editor myEditor; + + private FindModel myFindModel; + + private int myMatchesLimit = 100; + + public SearchResults(Editor editor) { + myEditor = editor; + } + + public int getActualFound() { + return myActualFound; + } + + public boolean hasMatches() { + return !getOccurrences().isEmpty(); + } + + public FindModel getFindModel() { + return myFindModel; + } + + public boolean isExcluded(LiveOccurrence occurrence) { + return myExcluded.contains(occurrence); + } + + public void exclude(LiveOccurrence occurrence) { + if (myExcluded.contains(occurrence)) { + myExcluded.remove(occurrence); + } else { + myExcluded.add(occurrence); + } + notifyChanged(); + } + + public Set getExcluded() { + return myExcluded; + } + + public interface SearchResultsListener { + void searchResultsUpdated(SearchResults sr); + void editorChanged(SearchResults sr, Editor oldEditor); + + void cursorMoved(); + } + + public void addListener(SearchResultsListener srl) { + myListeners.add(srl); + } + + public void removeListener(SearchResultsListener srl) { + myListeners.remove(srl); + } + + public int getMatchesLimit() { + return myMatchesLimit; + } + + public void setMatchesLimit(int matchesLimit) { + myMatchesLimit = matchesLimit; + } + + public LiveOccurrence getCursor() { + return myCursor; + } + + public List getOccurrences() { + return myOccurrences; + } + + public Project getProject() { + return myEditor.getProject(); + } + + public synchronized void setEditor(Editor editor) { + Editor oldOne = myEditor; + myEditor = editor; + notifyEditorChanged(oldOne); + } + + private void notifyEditorChanged(Editor oldOne) { + for (SearchResultsListener listener : myListeners) { + listener.editorChanged(this, oldOne); + } + } + + public synchronized Editor getEditor() { + return myEditor; + } + + private static void findResultsToOccurrences(ArrayList results, Collection occurrences) { + for (FindResult r : results) { + LiveOccurrence occurrence = new LiveOccurrence(); + occurrence.setPrimaryRange(r); + occurrences.add(occurrence); + } + } + + public void updateThreadSafe(final FindModel findModel) { + final ArrayList occurrences = new ArrayList(); + final Editor editor = getEditor(); + + final ArrayList results = new ArrayList(); + if (findModel != null) { + + TextRange r = findModel.isGlobal() ? new TextRange(0, Integer.MAX_VALUE) : + new TextRange(editor.getSelectionModel().getSelectionStart(), + editor.getSelectionModel().getSelectionEnd()); + if (r.getLength() == 0) { + r = new TextRange(0, Integer.MAX_VALUE); + } + int offset = r.getStartOffset(); + VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(editor.getDocument()); + + while (true) { + FindManager findManager = FindManager.getInstance(editor.getProject()); + FindResult result = findManager.findString(editor.getDocument().getCharsSequence(), offset, findModel, virtualFile); + if (!result.isStringFound()) break; + int newOffset = result.getEndOffset(); + if (offset == newOffset || result.getEndOffset() > r.getEndOffset()) break; + offset = newOffset; + results.add(result); + + if (results.size() > myMatchesLimit) break; + } + if (results.size() < myMatchesLimit) { + + findResultsToOccurrences(results, occurrences); + } + } + + final Runnable r = new Runnable() { + @Override + public void run() { + searchCompleted(occurrences, results.size(), editor, findModel); + } + }; + + if (!ApplicationManager.getApplication().isUnitTestMode()) { + ApplicationManager.getApplication().invokeLater(r); + } else { + r.run(); + } + } + + private void searchCompleted(List occurrences, int size, Editor editor, FindModel findModel) { + if (editor == getEditor()) { + myOccurrences = occurrences; + final TextRange oldCursorRange = myCursor != null ? myCursor.getPrimaryRange() : null; + Collections.sort(myOccurrences, new Comparator() { + @Override + public int compare(LiveOccurrence liveOccurrence, LiveOccurrence liveOccurrence1) { + return liveOccurrence.getPrimaryRange().getStartOffset() - liveOccurrence1.getPrimaryRange().getStartOffset(); + } + }); + + updateCursor(oldCursorRange); + myFindModel = findModel; + myActualFound = size; + notifyChanged(); + if (oldCursorRange == null) { + notifyCursorMoved(); + } + } + } + + private void updateCursor(TextRange oldCursorRange) { + if (!tryToRepairOldCursor(oldCursorRange)) { + LiveOccurrence afterCaret = firstOccurrenceAfterCaret(); + if (afterCaret != null) { + myCursor = afterCaret; + } else { + LiveOccurrence occurrence = firstVisibleOccurrence(); + myCursor = occurrence; + } + } + } + + private void notifyChanged() { + for (SearchResultsListener listener : myListeners) { + listener.searchResultsUpdated(this); + } + } + + static boolean insideVisibleArea(Editor e, TextRange r) { + Rectangle visibleArea = e.getScrollingModel().getVisibleArea(); + Point point = e.logicalPositionToXY(e.offsetToLogicalPosition(r.getStartOffset())); + + return visibleArea.contains(point); + } + + @Nullable + private LiveOccurrence firstVisibleOccurrence() { + int offset = Integer.MAX_VALUE; + LiveOccurrence firstOccurrence = null; + LiveOccurrence firstVisibleOccurrence = null; + for (LiveOccurrence o : getOccurrences()) { + if (insideVisibleArea(myEditor, o.getPrimaryRange())) { + if (firstVisibleOccurrence == null || o.getPrimaryRange().getStartOffset() < firstVisibleOccurrence.getPrimaryRange().getStartOffset()) { + firstVisibleOccurrence = o; + } + } + if (o.getPrimaryRange().getStartOffset() < offset) { + offset = o.getPrimaryRange().getStartOffset(); + firstOccurrence = o; + } + } + return firstVisibleOccurrence != null ? firstVisibleOccurrence : firstOccurrence; + } + + @Nullable + private LiveOccurrence firstOccurrenceAfterCaret() { + LiveOccurrence afterCaret = null; + int caret = myEditor.getCaretModel().getOffset(); + for (LiveOccurrence occurrence : getOccurrences()) { + if (occurrence.getPrimaryRange().getStartOffset() >= caret) { + if (afterCaret == null || occurrence.getPrimaryRange().getStartOffset() < afterCaret.getPrimaryRange().getStartOffset() ) { + afterCaret = occurrence; + } + } + } + return afterCaret; + } + + private boolean tryToRepairOldCursor(TextRange oldCursorRange) { + if (oldCursorRange == null) return false; + LiveOccurrence mayBeOldCursor = null; + for (LiveOccurrence searchResult : getOccurrences()) { + if (searchResult.getPrimaryRange().intersects(oldCursorRange)) { + mayBeOldCursor = searchResult; + break; + } + } + if (mayBeOldCursor != null) { + myCursor = mayBeOldCursor; + return true; + } + return false; + } + + @Nullable + private LiveOccurrence prevOccurrence(LiveOccurrence o) { + for (int i = 0; i < getOccurrences().size(); ++i) { + if (o == getOccurrences().get(i)) { + if (i > 0) { + return getOccurrences().get(i - 1); + } + } + } + return null; + } + + @Nullable + private LiveOccurrence nextOccurrence(LiveOccurrence o) { + boolean found = false; + for (LiveOccurrence occurrence : getOccurrences()) { + if (found) { + return occurrence; + } + if (o == occurrence) { + found = true; + } + } + return null; + } + + public void prevOccurrence() { + LiveOccurrence prev = prevOccurrence(myCursor); + if (prev == null && !getOccurrences().isEmpty()) { + prev = getOccurrences().get(getOccurrences().size() - 1); + } + moveCursorTo(prev); + } + + public void nextOccurrence() { + LiveOccurrence next = nextOccurrence(myCursor); + if (next == null && !getOccurrences().isEmpty()) { + next = getOccurrences().get(0); + } + moveCursorTo(next); + } + + public void moveCursorTo(LiveOccurrence next) { + if (next != null) { + setCursor(next); + } + } + + private void setCursor(LiveOccurrence liveOccurrence) { + myCursor = liveOccurrence; + notifyCursorMoved(); + } + + private void notifyCursorMoved() { + for (SearchResultsListener listener : myListeners) { + listener.cursorMoved(); + } + } +}