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