blob: 0e497ddea3c4c398ff6186cf433aedff4d7607c4 [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.codeInsight.navigation;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.hint.HintUtil;
import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.actionSystem.TypedAction;
import com.intellij.openapi.editor.actionSystem.TypedActionHandler;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.ui.HintHint;
import com.intellij.ui.JBColor;
import com.intellij.ui.LightweightHint;
import com.intellij.util.text.StringSearcher;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class IncrementalSearchHandler {
private static final Key<PerEditorSearchData> SEARCH_DATA_IN_EDITOR_VIEW_KEY = Key.create("IncrementalSearchHandler.SEARCH_DATA_IN_EDITOR_VIEW_KEY");
private static final Key<PerHintSearchData> SEARCH_DATA_IN_HINT_KEY = Key.create("IncrementalSearchHandler.SEARCH_DATA_IN_HINT_KEY");
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.navigation.IncrementalSearchHandler");
private static boolean ourActionsRegistered = false;
private static class PerHintSearchData {
final Project project;
final JLabel label;
int searchStart;
RangeHighlighter segmentHighlighter;
boolean ignoreCaretMove = false;
public PerHintSearchData(Project project, JLabel label) {
this.project = project;
this.label = label;
}
}
private static class PerEditorSearchData {
LightweightHint hint;
String lastSearch;
}
public static boolean isHintVisible(final Editor editor) {
final PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
return data != null && data.hint != null && data.hint.isVisible();
}
public void invoke(Project project, final Editor editor) {
if (!ourActionsRegistered){
ourActionsRegistered = true;
EditorActionManager actionManager = EditorActionManager.getInstance();
TypedAction typedAction = actionManager.getTypedAction();
typedAction.setupHandler(new MyTypedHandler(typedAction.getHandler()));
actionManager.setActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE, new BackSpaceHandler(actionManager.getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE)));
actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP, new UpHandler(actionManager.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP)));
actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN, new DownHandler(actionManager.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN)));
}
FeatureUsageTracker.getInstance().triggerFeatureUsed("editing.incremental.search");
String selection = editor.getSelectionModel().getSelectedText();
JLabel label2 = new MyLabel(selection == null ? "" : selection);
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
if (data == null) {
data = new PerEditorSearchData();
} else {
if (data.hint != null) {
if (data.lastSearch != null) {
PerHintSearchData hintData = data.hint.getUserData(SEARCH_DATA_IN_HINT_KEY);
//The user has not started typing
if ("".equals(hintData.label.getText())) {
label2 = new MyLabel(data.lastSearch);
}
}
data.hint.hide();
}
}
JLabel label1 = new MyLabel(" " + CodeInsightBundle.message("incremental.search.tooltip.prefix"));
label1.setFont(UIUtil.getLabelFont().deriveFont(Font.BOLD));
JPanel panel = new MyPanel(label1);
panel.add(label1, BorderLayout.WEST);
panel.add(label2, BorderLayout.CENTER);
panel.setBorder(BorderFactory.createLineBorder(Color.black));
final DocumentListener[] documentListener = new DocumentListener[1];
final CaretListener[] caretListener = new CaretListener[1];
final Document document = editor.getDocument();
final LightweightHint hint = new LightweightHint(panel) {
@Override
public void hide() {
PerHintSearchData data = getUserData(SEARCH_DATA_IN_HINT_KEY);
LOG.assertTrue(data != null);
String prefix = data.label.getText();
super.hide();
if (data.segmentHighlighter != null){
data.segmentHighlighter.dispose();
}
PerEditorSearchData editorData = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
editorData.hint = null;
editorData.lastSearch = prefix;
if (documentListener[0] != null){
document.removeDocumentListener(documentListener[0]);
}
if (caretListener[0] != null){
CaretListener listener = caretListener[0];
editor.getCaretModel().removeCaretListener(listener);
}
}
};
documentListener[0] = new DocumentAdapter() {
@Override
public void documentChanged(DocumentEvent e) {
if (!hint.isVisible()) return;
hint.hide();
}
};
document.addDocumentListener(documentListener[0]);
caretListener[0] = new CaretAdapter() {
@Override
public void caretPositionChanged(CaretEvent e) {
PerHintSearchData data = hint.getUserData(SEARCH_DATA_IN_HINT_KEY);
if (data != null && data.ignoreCaretMove) return;
if (!hint.isVisible()) return;
hint.hide();
}
};
CaretListener listener = caretListener[0];
editor.getCaretModel().addCaretListener(listener);
final JComponent component = editor.getComponent();
int x = SwingUtilities.convertPoint(component,0,0,component).x;
int y = - hint.getComponent().getPreferredSize().height;
Point p = SwingUtilities.convertPoint(component,x,y,component.getRootPane().getLayeredPane());
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, p, HintManagerImpl.HIDE_BY_ESCAPE | HintManagerImpl.HIDE_BY_TEXT_CHANGE, 0, false, new HintHint(editor, p).setAwtTooltip(false));
PerHintSearchData hintData = new PerHintSearchData(project, label2);
hintData.searchStart = editor.getCaretModel().getOffset();
hint.putUserData(SEARCH_DATA_IN_HINT_KEY, hintData);
data.hint = hint;
editor.putUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY, data);
if (hintData.label.getText().length() > 0) {
updatePosition(editor, hintData, true, false);
}
}
private static boolean acceptableRegExp(String pattern) {
final int len = pattern.length();
for(int i=0;i<len;++i) {
switch(pattern.charAt(i)) {
case '*': return true;
}
}
return false;
}
private static void updatePosition(Editor editor, PerHintSearchData data, boolean nothingIfFailed, boolean searchBack) {
final String prefix = data.label.getText();
int matchLength = prefix.length();
int index;
if (matchLength == 0) {
index = data.searchStart;
}
else {
final Document document = editor.getDocument();
final CharSequence text = document.getCharsSequence();
final int length = document.getTextLength();
final boolean caseSensitive = detectSmartCaseSensitive(prefix);
if (acceptableRegExp(prefix)) {
@NonNls final StringBuffer buf = new StringBuffer(prefix.length());
final int len = prefix.length();
for (int i = 0; i < len; ++i) {
final char ch = prefix.charAt(i);
// bother only * withing text
if (ch == '*' && i != 0 && i != len - 1) {
buf.append("\\w");
}
else if ("{}[].+^$*()?".indexOf(ch) != -1) {
// do not bother with other metachars
buf.append('\\');
}
buf.append(ch);
}
try {
Pattern pattern = Pattern.compile(buf.toString(), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
if (searchBack) {
int lastStart = -1;
int lastEnd = -1;
while (matcher.find() && matcher.start() < data.searchStart) {
lastStart = matcher.start();
lastEnd = matcher.end();
}
index = lastStart;
matchLength = lastEnd - lastStart;
}
else if (matcher.find(data.searchStart) || !nothingIfFailed && matcher.find(0)) {
index = matcher.start();
matchLength = matcher.end() - matcher.start();
}
else {
index = -1;
}
}
catch (PatternSyntaxException ex) {
index = -1; // let the user to make the garbage pattern
}
}
else {
StringSearcher searcher = new StringSearcher(prefix, caseSensitive, !searchBack);
if (searchBack) {
index = searcher.scan(text, 0, data.searchStart);
}
else {
index = searcher.scan(text, data.searchStart, length);
index = index < 0 ? -1 : index;
}
if (index < 0 && !nothingIfFailed) {
index = searcher.scan(text);
}
}
}
if (nothingIfFailed && index < 0) return;
if (data.segmentHighlighter != null) {
data.segmentHighlighter.dispose();
data.segmentHighlighter = null;
}
if (index < 0) {
data.label.setForeground(JBColor.RED);
}
else {
data.label.setForeground(JBColor.foreground());
if (matchLength > 0) {
TextAttributes attributes = editor.getColorsScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
data.segmentHighlighter = editor.getMarkupModel()
.addRangeHighlighter(index, index + matchLength, HighlighterLayer.LAST + 1, attributes, HighlighterTargetArea.EXACT_RANGE);
}
data.ignoreCaretMove = true;
editor.getCaretModel().moveToOffset(index);
editor.getSelectionModel().removeSelection();
editor.getScrollingModel().scrollToCaret(ScrollType.CENTER);
data.ignoreCaretMove = false;
IdeDocumentHistory.getInstance(data.project).includeCurrentCommandAsNavigation();
}
}
private static boolean detectSmartCaseSensitive(String prefix) {
boolean hasUpperCase = false;
for(int i = 0; i < prefix.length(); i++){
char c = prefix.charAt(i);
if (Character.isUpperCase(c) && Character.toUpperCase(c) != Character.toLowerCase(c)){
hasUpperCase = true;
break;
}
}
return hasUpperCase;
}
private static class MyLabel extends JLabel {
public MyLabel(String text) {
super(text);
this.setBackground(HintUtil.INFORMATION_COLOR);
this.setForeground(JBColor.foreground());
this.setOpaque(true);
}
}
private static class MyPanel extends JPanel{
private final Component myLeft;
public MyPanel(Component left) {
super(new BorderLayout());
myLeft = left;
}
@Override
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
Dimension lSize = myLeft.getPreferredSize();
return new Dimension(size.width + lSize.width, size.height);
}
public Dimension getTruePreferredSize() {
return super.getPreferredSize();
}
}
public static class MyTypedHandler extends TypedActionHandlerBase {
public MyTypedHandler(@Nullable TypedActionHandler originalHandler) {
super(originalHandler);
}
@Override
public void execute(@NotNull Editor editor, char charTyped, @NotNull DataContext dataContext) {
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
if (data == null || data.hint == null){
if (myOriginalHandler != null) myOriginalHandler.execute(editor, charTyped, dataContext);
}
else{
LightweightHint hint = data.hint;
PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY);
String text = hintData.label.getText();
text += charTyped;
hintData.label.setText(text);
MyPanel comp = (MyPanel)hint.getComponent();
if (comp.getTruePreferredSize().width > comp.getSize().width){
Rectangle bounds = hint.getBounds();
hint.updateBounds(bounds.x, bounds.y);
}
updatePosition(editor, hintData, false, false);
}
}
}
public static class BackSpaceHandler extends EditorActionHandler{
private final EditorActionHandler myOriginalHandler;
public BackSpaceHandler(EditorActionHandler originalAction) {
myOriginalHandler = originalAction;
}
@Override
public void doExecute(Editor editor, Caret caret, DataContext dataContext) {
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
if (data == null || data.hint == null){
myOriginalHandler.execute(editor, caret, dataContext);
}
else{
LightweightHint hint = data.hint;
PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY);
String text = hintData.label.getText();
if (text.length() > 0){
text = text.substring(0, text.length() - 1);
}
hintData.label.setText(text);
updatePosition(editor, hintData, false, false);
}
}
}
public static class UpHandler extends EditorActionHandler {
private final EditorActionHandler myOriginalHandler;
public UpHandler(EditorActionHandler originalHandler) {
myOriginalHandler = originalHandler;
}
@Override
public void doExecute(Editor editor, Caret caret, DataContext dataContext) {
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
if (data == null || data.hint == null){
myOriginalHandler.execute(editor, caret, dataContext);
}
else{
LightweightHint hint = data.hint;
PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY);
String prefix = hintData.label.getText();
if (prefix == null) return;
hintData.searchStart = editor.getCaretModel().getOffset();
if (hintData.searchStart == 0) return;
hintData.searchStart--;
updatePosition(editor, hintData, true, true);
hintData.searchStart = editor.getCaretModel().getOffset();
}
}
@Override
public boolean isEnabled(Editor editor, DataContext dataContext) {
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
return data != null && data.hint != null || myOriginalHandler.isEnabled(editor, dataContext);
}
}
public static class DownHandler extends EditorActionHandler {
private final EditorActionHandler myOriginalHandler;
public DownHandler(EditorActionHandler originalHandler) {
myOriginalHandler = originalHandler;
}
@Override
public void doExecute(Editor editor, Caret caret, DataContext dataContext) {
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
if (data == null || data.hint == null){
myOriginalHandler.execute(editor, caret, dataContext);
}
else{
LightweightHint hint = data.hint;
PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY);
String prefix = hintData.label.getText();
if (prefix == null) return;
hintData.searchStart = editor.getCaretModel().getOffset();
if (hintData.searchStart == editor.getDocument().getTextLength()) return;
hintData.searchStart++;
updatePosition(editor, hintData, true, false);
hintData.searchStart = editor.getCaretModel().getOffset();
}
}
@Override
public boolean isEnabled(Editor editor, DataContext dataContext) {
PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY);
return data != null && data.hint != null || myOriginalHandler.isEnabled(editor, dataContext);
}
}
}