blob: e104c166485787dac62534d36f7791b676c3aa70 [file] [log] [blame]
/*
* Copyright 2000-2014 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.find.FindModel;
import com.intellij.find.FindResult;
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.SelectionModel;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
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.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.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.PositionTracker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.PrintStream;
import java.util.*;
import java.util.List;
public class LivePreview extends DocumentAdapter implements SearchResults.SearchResultsListener, SelectionListener {
private static final Key<Object> IN_SELECTION_KEY = Key.create("LivePreview.IN_SELECTION_KEY");
private static final Object IN_SELECTION1 = new Object();
private static final Object IN_SELECTION2 = new Object();
private static final String EMPTY_STRING_DISPLAY_TEXT = "<Empty string>";
private boolean myListeningSelection = false;
private boolean mySuppressedUpdate = false;
private boolean myInSmartUpdate = false;
private static final Key<Object> MARKER_USED = Key.create("LivePreview.MARKER_USED");
private static final Object YES = new Object();
private static final Key<Object> SEARCH_MARKER = Key.create("LivePreview.SEARCH_MARKER");
public static PrintStream ourTestOutput;
private String myReplacementPreviewText;
private static boolean NotFound;
private final Set<RangeHighlighter> myHighlighters = new HashSet<RangeHighlighter>();
private RangeHighlighter myCursorHighlighter;
private final List<VisibleAreaListener> myVisibleAreaListenersToRemove = new ArrayList<VisibleAreaListener>();
private Delegate myDelegate;
private final SearchResults mySearchResults;
private Balloon myReplacementBalloon;
@Override
public void selectionChanged(SelectionEvent e) {
updateInSelectionHighlighters();
}
public void inSmartUpdate() {
myInSmartUpdate = true;
}
public static void processNotFound() {
NotFound = true;
}
public interface Delegate {
@Nullable
String getStringToReplace(@NotNull Editor editor, @Nullable FindResult findResult);
}
private static TextAttributes strikeout() {
Color color = EditorColorsManager.getInstance().getGlobalScheme().getDefaultForeground();
return new TextAttributes(null, null, color, EffectType.STRIKEOUT, 0);
}
@Override
public void searchResultsUpdated(SearchResults sr) {
final Project project = mySearchResults.getProject();
if (project == null || project.isDisposed()) return;
if (mySuppressedUpdate) {
mySuppressedUpdate = false;
return;
}
if (!myInSmartUpdate) {
removeFromEditor();
}
highlightUsages();
updateCursorHighlighting();
if (myInSmartUpdate) {
clearUnusedHightlighters();
myInSmartUpdate = false;
}
}
private void dumpState() {
if (ApplicationManager.getApplication().isUnitTestMode() && ourTestOutput != null) {
dumpEditorMarkupAndSelection(ourTestOutput);
}
}
private void dumpEditorMarkupAndSelection(PrintStream dumpStream) {
dumpStream.println(mySearchResults.getFindModel());
if (myReplacementPreviewText != null) {
dumpStream.println("--");
dumpStream.println("Replacement Preview: " + myReplacementPreviewText);
}
dumpStream.println("--");
Editor editor = mySearchResults.getEditor();
RangeHighlighter[] highlighters = editor.getMarkupModel().getAllHighlighters();
List<Pair<Integer, Character>> ranges = new ArrayList<Pair<Integer, Character>>();
for (RangeHighlighter highlighter : highlighters) {
ranges.add(new Pair<Integer, Character>(highlighter.getStartOffset(), '['));
ranges.add(new Pair<Integer, Character>(highlighter.getEndOffset(), ']'));
}
SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.getSelectionStart() != selectionModel.getSelectionEnd()) {
ranges.add(new Pair<Integer, Character>(selectionModel.getSelectionStart(), '<'));
ranges.add(new Pair<Integer, Character>(selectionModel.getSelectionEnd(), '>'));
}
ranges.add(new Pair<Integer, Character>(-1, '\n'));
ranges.add(new Pair<Integer, Character>(editor.getDocument().getTextLength()+1, '\n'));
ContainerUtil.sort(ranges, new Comparator<Pair<Integer, Character>>() {
@Override
public int compare(@NotNull Pair<Integer, Character> pair, @NotNull Pair<Integer, Character> pair2) {
int res = pair.first - pair2.first;
if (res == 0) {
Character c1 = pair.second;
Character c2 = pair2.second;
if (c1 == '<' && c2 == '[') {
return 1;
}
else if (c1 == '[' && c2 == '<') {
return -1;
}
return c1.compareTo(c2);
}
return res;
}
});
Document document = editor.getDocument();
for (int i = 0; i < ranges.size()-1; ++i) {
Pair<Integer, Character> pair = ranges.get(i);
Pair<Integer, Character> pair1 = ranges.get(i + 1);
dumpStream.print(pair.second + document.getText(TextRange.create(Math.max(pair.first, 0), Math.min(pair1.first, document.getTextLength() ))));
}
dumpStream.println("\n--");
if (NotFound) {
dumpStream.println("Not Found");
dumpStream.println("--");
NotFound = false;
}
for (RangeHighlighter highlighter : highlighters) {
dumpStream.println(highlighter + " : " + highlighter.getTextAttributes());
}
dumpStream.println("------------");
}
private void clearUnusedHightlighters() {
Set<RangeHighlighter> unused = new com.intellij.util.containers.HashSet<RangeHighlighter>();
for (RangeHighlighter highlighter : myHighlighters) {
if (highlighter.getUserData(MARKER_USED) == null) {
unused.add(highlighter);
} else {
highlighter.putUserData(MARKER_USED, null);
}
}
myHighlighters.removeAll(unused);
Project project = mySearchResults.getProject();
if (project != null && !project.isDisposed()) {
for (RangeHighlighter highlighter : unused) {
HighlightManager.getInstance(project).removeSegmentHighlighter(mySearchResults.getEditor(), highlighter);
}
}
}
@Override
public void cursorMoved() {
updateInSelectionHighlighters();
updateCursorHighlighting();
}
@Override
public void updateFinished() {
dumpState();
}
private void updateCursorHighlighting() {
hideBalloon();
if (myCursorHighlighter != null) {
HighlightManager.getInstance(mySearchResults.getProject()).removeSegmentHighlighter(mySearchResults.getEditor(), myCursorHighlighter);
myCursorHighlighter = null;
}
final FindResult cursor = mySearchResults.getCursor();
Editor editor = mySearchResults.getEditor();
if (cursor != null) {
Set<RangeHighlighter> dummy = new HashSet<RangeHighlighter>();
Color color = editor.getColorsScheme().getColor(EditorColors.CARET_COLOR);
highlightRange(cursor, new TextAttributes(null, null, color, EffectType.ROUNDED_BOX, 0), dummy);
if (!dummy.isEmpty()) {
myCursorHighlighter = dummy.iterator().next();
}
editor.getScrollingModel().runActionOnScrollingFinished(new Runnable() {
@Override
public void run() {
showReplacementPreview();
}
});
}
}
public LivePreview(SearchResults searchResults) {
mySearchResults = searchResults;
searchResultsUpdated(searchResults);
searchResults.addListener(this);
myListeningSelection = true;
mySearchResults.getEditor().getSelectionModel().addSelectionListener(this);
}
public Delegate getDelegate() {
return myDelegate;
}
public void setDelegate(Delegate delegate) {
myDelegate = delegate;
}
public void cleanUp() {
removeFromEditor();
}
public void dispose() {
cleanUp();
mySearchResults.removeListener(this);
}
private void removeFromEditor() {
Editor editor = mySearchResults.getEditor();
if (myReplacementBalloon != null) {
myReplacementBalloon.hide();
}
if (editor != null) {
for (VisibleAreaListener visibleAreaListener : myVisibleAreaListenersToRemove) {
editor.getScrollingModel().removeVisibleAreaListener(visibleAreaListener);
}
myVisibleAreaListenersToRemove.clear();
Project project = mySearchResults.getProject();
if (project != null && !project.isDisposed()) {
for (RangeHighlighter h : myHighlighters) {
HighlightManager.getInstance(project).removeSegmentHighlighter(editor, h);
}
if (myCursorHighlighter != null) {
HighlightManager.getInstance(project).removeSegmentHighlighter(editor, myCursorHighlighter);
myCursorHighlighter = null;
}
}
myHighlighters.clear();
if (myListeningSelection) {
editor.getSelectionModel().removeSelectionListener(this);
myListeningSelection = false;
}
}
}
private void highlightUsages() {
if (mySearchResults.getEditor() == null) return;
if (mySearchResults.getMatchesCount() >= mySearchResults.getMatchesLimit())
return;
for (FindResult range : mySearchResults.getOccurrences()) {
TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES);
if (range.getLength() == 0) {
attributes = attributes.clone();
attributes.setEffectType(EffectType.BOXED);
attributes.setEffectColor(attributes.getBackgroundColor());
}
if (mySearchResults.isExcluded(range)) {
highlightRange(range, strikeout(), myHighlighters);
} else {
highlightRange(range, attributes, myHighlighters);
}
}
updateInSelectionHighlighters();
if (!myListeningSelection) {
mySearchResults.getEditor().getSelectionModel().addSelectionListener(this);
myListeningSelection = true;
}
}
private void updateInSelectionHighlighters() {
if (mySearchResults.getEditor() == null) return;
final SelectionModel selectionModel = mySearchResults.getEditor().getSelectionModel();
int[] starts = selectionModel.getBlockSelectionStarts();
int[] ends = selectionModel.getBlockSelectionEnds();
final HashSet<RangeHighlighter> toRemove = new HashSet<RangeHighlighter>();
Set<RangeHighlighter> toAdd = new HashSet<RangeHighlighter>();
for (RangeHighlighter highlighter : myHighlighters) {
boolean intersectsWithSelection = false;
for (int i = 0; i < starts.length; ++i) {
TextRange selectionRange = new TextRange(starts[i], ends[i]);
intersectsWithSelection = selectionRange.intersects(highlighter.getStartOffset(), highlighter.getEndOffset()) &&
selectionRange.getEndOffset() != highlighter.getStartOffset() &&
highlighter.getEndOffset() != selectionRange.getStartOffset();
if (intersectsWithSelection) break;
}
final Object userData = highlighter.getUserData(IN_SELECTION_KEY);
if (userData != null) {
if (!intersectsWithSelection) {
if (userData == IN_SELECTION2) {
HighlightManager.getInstance(mySearchResults.getProject()).removeSegmentHighlighter(mySearchResults.getEditor(), highlighter);
toRemove.add(highlighter);
} else {
highlighter.putUserData(IN_SELECTION_KEY, null);
}
}
} else if (intersectsWithSelection) {
TextRange cursor = mySearchResults.getCursor();
if (cursor != null && highlighter.getStartOffset() == cursor.getStartOffset() &&
highlighter.getEndOffset() == cursor.getEndOffset()) continue;
final RangeHighlighter toAnnotate = highlightRange(new TextRange(highlighter.getStartOffset(), highlighter.getEndOffset()),
new TextAttributes(null, null, Color.WHITE, EffectType.ROUNDED_BOX, 0), toAdd);
highlighter.putUserData(IN_SELECTION_KEY, IN_SELECTION1);
toAnnotate.putUserData(IN_SELECTION_KEY, IN_SELECTION2);
}
}
myHighlighters.removeAll(toRemove);
myHighlighters.addAll(toAdd);
}
private void showReplacementPreview() {
hideBalloon();
final FindResult cursor = mySearchResults.getCursor();
final Editor editor = mySearchResults.getEditor();
if (myDelegate != null && cursor != null) {
String replacementPreviewText = myDelegate.getStringToReplace(editor, cursor);
if (StringUtil.isEmpty(replacementPreviewText)) {
replacementPreviewText = EMPTY_STRING_DISPLAY_TEXT;
}
final FindModel findModel = mySearchResults.getFindModel();
if (findModel.isRegularExpressions() && findModel.isReplaceState()) {
showBalloon(editor, replacementPreviewText);
}
}
}
private void showBalloon(Editor editor, String replacementPreviewText) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
myReplacementPreviewText = replacementPreviewText;
return;
}
ReplacementView replacementView = new ReplacementView(replacementPreviewText);
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();
myReplacementBalloon.show(new ReplacementBalloonPositionTracker(editor), Balloon.Position.above);
}
private void hideBalloon() {
if (ApplicationManager.getApplication().isUnitTestMode()) {
myReplacementPreviewText = null;
return;
}
if (myReplacementBalloon != null) {
myReplacementBalloon.hide();
myReplacementBalloon = null;
}
}
@NotNull
private RangeHighlighter highlightRange(TextRange textRange, TextAttributes attributes, Set<RangeHighlighter> highlighters) {
if (myInSmartUpdate) {
for (RangeHighlighter highlighter : myHighlighters) {
if (highlighter.isValid() && highlighter.getStartOffset() == textRange.getStartOffset() && highlighter.getEndOffset() == textRange.getEndOffset()) {
if (attributes.equals(highlighter.getTextAttributes())) {
highlighter.putUserData(MARKER_USED, YES);
if (highlighters != myHighlighters) {
highlighters.add(highlighter);
}
return highlighter;
}
}
}
}
final RangeHighlighter highlighter = doHightlightRange(textRange, attributes, highlighters);
if (myInSmartUpdate) {
highlighter.putUserData(MARKER_USED, YES);
}
return highlighter;
}
private RangeHighlighter doHightlightRange(final TextRange textRange, final TextAttributes attributes, Set<RangeHighlighter> highlighters) {
HighlightManager highlightManager = HighlightManager.getInstance(mySearchResults.getProject());
MarkupModelEx markupModel = (MarkupModelEx)mySearchResults.getEditor().getMarkupModel();
final RangeHighlighter[] candidate = new RangeHighlighter[1];
boolean notFound = markupModel.processRangeHighlightersOverlappingWith(
textRange.getStartOffset(), textRange.getEndOffset(),
new Processor<RangeHighlighterEx>() {
@Override
public boolean process(RangeHighlighterEx highlighter) {
if (!highlighter.getEditorFilter().avaliableIn(mySearchResults.getEditor())) return true;
TextAttributes textAttributes =
highlighter.getTextAttributes();
if (highlighter.getUserData(SEARCH_MARKER) != null &&
textAttributes != null &&
textAttributes.equals(attributes) &&
highlighter.getStartOffset() == textRange.getStartOffset() &&
highlighter.getEndOffset() == textRange.getEndOffset()) {
candidate[0] = highlighter;
return false;
}
return true;
}
});
if (!notFound && highlighters.contains(candidate[0])) {
return candidate[0];
}
final ArrayList<RangeHighlighter> dummy = new ArrayList<RangeHighlighter>();
highlightManager.addRangeHighlight(mySearchResults.getEditor(),
textRange.getStartOffset(),
textRange.getEndOffset(),
attributes,
false,
dummy);
final RangeHighlighter h = dummy.get(0);
highlighters.add(h);
h.putUserData(SEARCH_MARKER, YES);
return h;
}
private class ReplacementBalloonPositionTracker extends PositionTracker<Balloon> {
private final Editor myEditor;
public ReplacementBalloonPositionTracker(Editor editor) {
super(editor.getContentComponent());
myEditor = editor;
}
@Override
public RelativePoint recalculateLocation(final Balloon object) {
FindResult cursor = mySearchResults.getCursor();
if (cursor == null) return null;
final TextRange cur = cursor;
int startOffset = cur.getStartOffset();
int endOffset = cur.getEndOffset();
if (startOffset >= myEditor.getDocument().getTextLength()) {
if (!object.isDisposed()) {
requestBalloonHiding(object);
}
return null;
}
if (!SearchResults.insideVisibleArea(myEditor, cur)) {
requestBalloonHiding(object);
VisibleAreaListener visibleAreaListener = new VisibleAreaListener() {
@Override
public void visibleAreaChanged(VisibleAreaEvent e) {
if (SearchResults.insideVisibleArea(myEditor, cur)) {
showReplacementPreview();
final VisibleAreaListener visibleAreaListener = this;
final boolean remove = myVisibleAreaListenersToRemove.remove(visibleAreaListener);
if (remove) {
myEditor.getScrollingModel().removeVisibleAreaListener(visibleAreaListener);
}
}
}
};
myEditor.getScrollingModel().addVisibleAreaListener(visibleAreaListener);
myVisibleAreaListenersToRemove.add(visibleAreaListener);
}
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);
return new RelativePoint(myEditor.getContentComponent(), point);
}
}
private static void requestBalloonHiding(final Balloon object) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
object.hide();
}
});
}
}