2 * Copyright 2000-2015 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.coverage;
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;
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;
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;
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;
92 myCoverageByTestApplicable = coverageByTestApplicable;
93 myNewToOldConverter = newToOldConverter;
94 myOldToNewConverter = oldToNewConverter;
95 myCoverageSuite = coverageSuite;
96 mySubCoverageActive = subCoverageActive;
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();
105 if (editor.getSettings().isLineNumbersShown() || ((EditorGutterComponentEx)editor.getGutter()).isAnnotationsShown()) {
106 if (bgColor != null) {
107 bgColor = ColorUtil.toAlpha(bgColor, 150);
110 if (bgColor != null) {
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());
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);
131 public static TextAttributesKey getAttributesKey(final int lineNumber,
132 final TreeMap<Integer, LineData> lines) {
134 return getAttributesKey(lines.get(lineNumber));
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;
147 return CodeInsightColors.LINE_NONE_COVERAGE;
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();
159 public void doAction(final Editor editor, final MouseEvent e) {
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);
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);
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);
185 final LightweightHint hint = new LightweightHint(panel){
188 if (uEditor != null) EditorFactory.getInstance().releaseEditor(uEditor);
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));
197 private String getReport(final Editor editor, final int lineNumber) {
198 final LineData lineData = getLineData(lineNumber);
200 final Document document = editor.getDocument();
201 final Project project = editor.getProject();
202 assert project != null;
204 final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
205 assert psiFile != null;
207 final int lineStartOffset = document.getLineStartOffset(lineNumber);
208 final int lineEndOffset = document.getLineEndOffset(lineNumber);
210 return myCoverageSuite.getCoverageEngine().generateBriefReport(editor, psiFile, lineNumber, lineStartOffset, lineEndOffset, lineData);
213 protected JComponent createActionsToolbar(final Editor editor, final int lineNumber) {
215 final JComponent editorComponent = editor.getComponent();
217 final DefaultActionGroup group = new DefaultActionGroup();
218 final GotoPreviousCoveredLineAction prevAction = new GotoPreviousCoveredLineAction(editor, lineNumber);
219 final GotoNextCoveredLineAction nextAction = new GotoNextCoveredLineAction(editor, lineNumber);
221 group.add(prevAction);
222 group.add(nextAction);
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);
227 final LineData lineData = getLineData(lineNumber);
228 if (myCoverageByTestApplicable) {
229 group.add(new ShowCoveringTestsAction(myClassName, lineData));
231 final AnAction byteCodeViewAction = ActionManager.getInstance().getAction("ByteCodeViewer");
232 if (byteCodeViewAction != null) {
233 group.add(byteCodeViewAction);
235 group.add(new EditCoverageColorsAction(editor, lineNumber));
236 group.add(new HideCoverageInfoAction());
238 final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.FILEHISTORY_VIEW_TOOLBAR, group, true);
239 final JComponent toolbarComponent = toolbar.getComponent();
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;
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);
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);
264 public LineData getLineData(int lineNumber) {
265 return myLines != null ? myLines.get(myNewToOldConverter != null ? myNewToOldConverter.fun(lineNumber).intValue() : lineNumber) : null;
268 public Color getErrorStripeColor(final Editor editor) {
269 return editor.getColorsScheme().getAttributes(myKey).getErrorStripeColor();
273 public Position getPosition() {
274 return Position.LEFT;
277 private class GotoPreviousCoveredLineAction extends BaseGotoCoveredLineAction {
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");
285 protected boolean hasNext(final int idx, final List<Integer> list) {
289 protected int next(final int idx) {
294 public void update(AnActionEvent e) {
296 final String nextChange = getNextChange();
297 if (nextChange != null) {
298 e.getPresentation().setText("Previous " + nextChange);
303 private class GotoNextCoveredLineAction extends BaseGotoCoveredLineAction {
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");
311 protected boolean hasNext(final int idx, final List<Integer> list) {
312 return idx < list.size() - 1;
315 protected int next(final int idx) {
320 public void update(AnActionEvent e) {
322 final String nextChange = getNextChange();
323 if (nextChange != null) {
324 e.getPresentation().setText("Next " + nextChange);
329 private abstract class BaseGotoCoveredLineAction extends AnAction {
330 private final Editor myEditor;
331 private final int myLineNumber;
333 public BaseGotoCoveredLineAction(final Editor editor, final int lineNumber) {
335 myLineNumber = lineNumber;
338 public void actionPerformed(final AnActionEvent e) {
339 final Integer lineNumber = getLineEntry();
340 if (lineNumber != null) {
341 moveToLine(lineNumber.intValue(), myEditor);
345 protected abstract boolean hasNext(int idx, List<Integer> list);
346 protected abstract int next(int idx);
349 private Integer getLineEntry() {
350 final ArrayList<Integer> list = new ArrayList<>(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));
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;
373 protected String getNextChange() {
374 Integer entry = getLineEntry();
376 final LineData lineData = getLineData(entry);
377 if (lineData != null) {
378 switch (lineData.getStatus()) {
379 case LineCoverage.NONE:
381 case LineCoverage.PARTIAL:
382 return "Partial Covered";
383 case LineCoverage.FULL:
384 return "Fully Covered";
392 public void update(final AnActionEvent e) {
393 e.getPresentation().setEnabled(getLineEntry() != null);
397 private class EditCoverageColorsAction extends AnAction {
398 private final Editor myEditor;
399 private final int myLineNumber;
401 private EditCoverageColorsAction(Editor editor, int lineNumber) {
402 super("Edit coverage colors", "Edit coverage colors", AllIcons.General.EditColors);
404 myLineNumber = lineNumber;
408 public void update(AnActionEvent e) {
409 e.getPresentation().setVisible(getLineData(myLineNumber) != null);
413 public void actionPerformed(AnActionEvent e) {
414 final ColorAndFontOptions colorAndFontOptions = new ColorAndFontOptions(){
416 protected List<ColorAndFontPanelFactory> createPanelFactories() {
417 final GeneralColorsPage colorsPage = new GeneralColorsPage();
418 final ColorAndFontPanelFactory panelFactory = new ColorAndFontPanelFactory() {
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);
428 public String getPanelDisplayName() {
429 return "Editor | " + getDisplayName() + " | " + colorsPage.getDisplayName();
432 return Collections.singletonList(panelFactory);
435 final Configurable[] configurables = colorAndFontOptions.buildConfigurables();
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()));
445 for (Configurable configurable : configurables) {
446 configurable.disposeUIResources();
448 colorAndFontOptions.disposeUIResources();