hide feature not yet ready for the prime time
[idea/community.git] / platform / lang-impl / src / com / intellij / find / impl / FindManagerImpl.java
1
2 /*
3  * Copyright 2000-2009 JetBrains s.r.o.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package com.intellij.find.impl;
19
20 import com.intellij.codeInsight.highlighting.HighlightManager;
21 import com.intellij.codeInsight.highlighting.HighlightManagerImpl;
22 import com.intellij.codeInsight.hint.HintManager;
23 import com.intellij.codeInsight.hint.HintManagerImpl;
24 import com.intellij.codeInsight.hint.HintUtil;
25 import com.intellij.find.*;
26 import com.intellij.find.findUsages.FindUsagesManager;
27 import com.intellij.lang.Language;
28 import com.intellij.lang.LanguageParserDefinitions;
29 import com.intellij.lang.ParserDefinition;
30 import com.intellij.lexer.Lexer;
31 import com.intellij.openapi.actionSystem.ActionManager;
32 import com.intellij.openapi.actionSystem.AnAction;
33 import com.intellij.openapi.actionSystem.IdeActions;
34 import com.intellij.openapi.application.ApplicationManager;
35 import com.intellij.openapi.application.ex.ApplicationManagerEx;
36 import com.intellij.openapi.components.PersistentStateComponent;
37 import com.intellij.openapi.components.State;
38 import com.intellij.openapi.components.Storage;
39 import com.intellij.openapi.diagnostic.Logger;
40 import com.intellij.openapi.editor.Document;
41 import com.intellij.openapi.editor.Editor;
42 import com.intellij.openapi.editor.ScrollType;
43 import com.intellij.openapi.editor.markup.RangeHighlighter;
44 import com.intellij.openapi.fileEditor.FileEditor;
45 import com.intellij.openapi.fileEditor.TextEditor;
46 import com.intellij.openapi.fileTypes.FileType;
47 import com.intellij.openapi.fileTypes.LanguageFileType;
48 import com.intellij.openapi.fileTypes.SyntaxHighlighter;
49 import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
50 import com.intellij.openapi.keymap.KeymapUtil;
51 import com.intellij.openapi.project.Project;
52 import com.intellij.openapi.project.ProjectManager;
53 import com.intellij.openapi.ui.Messages;
54 import com.intellij.openapi.util.*;
55 import com.intellij.openapi.util.text.StringUtil;
56 import com.intellij.openapi.vfs.VirtualFile;
57 import com.intellij.psi.*;
58 import com.intellij.psi.tree.IElementType;
59 import com.intellij.psi.tree.TokenSet;
60 import com.intellij.ui.LightweightHint;
61 import com.intellij.ui.ReplacePromptDialog;
62 import com.intellij.usages.UsageViewManager;
63 import com.intellij.util.messages.MessageBus;
64 import com.intellij.util.text.StringSearcher;
65 import gnu.trove.THashSet;
66 import org.jdom.Element;
67 import org.jetbrains.annotations.NonNls;
68 import org.jetbrains.annotations.NotNull;
69 import org.jetbrains.annotations.Nullable;
70
71 import javax.swing.*;
72 import java.awt.*;
73 import java.util.Set;
74 import java.util.regex.Matcher;
75 import java.util.regex.Pattern;
76 import java.util.regex.PatternSyntaxException;
77
78 @State(
79   name = "FindManager",
80   storages = {
81     @Storage(
82       id ="other",
83       file = "$WORKSPACE_FILE$"
84     )}
85 )
86 public class FindManagerImpl extends FindManager implements PersistentStateComponent<Element> {
87   private static final Logger LOG = Logger.getInstance("#com.intellij.find.impl.FindManagerImpl");
88
89   private final FindUsagesManager myFindUsagesManager;
90   private boolean isFindWasPerformed = false;
91   private Point myReplaceInFilePromptPos = new Point(-1, -1);
92   private Point myReplaceInProjectPromptPos = new Point(-1, -1);
93   private final FindModel myFindInProjectModel = new FindModel();
94   private final FindModel myFindInFileModel = new FindModel();
95   private FindModel myFindNextModel = null;
96   private static final FindResultImpl NOT_FOUND_RESULT = new FindResultImpl();
97   private final Project myProject;
98   private final MessageBus myBus;
99   private static final Key<Boolean> HIGHLIGHTER_WAS_NOT_FOUND_KEY = Key.create("com.intellij.find.impl.FindManagerImpl.HighlighterNotFoundKey");
100   @NonNls private static final String FIND_USAGES_MANAGER_ELEMENT = "FindUsagesManager";
101   public static final boolean ourHasSearchInCommentsAndLiterals = ApplicationManagerEx.getApplicationEx().isInternal(); // TODO: maxim
102
103   public FindManagerImpl(Project project, FindSettings findSettings, UsageViewManager anotherManager, MessageBus bus) {
104     myProject = project;
105     myBus = bus;
106     findSettings.initModelBySetings(myFindInFileModel);
107     findSettings.initModelBySetings(myFindInProjectModel);
108
109     myFindInFileModel.setCaseSensitive(findSettings.isLocalCaseSensitive());
110     myFindInFileModel.setWholeWordsOnly(findSettings.isLocalWholeWordsOnly());
111
112     myFindUsagesManager = new FindUsagesManager(myProject, anotherManager);
113     myFindInProjectModel.setMultipleFiles(true);
114   }
115
116   public Element getState() {
117     Element element = new Element("FindManager");
118     final Element findUsages = new Element(FIND_USAGES_MANAGER_ELEMENT);
119     element.addContent(findUsages);
120     try {
121       myFindUsagesManager.writeExternal(findUsages);
122     }
123     catch (WriteExternalException e) {
124       LOG.error(e);
125     }
126     return element;
127   }
128
129   public void loadState(final Element state) {
130     final Element findUsages = state.getChild(FIND_USAGES_MANAGER_ELEMENT);
131     if (findUsages != null) {
132       try {
133         myFindUsagesManager.readExternal(findUsages);
134       }
135       catch (InvalidDataException e) {
136         LOG.error(e);
137       }
138     }
139   }
140
141   public int showPromptDialog(final FindModel model, String title) {
142     ReplacePromptDialog replacePromptDialog = new ReplacePromptDialog(model.isMultipleFiles(), title, myProject) {
143       @Nullable
144       public Point getInitialLocation() {
145         if (model.isMultipleFiles() && myReplaceInProjectPromptPos.x >= 0 && myReplaceInProjectPromptPos.y >= 0){
146           return myReplaceInProjectPromptPos;
147         }
148         if (!model.isMultipleFiles() && myReplaceInFilePromptPos.x >= 0 && myReplaceInFilePromptPos.y >= 0){
149           return myReplaceInFilePromptPos;
150         }
151         return null;
152       }
153     };
154
155     replacePromptDialog.show();
156
157     if (model.isMultipleFiles()){
158       myReplaceInProjectPromptPos = replacePromptDialog.getLocation();
159     }
160     else{
161       myReplaceInFilePromptPos = replacePromptDialog.getLocation();
162     }
163     return replacePromptDialog.getExitCode();
164   }
165
166   public void showFindDialog(@NotNull final FindModel model, @NotNull final Runnable okHandler) {
167     FindDialog findDialog = new FindDialog(myProject, model, new Runnable(){
168       public void run() {
169         String stringToFind = model.getStringToFind();
170         if (stringToFind == null || stringToFind.length() == 0){
171           return;
172         }
173         FindSettings.getInstance().addStringToFind(stringToFind);
174         if (!model.isMultipleFiles()){
175           setFindWasPerformed();
176         }
177         if (model.isReplaceState()){
178           FindSettings.getInstance().addStringToReplace(model.getStringToReplace());
179         }
180         if (model.isMultipleFiles() && !model.isProjectScope()){
181           FindSettings.getInstance().addDirectory(model.getDirectoryName());
182
183           if (model.getDirectoryName()!=null) {
184             myFindInProjectModel.setWithSubdirectories(model.isWithSubdirectories());
185           }
186         }
187         okHandler.run();
188       }
189     });
190     findDialog.setModal(false);
191     findDialog.show();
192   }
193
194   @NotNull
195   public FindModel getFindInFileModel() {
196     return myFindInFileModel;
197   }
198
199   @NotNull
200   public FindModel getFindInProjectModel() {
201     myFindInProjectModel.setFromCursor(false);
202     myFindInProjectModel.setForward(true);
203     myFindInProjectModel.setGlobal(true);
204     return myFindInProjectModel;
205   }
206
207   public boolean findWasPerformed() {
208     return isFindWasPerformed;
209   }
210
211   public void setFindWasPerformed() {
212     isFindWasPerformed = true;
213     myFindUsagesManager.clearFindingNextUsageInFile();
214   }
215
216   public FindModel getFindNextModel() {
217     return myFindNextModel;
218   }
219
220   public FindModel getFindNextModel(@NotNull final Editor editor) {
221     if (myFindNextModel == null) return null;
222
223     final JComponent header = editor.getHeaderComponent();
224     if (header instanceof EditorSearchComponent) {
225       final EditorSearchComponent searchComponent = (EditorSearchComponent)header;
226       final String textInField = searchComponent.getTextInField();
227       if (!Comparing.equal(textInField, myFindInFileModel.getStringToFind())) {
228         FindModel patched = new FindModel();
229         patched.copyFrom(myFindNextModel);
230         patched.setStringToFind(textInField);
231         return patched;
232       }
233     }
234
235     return myFindNextModel;
236   }
237
238   public void setFindNextModel(FindModel findNextModel) {
239     myFindNextModel = findNextModel;
240     myBus.syncPublisher(FIND_MODEL_TOPIC).findNextModelChanged();
241   }
242
243   @NotNull
244   public FindResult findString(@NotNull CharSequence text, int offset, @NotNull FindModel model){
245     return findString(text, offset, model, null);
246   }
247
248   @NotNull
249   @Override
250   public FindResult findString(@NotNull CharSequence text, int offset, @NotNull FindModel model, @Nullable VirtualFile file) {
251     if (LOG.isDebugEnabled()) {
252       LOG.debug("offset="+offset);
253       LOG.debug("textlength="+text.length());
254       LOG.debug(model.toString());
255     }
256
257     while(true){
258       FindResult result = doFindString(text, offset, model, file);
259
260       if (!model.isWholeWordsOnly()) {
261         return result;
262       }
263       if (!result.isStringFound()){
264         return result;
265       }
266       if (isWholeWord(text, result.getStartOffset(), result.getEndOffset())){
267         return result;
268       }
269
270       offset = model.isForward() ? result.getStartOffset() + 1 : result.getEndOffset() - 1;
271     }
272   }
273
274   private static boolean isWholeWord(CharSequence text, int startOffset, int endOffset) {
275     boolean isWordStart = startOffset == 0 ||
276                           !Character.isJavaIdentifierPart(text.charAt(startOffset - 1)) ||
277                           startOffset > 1 && text.charAt(startOffset - 2) == '\\';
278
279     boolean isWordEnd = endOffset == text.length() ||
280                         !Character.isJavaIdentifierPart(text.charAt(endOffset)) ||
281                         endOffset > 0 && !Character.isJavaIdentifierPart(text.charAt(endOffset - 1));
282
283     return isWordStart && isWordEnd;
284   }
285
286   private static FindResult doFindString(CharSequence text, int offset, final FindModel model, @Nullable VirtualFile file) {
287     String toFind = model.getStringToFind();
288     if (toFind == null || toFind.length() == 0){
289       return NOT_FOUND_RESULT;
290     }
291
292     if (model.isInCommentsOnly() || model.isInStringLiteralsOnly()) {
293       return findInCommentsAndLiterals(text, offset, model, file);
294     }
295
296     if (model.isRegularExpressions()){
297       return findStringByRegularExpression(text, offset, model);
298     }
299
300     final StringSearcher searcher = createStringSearcher(model);
301
302     int index;
303     if (model.isForward()){
304       final int res = searcher.scan(text.subSequence(offset, text.length()));
305       index = res < 0 ? -1 : res + offset;
306     }
307     else{
308       index = searcher.scan(text.subSequence(0, offset));
309     }
310     if (index < 0){
311       return NOT_FOUND_RESULT;
312     }
313     return new FindResultImpl(index, index + toFind.length());
314   }
315
316   private static StringSearcher createStringSearcher(FindModel model) {
317     return new StringSearcher(model.getStringToFind(), model.isCaseSensitive(), model.isForward());
318   }
319
320   static class CommentsLiteralsSearchData {
321     final VirtualFile lastFile;
322     int startOffset = 0;
323     final Lexer lexer;
324
325     TokenSet tokensOfInterest;
326     final StringSearcher searcher;
327     final Matcher matcher;
328     final Set<Language> relevantLanguages;
329
330     public CommentsLiteralsSearchData(VirtualFile lastFile, Set<Language> relevantLanguages, Lexer lexer, TokenSet tokensOfInterest,
331                                       StringSearcher searcher, Matcher matcher) {
332       this.lastFile = lastFile;
333       this.lexer = lexer;
334       this.tokensOfInterest = tokensOfInterest;
335       this.searcher = searcher;
336       this.matcher = matcher;
337       this.relevantLanguages = relevantLanguages;
338     }
339   }
340
341   private static final Key<CommentsLiteralsSearchData> ourCommentsLiteralsSearchDataKey = Key.create("comments.literals.search.data");
342
343   private static FindResult findInCommentsAndLiterals(CharSequence text, int offset, FindModel model, final VirtualFile file) {
344     if (file == null) return NOT_FOUND_RESULT;
345
346     FileType ftype = file.getFileType();
347     Language lang = null;
348     if (ftype instanceof LanguageFileType) {
349       lang = ((LanguageFileType)ftype).getLanguage();
350     }
351
352     if(lang == null) return NOT_FOUND_RESULT;
353
354     CommentsLiteralsSearchData data = model.getUserData(ourCommentsLiteralsSearchDataKey);
355     if (data == null || data.lastFile != file) {
356       Lexer lexer = getLexer(file, lang);
357
358       TokenSet tokensOfInterest = TokenSet.EMPTY;
359       final Language finalLang = lang;
360       Set<Language> relevantLanguages = ApplicationManager.getApplication().runReadAction(new Computable<Set<Language>>() {
361         public Set<Language> compute() {
362           THashSet<Language> result = new THashSet<Language>();
363
364           for(Project project:ProjectManager.getInstance().getOpenProjects()) {
365             FileViewProvider viewProvider = PsiManager.getInstance(project).findViewProvider(file);
366             if (viewProvider != null) {
367               result.addAll(viewProvider.getLanguages());
368               break;
369             }
370           }
371
372           if (result.size() == 0) {
373             result.add(finalLang);
374           }
375           return result;
376         }
377       });
378
379       for (Language relevantLanguage:relevantLanguages) {
380         tokensOfInterest = addTokenTypesForLanguage(model, relevantLanguage, tokensOfInterest);
381       }
382
383       if(model.isInStringLiteralsOnly()) {
384         // TODO: xml does not have string literals defined so we add XmlAttributeValue element type as convenience
385         final Lexer xmlLexer = getLexer(null, Language.findLanguageByID("XML"));
386         final String marker = "xxx";
387         xmlLexer.start("<a href=\"" + marker+ "\" />");
388
389         while (!marker.equals(xmlLexer.getTokenText())) {
390           xmlLexer.advance();
391           if (xmlLexer.getTokenType() == null) break;
392         }
393
394         IElementType convenienceXmlAttrType = xmlLexer.getTokenType();
395         if (convenienceXmlAttrType != null) {
396           tokensOfInterest = TokenSet.orSet(tokensOfInterest, TokenSet.create(convenienceXmlAttrType));
397         }
398       }
399
400       Matcher matcher = model.isRegularExpressions() ? compileRegExp(model, ""):null;
401       StringSearcher searcher = matcher != null ? null: createStringSearcher(model);
402       data = new CommentsLiteralsSearchData(file, relevantLanguages, lexer, tokensOfInterest, searcher, matcher);
403       model.putUserData(ourCommentsLiteralsSearchDataKey, data);
404     }
405
406     data.lexer.start(text, data.startOffset, text.length(), 0);
407
408     IElementType tokenType;
409     final Lexer lexer = data.lexer;
410     TokenSet tokens = data.tokensOfInterest;
411
412     int lastGoodOffset = 0;
413     boolean scanningForward = model.isForward();
414     FindResultImpl prevFindResult = NOT_FOUND_RESULT;
415
416     while((tokenType = lexer.getTokenType()) != null) {
417       if (lexer.getState() == 0) lastGoodOffset = lexer.getTokenStart();
418
419       if (tokens.contains(tokenType)) {
420         int start = lexer.getTokenStart();
421
422         if (start >= offset || !scanningForward) {
423           FindResultImpl findResult = null;
424
425           if (data.searcher != null) {
426             int i = data.searcher.scan(text, start, lexer.getTokenEnd());
427             if (i != -1) findResult = new FindResultImpl(i, i + model.getStringToFind().length());
428           } else {
429             data.matcher.reset(text.subSequence(start, lexer.getTokenEnd()));
430             if (data.matcher.find()) {
431               int matchStart = data.matcher.start();
432               findResult = new FindResultImpl(start + matchStart, start + data.matcher.end());
433             }
434           }
435
436           if (findResult != null) {
437             if (scanningForward) {
438               data.startOffset = lastGoodOffset;
439               return findResult;
440             } else {
441               if (start >= offset) return prevFindResult;
442               prevFindResult = findResult;
443             }
444           }
445         }
446       } else {
447         Language tokenLang = tokenType.getLanguage();
448         if (tokenLang != lang && tokenLang != Language.ANY && !data.relevantLanguages.contains(tokenLang)) {
449           tokens = addTokenTypesForLanguage(model, tokenLang, tokens);
450           data.tokensOfInterest = tokens;
451           data.relevantLanguages.add(tokenLang);
452         }
453       }
454
455       lexer.advance();
456     }
457
458     return prevFindResult;
459   }
460
461   private static TokenSet addTokenTypesForLanguage(FindModel model, Language lang, TokenSet tokensOfInterest) {
462     ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(lang);
463     if (definition != null) {
464       tokensOfInterest = TokenSet.orSet(tokensOfInterest, model.isInCommentsOnly() ? definition.getCommentTokens(): TokenSet.EMPTY);
465       tokensOfInterest = TokenSet.orSet(tokensOfInterest, model.isInStringLiteralsOnly() ? definition.getStringLiteralElements() : TokenSet.EMPTY);
466     }
467     return tokensOfInterest;
468   }
469
470   private static Lexer getLexer(VirtualFile file, Language lang) {
471     SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(lang, null, file);
472     assert syntaxHighlighter != null:"Syntax highlighter is null:"+file;
473     return syntaxHighlighter.getHighlightingLexer();
474   }
475
476   private static FindResult findStringByRegularExpression(CharSequence text, int startOffset, FindModel model) {
477     Matcher matcher = compileRegExp(model, text);
478
479     if (model.isForward()){
480       if (matcher.find(startOffset)) {
481         if (matcher.end() <= text.length()) {
482           return new FindResultImpl(matcher.start(), matcher.end());
483         }
484       }
485       return NOT_FOUND_RESULT;
486     }
487     else{
488       int start = -1;
489       int end = -1;
490       while(matcher.find() && matcher.end() < startOffset){
491         start = matcher.start();
492         end = matcher.end();
493       }
494       if (start < 0){
495         return NOT_FOUND_RESULT;
496       }
497       return new FindResultImpl(start, end);
498     }
499   }
500
501   private static Matcher compileRegExp(FindModel model, CharSequence text) {
502     String toFind = model.getStringToFind();
503
504     Pattern pattern;
505     try {
506       pattern = Pattern.compile(toFind, model.isCaseSensitive() ? Pattern.MULTILINE : Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
507     }
508     catch(PatternSyntaxException e){
509       LOG.error(e);
510       return null;
511     }
512
513     return pattern.matcher(text);
514   }
515
516   public String getStringToReplace(@NotNull String foundString, FindModel model) {
517     if (model == null) {
518       return null;
519     }
520     String toReplace = model.getStringToReplace();
521     if (!model.isRegularExpressions()) {
522       if (model.isPreserveCase()) {
523         return replaceWithCaseRespect (toReplace, foundString);
524       }
525       return toReplace;
526     }
527
528     String toFind = model.getStringToFind();
529
530     Pattern pattern;
531     try{
532       int flags = Pattern.MULTILINE;
533       if (!model.isCaseSensitive()) {
534         flags |= Pattern.CASE_INSENSITIVE;
535       }
536       pattern = Pattern.compile(toFind, flags);
537     }
538     catch(PatternSyntaxException e){
539       return toReplace;
540     }
541
542     Matcher matcher = pattern.matcher(foundString);
543     if (matcher.matches()) {
544       try {
545         return matcher.replaceAll(StringUtil.unescapeStringCharacters(toReplace));
546       }
547       catch (Exception e) {
548         ApplicationManager.getApplication().invokeLater(new Runnable() {
549               public void run() {
550                 Messages.showErrorDialog(myProject, FindBundle.message("find.replace.invalid.replacement.string"),
551                                          FindBundle.message("find.replace.invalid.replacement.string.title"));
552               }
553             });
554         return null;
555       }
556     } else {
557       // There are valid situations (for example, IDEADEV-2543 or positive lookbehind assertions)
558       // where an expression which matches a string in context will not match the same string
559       // separately).
560       return toReplace;
561     }
562   }
563
564   private static String replaceWithCaseRespect(String toReplace, String foundString) {
565     if (foundString.length() == 0 || toReplace.length() == 0) return toReplace;
566     StringBuilder buffer = new StringBuilder();
567
568     if (Character.isUpperCase(foundString.charAt(0))) {
569       buffer.append(Character.toUpperCase(toReplace.charAt(0)));
570     } else {
571       buffer.append(Character.toLowerCase(toReplace.charAt(0)));
572     }
573     if (toReplace.length() == 1) return buffer.toString();
574
575     if (foundString.length() == 1) {
576       buffer.append(toReplace.substring(1));
577       return buffer.toString();
578     }
579
580     boolean isTailUpper = true;
581     boolean isTailLower = true;
582     for (int i = 1; i < foundString.length(); i++) {
583       isTailUpper &= Character.isUpperCase(foundString.charAt(i));
584       isTailLower &= Character.isLowerCase(foundString.charAt(i));
585       if (!isTailUpper && !isTailLower) break;
586     }
587
588     if (isTailUpper) {
589       buffer.append(toReplace.substring(1).toUpperCase());
590     } else if (isTailLower) {
591       buffer.append(toReplace.substring(1).toLowerCase());
592     } else {
593       buffer.append(toReplace.substring(1));
594     }
595     return buffer.toString();
596   }
597
598   public boolean canFindUsages(@NotNull PsiElement element) {
599     return myFindUsagesManager.canFindUsages(element);
600   }
601
602   public void findUsages(@NotNull PsiElement element) {
603     myFindUsagesManager.findUsages(element, null, null);
604   }
605
606   public void findUsagesInEditor(@NotNull PsiElement element, @NotNull FileEditor fileEditor) {
607     if (fileEditor instanceof TextEditor) {
608       TextEditor textEditor = (TextEditor)fileEditor;
609       Editor editor = textEditor.getEditor();
610       Document document = editor.getDocument();
611       PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
612
613       myFindUsagesManager.findUsages(element, psiFile, fileEditor);
614     }
615   }
616
617   public boolean findNextUsageInEditor(@NotNull FileEditor fileEditor) {
618     if (fileEditor instanceof TextEditor) {
619       TextEditor textEditor = (TextEditor)fileEditor;
620       Editor editor = textEditor.getEditor();
621
622       FindModel model = getFindNextModel(editor);
623       if (model != null && model.searchHighlighters()) {
624         RangeHighlighter[] highlighters = ((HighlightManagerImpl)HighlightManager.getInstance(myProject)).getHighlighters(editor);
625         if (highlighters.length > 0) {
626           return highlightNextHighlighter(highlighters, editor, editor.getCaretModel().getOffset(), true, false);
627         }
628       }
629     }
630
631     return myFindUsagesManager.findNextUsageInFile(fileEditor);
632   }
633
634   private static boolean highlightNextHighlighter(RangeHighlighter[] highlighters, Editor editor, int offset, boolean isForward, boolean secondPass) {
635     RangeHighlighter highlighterToSelect = null;
636     Object wasNotFound = editor.getUserData(HIGHLIGHTER_WAS_NOT_FOUND_KEY);
637     for (RangeHighlighter highlighter : highlighters) {
638       int start = highlighter.getStartOffset();
639       int end = highlighter.getEndOffset();
640       if (highlighter.isValid() && start < end) {
641         if (isForward && (start > offset || start == offset && secondPass)) {
642           if (highlighterToSelect == null || highlighterToSelect.getStartOffset() > start) highlighterToSelect = highlighter;
643         }
644         if (!isForward && (end < offset || end == offset && secondPass)) {
645           if (highlighterToSelect == null || highlighterToSelect.getEndOffset() < end) highlighterToSelect = highlighter;
646         }
647       }
648     }
649     if (highlighterToSelect != null) {
650       editor.getSelectionModel().setSelection(highlighterToSelect.getStartOffset(), highlighterToSelect.getEndOffset());
651       editor.getCaretModel().moveToOffset(highlighterToSelect.getStartOffset());
652       ScrollType scrollType;
653       if (!secondPass) {
654         scrollType = isForward ? ScrollType.CENTER_DOWN : ScrollType.CENTER_UP;
655       }
656       else {
657         scrollType = isForward ? ScrollType.CENTER_UP : ScrollType.CENTER_DOWN;
658       }
659       editor.getScrollingModel().scrollToCaret(scrollType);
660       editor.putUserData(HIGHLIGHTER_WAS_NOT_FOUND_KEY, null);
661       return true;
662     }
663
664     if (wasNotFound == null) {
665       editor.putUserData(HIGHLIGHTER_WAS_NOT_FOUND_KEY, Boolean.TRUE);
666       String message = FindBundle.message("find.highlight.no.more.highlights.found");
667       if (isForward) {
668         AnAction action=ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_NEXT);
669         String shortcutsText=KeymapUtil.getFirstKeyboardShortcutText(action);
670         if (shortcutsText.length() > 0) {
671           message = FindBundle.message("find.search.again.from.top.hotkey.message", message, shortcutsText);
672         }
673         else {
674           message = FindBundle.message("find.search.again.from.top.action.message", message);
675         }
676       }
677       else {
678         AnAction action=ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_PREVIOUS);
679         String shortcutsText=KeymapUtil.getFirstKeyboardShortcutText(action);
680         if (shortcutsText.length() > 0) {
681           message = FindBundle.message("find.search.again.from.bottom.hotkey.message", message, shortcutsText);
682         }
683         else {
684           message = FindBundle.message("find.search.again.from.bottom.action.message", message);
685         }
686       }
687       JComponent component = HintUtil.createInformationLabel(message);
688       final LightweightHint hint = new LightweightHint(component);
689       HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, HintManager.UNDER, HintManager.HIDE_BY_ANY_KEY |
690                                                                                         HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING, 0, false);
691       return true;
692     } else if (!secondPass) {
693       offset = isForward ? 0 : editor.getDocument().getTextLength();
694       return highlightNextHighlighter(highlighters, editor, offset, isForward, true);
695     }
696
697     return false;
698   }
699
700   public boolean findPreviousUsageInEditor(@NotNull FileEditor fileEditor) {
701     if (fileEditor instanceof TextEditor) {
702       TextEditor textEditor = (TextEditor)fileEditor;
703       Editor editor = textEditor.getEditor();
704
705       FindModel model = getFindNextModel(editor);
706       if (model != null && model.searchHighlighters()) {
707         RangeHighlighter[] highlighters = ((HighlightManagerImpl)HighlightManager.getInstance(myProject)).getHighlighters(editor);
708         if (highlighters.length > 0) {
709           return highlightNextHighlighter(highlighters, editor, editor.getCaretModel().getOffset(), false, false);
710         }
711       }
712     }
713
714     return myFindUsagesManager.findPreviousUsageInFile(fileEditor);
715   }
716
717   public FindUsagesManager getFindUsagesManager() {
718     return myFindUsagesManager;
719   }
720 }
721