2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com
.intellij
.codeInsight
.navigation
;
19 import com
.intellij
.codeInsight
.CodeInsightBundle
;
20 import com
.intellij
.codeInsight
.TargetElementUtilBase
;
21 import com
.intellij
.codeInsight
.documentation
.DocumentationManager
;
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
.codeInsight
.navigation
.actions
.GotoDeclarationAction
;
26 import com
.intellij
.codeInsight
.navigation
.actions
.GotoTypeDeclarationAction
;
27 import com
.intellij
.ide
.util
.EditSourceUtil
;
28 import com
.intellij
.lang
.documentation
.DocumentationProvider
;
29 import com
.intellij
.navigation
.ItemPresentation
;
30 import com
.intellij
.navigation
.NavigationItem
;
31 import com
.intellij
.openapi
.actionSystem
.IdeActions
;
32 import com
.intellij
.openapi
.actionSystem
.MouseShortcut
;
33 import com
.intellij
.openapi
.actionSystem
.Shortcut
;
34 import com
.intellij
.openapi
.application
.ApplicationManager
;
35 import com
.intellij
.openapi
.components
.AbstractProjectComponent
;
36 import com
.intellij
.openapi
.editor
.Document
;
37 import com
.intellij
.openapi
.editor
.Editor
;
38 import com
.intellij
.openapi
.editor
.EditorFactory
;
39 import com
.intellij
.openapi
.editor
.LogicalPosition
;
40 import com
.intellij
.openapi
.editor
.colors
.EditorColorsManager
;
41 import com
.intellij
.openapi
.editor
.colors
.TextAttributesKey
;
42 import com
.intellij
.openapi
.editor
.event
.*;
43 import com
.intellij
.openapi
.editor
.markup
.*;
44 import com
.intellij
.openapi
.fileEditor
.FileEditorManager
;
45 import com
.intellij
.openapi
.fileEditor
.FileEditorManagerAdapter
;
46 import com
.intellij
.openapi
.fileEditor
.FileEditorManagerEvent
;
47 import com
.intellij
.openapi
.fileEditor
.FileEditorManagerListener
;
48 import com
.intellij
.openapi
.keymap
.Keymap
;
49 import com
.intellij
.openapi
.keymap
.KeymapManager
;
50 import com
.intellij
.openapi
.project
.DumbAwareRunnable
;
51 import com
.intellij
.openapi
.project
.DumbService
;
52 import com
.intellij
.openapi
.project
.IndexNotReadyException
;
53 import com
.intellij
.openapi
.project
.Project
;
54 import com
.intellij
.openapi
.startup
.StartupManager
;
55 import com
.intellij
.openapi
.ui
.MultiLineLabelUI
;
56 import com
.intellij
.openapi
.util
.Comparing
;
57 import com
.intellij
.openapi
.util
.TextRange
;
58 import com
.intellij
.openapi
.vfs
.VirtualFile
;
59 import com
.intellij
.pom
.Navigatable
;
60 import com
.intellij
.psi
.*;
61 import com
.intellij
.psi
.impl
.source
.tree
.injected
.InjectedLanguageUtil
;
62 import com
.intellij
.psi
.search
.searches
.DefinitionsSearch
;
63 import com
.intellij
.ui
.LightweightHint
;
64 import com
.intellij
.util
.Processor
;
65 import com
.intellij
.util
.ui
.UIUtil
;
66 import org
.jetbrains
.annotations
.NotNull
;
67 import org
.jetbrains
.annotations
.Nullable
;
71 import java
.awt
.event
.*;
72 import java
.util
.ArrayList
;
73 import java
.util
.List
;
75 public class CtrlMouseHandler
extends AbstractProjectComponent
{
76 private final TextAttributes ourReferenceAttributes
;
77 private RangeHighlighter myHighlighter
;
78 private Editor myHighlighterView
;
79 private Cursor myStoredCursor
;
80 private Info myStoredInfo
;
81 private int myStoredModifiers
= 0;
82 private TooltipProvider myTooltipProvider
= null;
83 private final FileEditorManager myFileEditorManager
;
85 private enum BrowseMode
{ None
, Declaration
, TypeDeclaration
, Implementation
}
87 private final KeyListener myEditorKeyListener
= new KeyAdapter() {
88 public void keyPressed(final KeyEvent e
) {
92 public void keyReleased(final KeyEvent e
) {
96 private void handleKey(final KeyEvent e
) {
97 int modifiers
= e
.getModifiers();
98 if ( modifiers
== myStoredModifiers
) {
102 BrowseMode browseMode
= getBrowseMode(modifiers
);
104 if (browseMode
!= BrowseMode
.None
) {
105 if (myTooltipProvider
!= null) {
106 if (browseMode
!= myTooltipProvider
.getBrowseMode()) {
107 disposeHighlighter();
109 myStoredModifiers
= modifiers
;
110 myTooltipProvider
.execute(browseMode
);
113 disposeHighlighter();
114 myTooltipProvider
= null;
119 private final FileEditorManagerListener myFileEditorManagerListener
= new FileEditorManagerAdapter() {
120 public void selectionChanged(FileEditorManagerEvent e
) {
121 disposeHighlighter();
122 myTooltipProvider
= null;
126 private final VisibleAreaListener myVisibleAreaListener
= new VisibleAreaListener() {
127 public void visibleAreaChanged(VisibleAreaEvent e
) {
128 disposeHighlighter();
129 myTooltipProvider
= null;
133 private final EditorMouseAdapter myEditorMouseAdapter
= new EditorMouseAdapter() {
134 public void mouseReleased(EditorMouseEvent e
) {
135 disposeHighlighter();
136 myTooltipProvider
= null;
140 private final EditorMouseMotionListener myEditorMouseMotionListener
= new EditorMouseMotionAdapter() {
141 public void mouseMoved(final EditorMouseEvent e
) {
142 if (e
.isConsumed() || myProject
.isDisposed()) {
145 MouseEvent mouseEvent
= e
.getMouseEvent();
147 Editor editor
= e
.getEditor();
148 if (editor
.getProject() != null && editor
.getProject() != myProject
) return;
149 PsiFile psiFile
= PsiDocumentManager
.getInstance(myProject
).getPsiFile(editor
.getDocument());
150 Point point
= new Point(mouseEvent
.getPoint());
151 if (!PsiDocumentManager
.getInstance(myProject
).isUncommited(editor
.getDocument())) {
152 // when document is committed, try to check injected stuff - it's fast
153 editor
= InjectedLanguageUtil
.getEditorForInjectedLanguageNoCommit(editor
, psiFile
, editor
.logicalPositionToOffset(editor
.xyToLogicalPosition(point
)));
156 LogicalPosition pos
= editor
.xyToLogicalPosition(point
);
157 int offset
= editor
.logicalPositionToOffset(pos
);
158 int selStart
= editor
.getSelectionModel().getSelectionStart();
159 int selEnd
= editor
.getSelectionModel().getSelectionEnd();
161 myStoredModifiers
= mouseEvent
.getModifiers();
162 BrowseMode browseMode
= getBrowseMode(myStoredModifiers
);
164 if (myTooltipProvider
!= null) {
165 myTooltipProvider
.dispose();
168 if (browseMode
== BrowseMode
.None
|| offset
>= selStart
&& offset
< selEnd
) {
169 disposeHighlighter();
170 myTooltipProvider
= null;
174 myTooltipProvider
= new TooltipProvider(editor
, pos
);
175 myTooltipProvider
.execute(browseMode
);
179 private static final TextAttributesKey CTRL_CLICKABLE_ATTRIBUTES_KEY
=
180 TextAttributesKey
.createTextAttributesKey("CTRL_CLICKABLE", new TextAttributes(Color
.blue
, null, Color
.blue
, EffectType
.LINE_UNDERSCORE
, 0));
182 public CtrlMouseHandler(final Project project
, StartupManager startupManager
, EditorColorsManager colorsManager
,
183 FileEditorManager fileEditorManager
) {
185 startupManager
.registerPostStartupActivity(new DumbAwareRunnable(){
187 EditorEventMulticaster eventMulticaster
= EditorFactory
.getInstance().getEventMulticaster();
188 eventMulticaster
.addEditorMouseListener(myEditorMouseAdapter
, project
);
189 eventMulticaster
.addEditorMouseMotionListener(myEditorMouseMotionListener
, project
);
192 ourReferenceAttributes
= colorsManager
.getGlobalScheme().getAttributes(CTRL_CLICKABLE_ATTRIBUTES_KEY
);
193 myFileEditorManager
= fileEditorManager
;
197 public String
getComponentName() {
198 return "CtrlMouseHandler";
201 private static BrowseMode
getBrowseMode(final int modifiers
) {
202 if ( modifiers
!= 0 ) {
203 final Keymap activeKeymap
= KeymapManager
.getInstance().getActiveKeymap();
204 if (matchMouseShourtcut(activeKeymap
, modifiers
, IdeActions
.ACTION_GOTO_DECLARATION
)) return BrowseMode
.Declaration
;
205 if (matchMouseShourtcut(activeKeymap
, modifiers
, IdeActions
.ACTION_GOTO_TYPE_DECLARATION
)) return BrowseMode
.TypeDeclaration
;
206 if (matchMouseShourtcut(activeKeymap
, modifiers
, IdeActions
.ACTION_GOTO_IMPLEMENTATION
)) return BrowseMode
.Implementation
;
208 return BrowseMode
.None
;
211 private static boolean matchMouseShourtcut(final Keymap activeKeymap
, final int modifiers
, final String actionId
) {
212 final MouseShortcut syntheticShortcat
= new MouseShortcut(MouseEvent
.BUTTON1
, modifiers
, 1);
213 for ( Shortcut shortcut
: activeKeymap
.getShortcuts(actionId
)) {
214 if ( shortcut
instanceof MouseShortcut
) {
215 final MouseShortcut mouseShortcut
= (MouseShortcut
)shortcut
;
216 if ( mouseShortcut
.getModifiers() == syntheticShortcat
.getModifiers() ) {
225 private static String
generateInfo(PsiElement element
) {
226 final DocumentationProvider documentationProvider
= DocumentationManager
.getProviderFromElement(element
);
228 String info
= documentationProvider
.getQuickNavigateInfo(element
);
233 if (element
instanceof PsiFile
) {
234 final VirtualFile virtualFile
= ((PsiFile
)element
).getVirtualFile();
235 if (virtualFile
!= null) {
236 return virtualFile
.getPresentableUrl();
240 if (element
instanceof NavigationItem
) {
241 final ItemPresentation presentation
= ((NavigationItem
)element
).getPresentation();
242 if (presentation
!= null) {
243 return presentation
.getPresentableText();
250 private abstract static class Info
{
251 @NotNull protected final PsiElement myElementAtPointer
;
252 public final int myStartOffset
;
253 public final int myEndOffset
;
255 public Info(@NotNull PsiElement elementAtPointer
, int startOffset
, int endOffset
) {
256 myElementAtPointer
= elementAtPointer
;
257 myStartOffset
= startOffset
;
258 myEndOffset
= endOffset
;
261 public Info(@NotNull PsiElement elementAtPointer
) {
262 this(elementAtPointer
, elementAtPointer
.getTextOffset(), elementAtPointer
.getTextOffset() + elementAtPointer
.getTextLength());
265 boolean isSimilarTo(final Info that
) {
266 return Comparing
.equal(myElementAtPointer
, that
.myElementAtPointer
) &&
267 myStartOffset
== that
.myStartOffset
&&
268 myEndOffset
== that
.myEndOffset
;
272 public abstract String
getInfo();
274 public abstract boolean isValid(Document document
);
277 private static void showDumbModeNotification(final Project project
) {
278 DumbService
.getInstance(project
).showDumbModeNotification("Element information is not available during index update");
281 private static class InfoSingle
extends Info
{
282 @NotNull private final PsiElement myTargetElement
;
284 public InfoSingle(@NotNull PsiElement elementAtPointer
, @NotNull PsiElement targetElement
) {
285 super(elementAtPointer
);
286 myTargetElement
= targetElement
;
289 public InfoSingle(final PsiReference ref
, @NotNull final PsiElement targetElement
) {
290 super(ref
.getElement(), ref
.getElement().getTextRange().getStartOffset() + ref
.getRangeInElement().getStartOffset(),
291 ref
.getElement().getTextRange().getStartOffset() + ref
.getRangeInElement().getEndOffset());
292 myTargetElement
= targetElement
;
296 public String
getInfo() {
298 return generateInfo(myTargetElement
);
300 catch (IndexNotReadyException e
) {
301 showDumbModeNotification(myTargetElement
.getProject());
306 public boolean isValid(Document document
) {
307 return myTargetElement
.isValid() &&
308 myTargetElement
!= myElementAtPointer
&&
309 myTargetElement
!= myElementAtPointer
.getParent() &&
310 new TextRange(0, document
.getTextLength()).containsRange(myStartOffset
, myEndOffset
)
311 /* && targetNavigateable(myTargetElement)*/;
315 private static class InfoMultiple
extends Info
{
317 public InfoMultiple(@NotNull final PsiElement elementAtPointer
) {
318 super(elementAtPointer
);
321 public String
getInfo() {
322 return CodeInsightBundle
.message("multiple.implementations.tooltip");
325 public boolean isValid(Document document
) {
326 return new TextRange(0, document
.getTextLength()).containsRange(myStartOffset
, myEndOffset
);
331 private Info
getInfoAt(final Editor editor
, LogicalPosition pos
, BrowseMode browseMode
) {
332 Document document
= editor
.getDocument();
333 PsiFile file
= PsiDocumentManager
.getInstance(myProject
).getPsiFile(document
);
334 if (file
== null) return null;
335 PsiDocumentManager
.getInstance(myProject
).commitAllDocuments();
337 if (TargetElementUtilBase
.inVirtualSpace(editor
, pos
)) {
341 final int offset
= editor
.logicalPositionToOffset(pos
);
343 int selStart
= editor
.getSelectionModel().getSelectionStart();
344 int selEnd
= editor
.getSelectionModel().getSelectionEnd();
346 if (offset
>= selStart
&& offset
< selEnd
) return null;
348 PsiElement targetElement
= null;
350 if (browseMode
== BrowseMode
.TypeDeclaration
) {
352 targetElement
= GotoTypeDeclarationAction
.findSymbolType(editor
, offset
);
354 catch (IndexNotReadyException e
) {
355 showDumbModeNotification(myProject
);
358 else if (browseMode
== BrowseMode
.Declaration
) {
359 PsiReference ref
= TargetElementUtilBase
.findReference(editor
, offset
);
361 PsiElement resolvedElement
= resolve(ref
);
362 if (resolvedElement
!= null) {
363 return new InfoSingle (ref
, resolvedElement
);
366 targetElement
= GotoDeclarationAction
.findTargetElement(myProject
, editor
, offset
);
367 } else if ( browseMode
== BrowseMode
.Implementation
) {
368 final PsiElement element
= TargetElementUtilBase
.getInstance().findTargetElement(editor
, ImplementationSearcher
.getFlags(), offset
);
369 PsiElement
[] targetElements
= new ImplementationSearcher() {
371 protected PsiElement
[] searchDefinitions(final PsiElement element
) {
372 final List
<PsiElement
> found
= new ArrayList
<PsiElement
>(2);
373 DefinitionsSearch
.search(element
).forEach( new Processor
<PsiElement
>() {
374 public boolean process(final PsiElement psiElement
) {
375 found
.add ( psiElement
);
376 return found
.size() != 2;
379 return found
.toArray(new PsiElement
[found
.size()]);
381 }.searchImplementations(editor
, element
, offset
);
382 if ( targetElements
.length
> 1) {
383 PsiElement elementAtPointer
= file
.findElementAt(offset
);
384 if (elementAtPointer
!= null) {
385 return new InfoMultiple(elementAtPointer
);
389 if (targetElements
.length
== 1) {
390 Navigatable descriptor
= EditSourceUtil
.getDescriptor(targetElements
[0]);
391 if (descriptor
== null || !descriptor
.canNavigate()) {
394 targetElement
= targetElements
[ 0 ];
398 if (targetElement
!= null && targetElement
.isPhysical() ) {
399 PsiElement elementAtPointer
= file
.findElementAt(offset
);
400 if ( elementAtPointer
!= null ) {
401 return new InfoSingle(elementAtPointer
, targetElement
);
409 private static PsiElement
resolve(final PsiReference ref
) {
410 PsiElement resolvedElement
= null;
412 if (ref
instanceof PsiPolyVariantReference
) {
413 final ResolveResult
[] psiElements
= ((PsiPolyVariantReference
)ref
).multiResolve(false);
414 if (psiElements
.length
> 0) {
415 final ResolveResult resolveResult
= psiElements
[0];
416 if (resolveResult
!= null) {
417 resolvedElement
= resolveResult
.getElement();
422 resolvedElement
= ref
.resolve();
424 return resolvedElement
;
427 private void disposeHighlighter() {
428 if (myHighlighter
!= null) {
429 myHighlighterView
.getMarkupModel().removeHighlighter(myHighlighter
);
430 Component internalComponent
= myHighlighterView
.getContentComponent();
431 internalComponent
.setCursor(myStoredCursor
);
432 internalComponent
.removeKeyListener(myEditorKeyListener
);
433 myHighlighterView
.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener
);
434 myFileEditorManager
.removeFileEditorManagerListener(myFileEditorManagerListener
);
435 HintManager
.getInstance().hideAllHints();
436 myHighlighter
= null;
437 myHighlighterView
= null;
438 myStoredCursor
= null;
443 private class TooltipProvider
{
444 private final Editor myEditor
;
445 private final LogicalPosition myPosition
;
446 private BrowseMode myBrowseMode
;
447 private boolean myDisposed
;
449 public TooltipProvider(Editor editor
, LogicalPosition pos
) {
454 public void dispose() {
458 public BrowseMode
getBrowseMode() {
462 public void execute(BrowseMode browseMode
) {
463 myBrowseMode
= browseMode
;
465 ApplicationManager
.getApplication().executeOnPooledThread(new Runnable() {
467 ApplicationManager
.getApplication().runReadAction(new Runnable() {
476 private void doExecute() {
479 info
= getInfoAt(myEditor
, myPosition
, myBrowseMode
);
481 catch (IndexNotReadyException e
) {
482 showDumbModeNotification(myProject
);
485 if (info
== null) return;
487 SwingUtilities
.invokeLater(new Runnable() {
494 private void showHint(Info info
) {
495 if (myDisposed
) return;
496 Component internalComponent
= myEditor
.getContentComponent();
497 if (myHighlighter
!= null) {
498 if (!info
.isSimilarTo(myStoredInfo
)) {
499 disposeHighlighter();
501 // highlighter already set
502 internalComponent
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
507 if (info
.isValid(myEditor
.getDocument())) {
508 installLinkHighlighter(info
);
510 internalComponent
.addKeyListener(myEditorKeyListener
);
511 myEditor
.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener
);
512 myStoredCursor
= internalComponent
.getCursor();
514 internalComponent
.setCursor(Cursor
.getPredefinedCursor(Cursor
.HAND_CURSOR
));
515 myFileEditorManager
.addFileEditorManagerListener(myFileEditorManagerListener
);
517 String text
= info
.getInfo();
519 if (text
== null) return;
521 JLabel label
= HintUtil
.createInformationLabel(text
);
522 label
.setUI(new MultiLineLabelUI());
523 Font FONT
= UIUtil
.getLabelFont();
525 final LightweightHint hint
= new LightweightHint(label
);
526 final HintManagerImpl hintManager
= HintManagerImpl
.getInstanceImpl();
527 label
.addMouseMotionListener(new MouseMotionAdapter() {
528 public void mouseMoved(MouseEvent e
) {
529 hintManager
.hideAllHints();
532 Point p
= HintManagerImpl
.getHintPosition(hint
, myEditor
, myPosition
, HintManager
.ABOVE
);
533 hintManager
.showEditorHint(hint
, myEditor
, p
,
534 HintManager
.HIDE_BY_ANY_KEY
| HintManager
.HIDE_BY_TEXT_CHANGE
| HintManager
.HIDE_BY_SCROLLING
,
539 private void installLinkHighlighter(Info info
) {
540 int startOffset
= info
.myStartOffset
;
541 int endOffset
= info
.myEndOffset
;
543 myEditor
.getMarkupModel().addRangeHighlighter(startOffset
, endOffset
, HighlighterLayer
.SELECTION
+ 1,
544 ourReferenceAttributes
, HighlighterTargetArea
.EXACT_RANGE
);
545 myHighlighterView
= myEditor
;