/* * Copyright 2000-2017 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.util.EditorUtil; 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.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.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class LivePreview implements SearchResults.SearchResultsListener, SelectionListener, DocumentListener { 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<>(); private RangeHighlighter myCursorHighlighter; private final List<VisibleAreaListener> myVisibleAreaListenersToRemove = new ArrayList<>(); 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, Font.PLAIN); } @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<>(); for (RangeHighlighter highlighter : highlighters) { ranges.add(new Pair<>(highlighter.getStartOffset(), '[')); ranges.add(new Pair<>(highlighter.getEndOffset(), ']')); } SelectionModel selectionModel = editor.getSelectionModel(); if (selectionModel.getSelectionStart() != selectionModel.getSelectionEnd()) { ranges.add(new Pair<>(selectionModel.getSelectionStart(), '<')); ranges.add(new Pair<>(selectionModel.getSelectionEnd(), '>')); } ranges.add(new Pair<>(-1, '\n')); ranges.add(new Pair<>(editor.getDocument().getTextLength() + 1, '\n')); ContainerUtil.sort(ranges, (pair, 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<>(); 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 && cursor.getEndOffset() <= editor.getDocument().getTextLength()) { Set<RangeHighlighter> dummy = new HashSet<>(); Color color = editor.getColorsScheme().getColor(EditorColors.CARET_COLOR); highlightRange(cursor, new TextAttributes(null, null, color, EffectType.ROUNDED_BOX, Font.PLAIN), dummy); if (!dummy.isEmpty()) { myCursorHighlighter = dummy.iterator().next(); } editor.getScrollingModel().runActionOnScrollingFinished(() -> 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()) { if (range.getEndOffset() > mySearchResults.getEditor().getDocument().getTextLength()) continue; 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<>(); Set<RangeHighlighter> toAdd = new HashSet<>(); for (RangeHighlighter highlighter : myHighlighters) { if (!highlighter.isValid()) continue; 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, Font.PLAIN), 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(); if (!mySearchResults.isUpToDate()) return; 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(); EditorUtil.disposeWithEditor(editor, myReplacementBalloon); myReplacementBalloon.show(new ReplacementBalloonPositionTracker(editor), Balloon.Position.below); } 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(), highlighter -> { 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<>(); 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 (endOffset > 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 + myEditor.getLineHeight()); return new RelativePoint(myEditor.getContentComponent(), point); } } private static void requestBalloonHiding(final Balloon object) { ApplicationManager.getApplication().invokeLater(() -> object.hide()); } }