d01d0cac39c05e47ecd7a1f057f713edb6304e7a
[idea/community.git] / plugins / coverage-common / src / com / intellij / coverage / CoverageLineMarkerRenderer.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.intellij.coverage;
18
19 import com.intellij.application.options.colors.ColorAndFontOptions;
20 import com.intellij.application.options.colors.ColorAndFontPanelFactory;
21 import com.intellij.application.options.colors.NewColorAndFontPanel;
22 import com.intellij.application.options.colors.SimpleEditorPreview;
23 import com.intellij.codeInsight.hint.EditorFragmentComponent;
24 import com.intellij.codeInsight.hint.HintManagerImpl;
25 import com.intellij.coverage.actions.HideCoverageInfoAction;
26 import com.intellij.coverage.actions.ShowCoveringTestsAction;
27 import com.intellij.icons.AllIcons;
28 import com.intellij.openapi.actionSystem.*;
29 import com.intellij.openapi.editor.Document;
30 import com.intellij.openapi.editor.Editor;
31 import com.intellij.openapi.editor.EditorFactory;
32 import com.intellij.openapi.editor.ScrollType;
33 import com.intellij.openapi.editor.colors.CodeInsightColors;
34 import com.intellij.openapi.editor.colors.EditorColors;
35 import com.intellij.openapi.editor.colors.TextAttributesKey;
36 import com.intellij.openapi.editor.ex.EditorEx;
37 import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
38 import com.intellij.openapi.editor.impl.EditorImpl;
39 import com.intellij.openapi.editor.markup.ActiveGutterRenderer;
40 import com.intellij.openapi.editor.markup.LineMarkerRendererEx;
41 import com.intellij.openapi.editor.markup.TextAttributes;
42 import com.intellij.openapi.options.Configurable;
43 import com.intellij.openapi.options.SearchableConfigurable;
44 import com.intellij.openapi.options.ShowSettingsUtil;
45 import com.intellij.openapi.options.colors.pages.GeneralColorsPage;
46 import com.intellij.openapi.project.Project;
47 import com.intellij.psi.PsiDocumentManager;
48 import com.intellij.psi.PsiFile;
49 import com.intellij.rt.coverage.data.LineCoverage;
50 import com.intellij.rt.coverage.data.LineData;
51 import com.intellij.ui.ColorUtil;
52 import com.intellij.ui.ColoredSideBorder;
53 import com.intellij.ui.HintHint;
54 import com.intellij.ui.LightweightHint;
55 import com.intellij.util.Function;
56 import com.intellij.util.ImageLoader;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import javax.swing.*;
61 import java.awt.*;
62 import java.awt.event.InputEvent;
63 import java.awt.event.KeyEvent;
64 import java.awt.event.MouseEvent;
65 import java.util.ArrayList;
66 import java.util.Collections;
67 import java.util.List;
68 import java.util.TreeMap;
69
70 /**
71  * @author ven
72  */
73 public class CoverageLineMarkerRenderer implements LineMarkerRendererEx, ActiveGutterRenderer {
74   private static final int THICKNESS = 8;
75   private final TextAttributesKey myKey;
76   private final String myClassName;
77   private final TreeMap<Integer, LineData> myLines;
78   private final boolean myCoverageByTestApplicable;
79   private final Function<Integer, Integer> myNewToOldConverter;
80   private final Function<Integer, Integer> myOldToNewConverter;
81   private final CoverageSuitesBundle myCoverageSuite;
82   private final boolean mySubCoverageActive;
83
84   protected CoverageLineMarkerRenderer(final TextAttributesKey textAttributesKey, @Nullable final String className, final TreeMap<Integer, LineData> lines,
85                              final boolean coverageByTestApplicable,
86                              final Function<Integer, Integer> newToOldConverter,
87                              final Function<Integer, Integer> oldToNewConverter,
88                              final CoverageSuitesBundle coverageSuite, boolean subCoverageActive) {
89     myKey = textAttributesKey;
90     myClassName = className;
91     myLines = lines;
92     myCoverageByTestApplicable = coverageByTestApplicable;
93     myNewToOldConverter = newToOldConverter;
94     myOldToNewConverter = oldToNewConverter;
95     myCoverageSuite = coverageSuite;
96     mySubCoverageActive = subCoverageActive;
97   }
98
99   public void paint(Editor editor, Graphics g, Rectangle r) {
100     final TextAttributes color = editor.getColorsScheme().getAttributes(myKey);
101     Color bgColor = color.getBackgroundColor();
102     if (bgColor == null) {
103       bgColor = color.getForegroundColor();
104     }
105     if (editor.getSettings().isLineNumbersShown() || ((EditorGutterComponentEx)editor.getGutter()).isAnnotationsShown()) {
106       if (bgColor != null) {
107         bgColor = ColorUtil.toAlpha(bgColor, 150);
108       }
109     }
110     if (bgColor != null) {
111       g.setColor(bgColor);
112     }
113     g.fillRect(r.x, r.y, r.width, r.height);
114     final LineData lineData = getLineData(editor.xyToLogicalPosition(new Point(0, r.y)).line);
115     if (lineData != null && lineData.isCoveredByOneTest()) {
116       g.drawImage( ImageLoader.loadFromResource("/gutter/unique.png"), r.x, r.y, 8, 8, editor.getComponent());
117     }
118   }
119
120   public static CoverageLineMarkerRenderer getRenderer(int lineNumber,
121                                                        @Nullable final String className,
122                                                        final TreeMap<Integer, LineData> lines,
123                                                        final boolean coverageByTestApplicable,
124                                                        @NotNull final CoverageSuitesBundle coverageSuite,
125                                                        final Function<Integer, Integer> newToOldConverter,
126                                                        final Function<Integer, Integer> oldToNewConverter, boolean subCoverageActive) {
127     return new CoverageLineMarkerRenderer(getAttributesKey(lineNumber, lines), className, lines, coverageByTestApplicable, newToOldConverter,
128                                           oldToNewConverter, coverageSuite, subCoverageActive);
129   }
130
131   public static TextAttributesKey getAttributesKey(final int lineNumber,
132                                                    final TreeMap<Integer, LineData> lines) {
133
134     return getAttributesKey(lines.get(lineNumber));
135   }
136
137   private static TextAttributesKey getAttributesKey(LineData lineData) {
138     if (lineData != null) {
139       switch (lineData.getStatus()) {
140         case LineCoverage.FULL:
141           return CodeInsightColors.LINE_FULL_COVERAGE;
142         case LineCoverage.PARTIAL:
143           return CodeInsightColors.LINE_PARTIAL_COVERAGE;
144       }
145     }
146
147     return CodeInsightColors.LINE_NONE_COVERAGE;
148   }
149
150   public boolean canDoAction(final MouseEvent e) {
151     Component component = e.getComponent();
152     if (component instanceof EditorGutterComponentEx) {
153       EditorGutterComponentEx gutter = (EditorGutterComponentEx)component;
154       return e.getX() > gutter.getLineMarkerAreaOffset() && e.getX() < gutter.getIconAreaOffset();
155     }
156     return false;
157   }
158
159   public void doAction(final Editor editor, final MouseEvent e) {
160     e.consume();
161     final JComponent comp = (JComponent)e.getComponent();
162     final JRootPane rootPane = comp.getRootPane();
163     final JLayeredPane layeredPane = rootPane.getLayeredPane();
164     final Point point = SwingUtilities.convertPoint(comp, THICKNESS, e.getY(), layeredPane);
165     showHint(editor, point, editor.xyToLogicalPosition(e.getPoint()).line);
166   }
167
168   private void showHint(final Editor editor, final Point point, final int lineNumber) {
169     final JPanel panel = new JPanel(new BorderLayout());
170     panel.add(createActionsToolbar(editor, lineNumber), BorderLayout.NORTH);
171
172     final LineData lineData = getLineData(lineNumber);
173     final EditorImpl uEditor;
174     if (lineData != null && lineData.getStatus() != LineCoverage.NONE && !mySubCoverageActive) {
175       final EditorFactory factory = EditorFactory.getInstance();
176       final Document doc = factory.createDocument(getReport(editor, lineNumber));
177       doc.setReadOnly(true);
178       uEditor = (EditorImpl)factory.createEditor(doc, editor.getProject());
179       panel.add(EditorFragmentComponent.createEditorFragmentComponent(uEditor, 0, doc.getLineCount(), false, false), BorderLayout.CENTER);
180     } else {
181       uEditor = null;
182     }
183
184
185     final LightweightHint hint = new LightweightHint(panel){
186       @Override
187       public void hide() {
188         if (uEditor != null) EditorFactory.getInstance().releaseEditor(uEditor);
189         super.hide();
190
191       }
192     };
193     HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point,
194         HintManagerImpl.HIDE_BY_ANY_KEY | HintManagerImpl.HIDE_BY_TEXT_CHANGE | HintManagerImpl.HIDE_BY_OTHER_HINT | HintManagerImpl.HIDE_BY_SCROLLING, -1, false, new HintHint(editor, point));
195   }
196
197   private String getReport(final Editor editor, final int lineNumber) {
198     final LineData lineData = getLineData(lineNumber);
199
200     final Document document = editor.getDocument();
201     final Project project = editor.getProject();
202     assert project != null;
203
204     final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
205     assert psiFile != null;
206     
207     final int lineStartOffset = document.getLineStartOffset(lineNumber);
208     final int lineEndOffset = document.getLineEndOffset(lineNumber);
209
210     return myCoverageSuite.getCoverageEngine().generateBriefReport(editor, psiFile, lineNumber, lineStartOffset, lineEndOffset, lineData);
211   }
212
213   protected JComponent createActionsToolbar(final Editor editor, final int lineNumber) {
214
215     final JComponent editorComponent = editor.getComponent();
216
217     final DefaultActionGroup group = new DefaultActionGroup();
218     final GotoPreviousCoveredLineAction prevAction = new GotoPreviousCoveredLineAction(editor, lineNumber);
219     final GotoNextCoveredLineAction nextAction = new GotoNextCoveredLineAction(editor, lineNumber);
220
221     group.add(prevAction);
222     group.add(nextAction);
223
224     prevAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_MASK|InputEvent.SHIFT_MASK)), editorComponent);
225     nextAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_MASK|InputEvent.SHIFT_MASK)), editorComponent);
226
227     final LineData lineData = getLineData(lineNumber);
228     if (myCoverageByTestApplicable) {
229       group.add(new ShowCoveringTestsAction(myClassName, lineData));
230     }
231     final AnAction byteCodeViewAction = ActionManager.getInstance().getAction("ByteCodeViewer");
232     if (byteCodeViewAction != null) {
233       group.add(byteCodeViewAction);
234     }
235     group.add(new EditCoverageColorsAction(editor, lineNumber));
236     group.add(new HideCoverageInfoAction());
237
238     final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.FILEHISTORY_VIEW_TOOLBAR, group, true);
239     final JComponent toolbarComponent = toolbar.getComponent();
240
241     final Color background = ((EditorEx)editor).getBackgroundColor();
242     final Color foreground = editor.getColorsScheme().getColor(EditorColors.CARET_COLOR);
243     toolbarComponent.setBackground(background);
244     toolbarComponent.setBorder(new ColoredSideBorder(foreground, foreground, lineData == null || lineData.getStatus() == LineCoverage.NONE || mySubCoverageActive ? foreground : null, foreground, 1));
245     toolbar.updateActionsImmediately();
246     return toolbarComponent;
247   }
248
249   public void moveToLine(final int lineNumber, final Editor editor) {
250     final int firstOffset = editor.getDocument().getLineStartOffset(lineNumber);
251     editor.getCaretModel().moveToOffset(firstOffset);
252     editor.getScrollingModel().scrollToCaret(ScrollType.CENTER);
253
254     editor.getScrollingModel().runActionOnScrollingFinished(() -> {
255       Point p = editor.visualPositionToXY(editor.offsetToVisualPosition(firstOffset));
256       EditorGutterComponentEx editorComponent = (EditorGutterComponentEx)editor.getGutter();
257       JLayeredPane layeredPane = editorComponent.getRootPane().getLayeredPane();
258       p = SwingUtilities.convertPoint(editorComponent, THICKNESS, p.y, layeredPane);
259       showHint(editor, p, lineNumber);
260     });
261   }
262
263   @Nullable
264   public LineData getLineData(int lineNumber) {
265     return myLines != null ? myLines.get(myNewToOldConverter != null ? myNewToOldConverter.fun(lineNumber).intValue() : lineNumber) : null;
266   }
267
268   public Color getErrorStripeColor(final Editor editor) {
269     return editor.getColorsScheme().getAttributes(myKey).getErrorStripeColor();
270   }
271
272   @Override
273   public Position getPosition() {
274     return Position.LEFT;
275   }
276
277   private class GotoPreviousCoveredLineAction extends BaseGotoCoveredLineAction {
278
279     public GotoPreviousCoveredLineAction(final Editor editor, final int lineNumber) {
280       super(editor, lineNumber);
281       copyFrom(ActionManager.getInstance().getAction(IdeActions.ACTION_PREVIOUS_OCCURENCE));
282       getTemplatePresentation().setText("Previous Coverage Mark");
283     }
284
285     protected boolean hasNext(final int idx, final List<Integer> list) {
286       return idx > 0;
287     }
288
289     protected int next(final int idx) {
290       return idx - 1;
291     }
292
293     @Override
294     public void update(AnActionEvent e) {
295       super.update(e);
296       final String nextChange = getNextChange();
297       if (nextChange != null) {
298         e.getPresentation().setText("Previous " + nextChange);
299       }
300     }
301   }
302
303   private class GotoNextCoveredLineAction extends BaseGotoCoveredLineAction {
304
305     public GotoNextCoveredLineAction(final Editor editor, final int lineNumber) {
306       super(editor, lineNumber);
307       copyFrom(ActionManager.getInstance().getAction(IdeActions.ACTION_NEXT_OCCURENCE));
308       getTemplatePresentation().setText("Next Coverage Mark");
309     }
310
311     protected boolean hasNext(final int idx, final List<Integer> list) {
312       return idx < list.size() - 1;
313     }
314
315     protected int next(final int idx) {
316       return idx + 1;
317     }
318
319     @Override
320     public void update(AnActionEvent e) {
321       super.update(e);
322       final String nextChange = getNextChange();
323       if (nextChange != null) {
324         e.getPresentation().setText("Next " + nextChange);
325       }
326     }
327   }
328
329   private abstract class BaseGotoCoveredLineAction extends AnAction {
330     private final Editor myEditor;
331     private final int myLineNumber;
332
333     public BaseGotoCoveredLineAction(final Editor editor, final int lineNumber) {
334       myEditor = editor;
335       myLineNumber = lineNumber;
336     }
337
338     public void actionPerformed(final AnActionEvent e) {
339       final Integer lineNumber = getLineEntry();
340       if (lineNumber != null) {
341         moveToLine(lineNumber.intValue(), myEditor);
342       }
343     }
344
345     protected abstract boolean hasNext(int idx, List<Integer> list);
346     protected abstract int next(int idx);
347
348     @Nullable
349     private Integer getLineEntry() {
350       final ArrayList<Integer> list = new ArrayList<Integer>(myLines.keySet());
351       Collections.sort(list);
352       final LineData data = getLineData(myLineNumber);
353       final int currentStatus = data != null ? data.getStatus() : LineCoverage.NONE;
354       int idx = list.indexOf(myNewToOldConverter != null ? myNewToOldConverter.fun(myLineNumber).intValue() : myLineNumber);
355       while (hasNext(idx, list)) {
356         final int index = next(idx);
357         final LineData lineData = myLines.get(list.get(index));
358         idx = index;
359         if (lineData != null && lineData.getStatus() != currentStatus) {
360           final Integer line = list.get(idx);
361           if (myOldToNewConverter != null) {
362             final int newLine = myOldToNewConverter.fun(line).intValue();
363             if (newLine != 0) return newLine;
364           } else {
365             return line;
366           }
367         }
368       }
369       return null;
370     }
371
372     @Nullable
373     protected String getNextChange() {
374       Integer entry = getLineEntry();
375       if (entry != null) {
376         final LineData lineData = getLineData(entry);
377         if (lineData != null) {
378           switch (lineData.getStatus()) {
379             case LineCoverage.NONE: 
380               return "Uncovered";
381             case LineCoverage.PARTIAL:
382               return "Partial Covered";
383             case LineCoverage.FULL:
384               return "Fully Covered";
385           }
386         }
387       }
388       return null;
389     }
390
391     @Override
392     public void update(final AnActionEvent e) {
393       e.getPresentation().setEnabled(getLineEntry() != null);
394     }
395   }
396
397   private class EditCoverageColorsAction extends AnAction {
398     private final Editor myEditor;
399     private final int myLineNumber;
400
401     private EditCoverageColorsAction(Editor editor, int lineNumber) {
402       super("Edit coverage colors", "Edit coverage colors", AllIcons.General.EditColors);
403       myEditor = editor;
404       myLineNumber = lineNumber;
405     }
406
407     @Override
408     public void update(AnActionEvent e) {
409       e.getPresentation().setVisible(getLineData(myLineNumber) != null);
410     }
411
412     @Override
413     public void actionPerformed(AnActionEvent e) {
414       final ColorAndFontOptions colorAndFontOptions = new ColorAndFontOptions(){
415         @Override
416         protected List<ColorAndFontPanelFactory> createPanelFactories() {
417           final GeneralColorsPage colorsPage = new GeneralColorsPage();
418           final ColorAndFontPanelFactory panelFactory = new ColorAndFontPanelFactory() {
419             @NotNull
420             @Override
421             public NewColorAndFontPanel createPanel(@NotNull ColorAndFontOptions options) {
422               final SimpleEditorPreview preview = new SimpleEditorPreview(options, colorsPage);
423               return NewColorAndFontPanel.create(preview, colorsPage.getDisplayName(), options, null, colorsPage);
424             }
425
426             @NotNull
427             @Override
428             public String getPanelDisplayName() {
429               return "Editor | " + getDisplayName() + " | " + colorsPage.getDisplayName();
430             }
431           };
432           return Collections.singletonList(panelFactory);
433         }
434       };
435       final Configurable[] configurables = colorAndFontOptions.buildConfigurables();
436       try {
437         final SearchableConfigurable general = colorAndFontOptions.findSubConfigurable(GeneralColorsPage.class);
438         if (general != null) {
439           final LineData lineData = getLineData(myLineNumber);
440           ShowSettingsUtil.getInstance().editConfigurable(myEditor.getProject(), general,
441                                                           general.enableSearch(getAttributesKey(lineData).getExternalName()));
442         }
443       }
444       finally {
445         for (Configurable configurable : configurables) {
446           configurable.disposeUIResources();
447         }
448         colorAndFontOptions.disposeUIResources();
449       }
450     }
451   }
452 }