f312f8105d0c5e243b6411e08fc986a013e9c585
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / impl / TrafficLightRenderer.java
1 /*
2  * Copyright 2000-2009 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.codeInsight.daemon.impl;
18
19 import com.intellij.codeHighlighting.HighlightDisplayLevel;
20 import com.intellij.codeHighlighting.TextEditorHighlightingPass;
21 import com.intellij.codeInsight.daemon.DaemonBundle;
22 import com.intellij.codeInsight.daemon.impl.analysis.HighlightLevelUtil;
23 import com.intellij.ide.PowerSaveMode;
24 import com.intellij.lang.annotation.HighlightSeverity;
25 import com.intellij.openapi.editor.Document;
26 import com.intellij.openapi.editor.markup.ErrorStripeRenderer;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.util.IconLoader;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.psi.PsiFile;
31 import com.intellij.ui.LayeredIcon;
32 import com.intellij.util.ArrayUtil;
33 import com.intellij.util.Processor;
34 import com.intellij.util.ui.EmptyIcon;
35 import com.intellij.util.ui.UIUtil;
36 import gnu.trove.TIntArrayList;
37 import org.jetbrains.annotations.NonNls;
38 import org.jetbrains.annotations.Nullable;
39
40 import javax.swing.*;
41 import java.awt.*;
42 import java.net.URL;
43 import java.util.ArrayList;
44 import java.util.List;
45
46 public class TrafficLightRenderer implements ErrorStripeRenderer {
47   private static final Icon IN_PROGRESS_ICON = IconLoader.getIcon("/general/errorsInProgress.png");
48   private static final Icon NO_ANALYSIS_ICON = IconLoader.getIcon("/general/noAnalysis.png");
49   private static final Icon NO_ICON = new EmptyIcon(IN_PROGRESS_ICON.getIconWidth(), IN_PROGRESS_ICON.getIconHeight());
50   private static final Icon STARING_EYE_ICON = IconLoader.getIcon("/general/inspectionInProgress.png");
51
52   private final Project myProject;
53   private final Document myDocument;
54   private final PsiFile myFile;
55   private final DaemonCodeAnalyzerImpl myDaemonCodeAnalyzer;
56
57   @NonNls private static final String HTML_HEADER = "<html><body>";
58   @NonNls private static final String HTML_FOOTER = "</body></html>";
59   @NonNls private static final String BR = "<br>";
60   @NonNls private static final String NO_PASS_FOR_MESSAGE_KEY_SUFFIX = ".for";
61   private final SeverityRegistrar mySeverityRegistrar;
62
63   public TrafficLightRenderer(Project project, DaemonCodeAnalyzerImpl highlighter, Document document, PsiFile file) {
64     myProject = project;
65     myDaemonCodeAnalyzer = highlighter;
66     myDocument = document;
67     myFile = file;
68     mySeverityRegistrar = SeverityRegistrar.getInstance(myProject);
69   }
70
71   public static class DaemonCodeAnalyzerStatus {
72     public boolean errorAnalyzingFinished; // all passes done
73     public final List<ProgressableTextEditorHighlightingPass> passStati = new ArrayList<ProgressableTextEditorHighlightingPass>();
74     public String[/*rootsNumber*/] noHighlightingRoots;
75     public String[/*rootsNumber*/] noInspectionRoots;
76     public int[] errorCount = ArrayUtil.EMPTY_INT_ARRAY;
77
78     public int rootsNumber;
79     public String toString() {
80       @NonNls String s = "DS: finished=" + errorAnalyzingFinished;
81       s += "; pass statuses: " + passStati.size() + "; ";
82       for (ProgressableTextEditorHighlightingPass passStatus : passStati) {
83         s += String.format("(%s %2.0f%% %b)", passStatus.getPresentableName(), passStatus.getProgress() *100, passStatus.isFinished());
84       }
85       s += "; error count: "+errorCount.length + ": "+new TIntArrayList(errorCount);
86       return s;
87     }
88   }
89
90   @Nullable
91   protected DaemonCodeAnalyzerStatus getDaemonCodeAnalyzerStatus(boolean fillErrorsCount, SeverityRegistrar severityRegistrar) {
92     if (myFile == null || myProject.isDisposed() || !myDaemonCodeAnalyzer.isHighlightingAvailable(myFile)) return null;
93
94     List<String> noInspectionRoots = new ArrayList<String>();
95     List<String> noHighlightingRoots = new ArrayList<String>();
96     final PsiFile[] roots = myFile.getPsiRoots();
97     for (PsiFile file : roots) {
98       if (!HighlightLevelUtil.shouldHighlight(file)) {
99         noHighlightingRoots.add(file.getLanguage().getID());
100       }
101       else if (!HighlightLevelUtil.shouldInspect(file)) {
102         noInspectionRoots.add(file.getLanguage().getID());
103       }
104     }
105     DaemonCodeAnalyzerStatus status = new DaemonCodeAnalyzerStatus();
106     status.noInspectionRoots = noInspectionRoots.isEmpty() ? null : ArrayUtil.toStringArray(noInspectionRoots);
107     status.noHighlightingRoots = noHighlightingRoots.isEmpty() ? null : ArrayUtil.toStringArray(noHighlightingRoots);
108
109     status.errorCount = new int[severityRegistrar.getSeveritiesCount()];
110     status.rootsNumber = roots.length;
111     fillDaemonCodeAnalyzerErrorsStatus(status, fillErrorsCount, severityRegistrar);
112     List<TextEditorHighlightingPass> passes = myDaemonCodeAnalyzer.getPassesToShowProgressFor(myDocument);
113     for (TextEditorHighlightingPass tepass : passes) {
114       if (!(tepass instanceof ProgressableTextEditorHighlightingPass)) continue;
115       ProgressableTextEditorHighlightingPass pass = (ProgressableTextEditorHighlightingPass)tepass;
116
117       if (pass.getProgress() < 0) continue;
118       status.passStati.add(pass);
119     }
120     status.errorAnalyzingFinished = myDaemonCodeAnalyzer.isAllAnalysisFinished(myFile);
121
122     return status;
123   }
124
125   protected void fillDaemonCodeAnalyzerErrorsStatus(final DaemonCodeAnalyzerStatus status,
126                                                     final boolean fillErrorsCount,
127                                                     final SeverityRegistrar severityRegistrar) {
128     final int count = severityRegistrar.getSeveritiesCount() - 1;
129     final HighlightSeverity maxPossibleSeverity = severityRegistrar.getSeverityByIndex(count);
130     final HighlightSeverity[] maxFoundSeverity = {null};
131
132     DaemonCodeAnalyzerImpl.processHighlights(myDocument, myProject, null, 0, myDocument.getTextLength(), new Processor<HighlightInfo>() {
133       public boolean process(HighlightInfo info) {
134         HighlightSeverity infoSeverity = info.getSeverity();
135         if (fillErrorsCount) {
136           final int severityIdx = severityRegistrar.getSeverityIdx(infoSeverity);
137           if (severityIdx != -1) {
138             status.errorCount[severityIdx] ++;
139           }
140         }
141         else {
142           if (maxFoundSeverity[0] == null || severityRegistrar.compare(maxFoundSeverity[0], infoSeverity) < 0) {
143             maxFoundSeverity[0] = infoSeverity;
144           }
145           if (infoSeverity == maxPossibleSeverity) {
146             return false;
147           }
148         }
149         return true;
150       }
151     });
152     if (maxFoundSeverity[0] != null) {
153       final int severityIdx = severityRegistrar.getSeverityIdx(maxFoundSeverity[0]);
154       if (severityIdx != -1) {
155         status.errorCount[severityIdx] = 1;
156       }
157     }
158   }
159
160   public final Project getProject() {
161     return myProject;
162   }
163
164   public String getTooltipMessage() {
165     DaemonCodeAnalyzerStatus status = getDaemonCodeAnalyzerStatus(true, mySeverityRegistrar);
166
167     if (status == null) return null;
168     @NonNls String text = HTML_HEADER;
169     if (PowerSaveMode.isEnabled()) {
170       text += "Code analysis is disabled in power save mode.";
171       text += HTML_FOOTER;
172       return text;
173     }
174     if (status.noHighlightingRoots != null && status.noHighlightingRoots.length == status.rootsNumber) {
175       text += DaemonBundle.message("analysis.hasnot.been.run");
176       text += HTML_FOOTER;
177       return text;
178     }
179
180     if (status.errorAnalyzingFinished) {
181       text += DaemonBundle.message("analysis.completed");
182     }
183     else {
184       text += DaemonBundle.message("performing.code.analysis");
185       text += "<table>";
186       for (ProgressableTextEditorHighlightingPass passStatus : status.passStati) {
187         if (passStatus.isFinished()) continue;
188         text += "<tr><td>" + passStatus.getPresentableName() + ":</td><td>" + renderProgressHtml(passStatus.getProgress()) +  "</td></tr>";
189       }
190       text += "</table>";
191     }
192
193     int currentSeverityErrors = 0;
194     for (int i = status.errorCount.length - 1; i >= 0; i--) {
195       if (status.errorCount[i] > 0) {
196         final HighlightSeverity severity = mySeverityRegistrar.getSeverityByIndex(i);
197         text += BR;
198         String name = status.errorCount[i] > 1 ? StringUtil.pluralize(severity.toString().toLowerCase()) : severity.toString().toLowerCase();
199         text += status.errorAnalyzingFinished
200                 ? DaemonBundle.message("errors.found", status.errorCount[i], name)
201                 : DaemonBundle.message("errors.found.so.far", status.errorCount[i], name);
202         currentSeverityErrors += status.errorCount[i];
203       }
204     }
205     if (currentSeverityErrors == 0) {
206       text += BR;
207       text += status.errorAnalyzingFinished
208               ? DaemonBundle.message("no.errors.or.warnings.found")
209               : DaemonBundle.message("no.errors.or.warnings.found.so.far");
210     }
211
212     text += getMessageByRoots(status.noHighlightingRoots, status.rootsNumber, "no.syntax.highlighting.performed");
213     text += getMessageByRoots(status.noInspectionRoots, status.rootsNumber, "no.inspections.performed");
214     text += HTML_FOOTER;
215
216     text = UIUtil.convertSpace2Nbsp(text);
217
218     return text;
219   }
220
221   private static final URL progressUrl = TrafficLightRenderer.class.getClassLoader().getResource("/general/progress.png");
222   private static final URL progressPlaceHolderUrl = TrafficLightRenderer.class.getClassLoader().getResource("/general/progressTransparentPlaceHolder.png");
223
224   private static String renderProgressHtml(double progress) {
225     @NonNls String text = "<table><tr><td>";
226     int nBricks = 5;
227     int nFilledBricks = (int)(nBricks * progress);
228     int i;
229     for (i = 0; i < nFilledBricks; i++) {
230       text += "<img src=\"" + progressUrl + "\">";
231     }
232     for (; i < nBricks; i++) {
233       text += "<img src=\"" + progressPlaceHolderUrl + "\">";
234     }
235     text += "&nbsp;"+String.format("%2.0f%%", progress * 100);
236     text += "</td></tr></table>";
237     return text;
238   }
239
240   private static String getMessageByRoots(String [] roots, int rootsNumber, @NonNls String prefix){
241     if (roots != null && roots.length > 0) {
242       return BR + (rootsNumber > 1
243                    ? DaemonBundle.message(prefix + NO_PASS_FOR_MESSAGE_KEY_SUFFIX, StringUtil.join(roots, ", "))
244                    : DaemonBundle.message(prefix));
245     }
246     return "";
247   }
248
249   public void paint(Component c, Graphics g, Rectangle r) {
250     Icon icon = getIcon();
251
252     int height = icon.getIconHeight();
253     int width = icon.getIconWidth();
254     icon.paintIcon(c, g, r.x + (r.width - width) / 2, r.y + (r.height - height) / 2);
255   }
256
257   private Icon getIcon() {
258     DaemonCodeAnalyzerStatus status = getDaemonCodeAnalyzerStatus(false, mySeverityRegistrar);
259
260     if (status == null) {
261       return NO_ICON;
262     }
263     if (status.noHighlightingRoots != null && status.noHighlightingRoots.length == status.rootsNumber) {
264       return NO_ANALYSIS_ICON;
265     }
266
267     Icon icon = HighlightDisplayLevel.DO_NOT_SHOW.getIcon();
268     for (int i = status.errorCount.length - 1; i >= 0; i--) {
269       if (status.errorCount[i] != 0) {
270         icon = mySeverityRegistrar.getRendererIconByIndex(i);
271         break;
272       }
273     }
274
275     if (status.errorAnalyzingFinished) return icon;
276     double progress = getOverallProgress(status);
277     TruncatingIcon trunc = new TruncatingIcon(icon, (int)(icon.getIconWidth() * progress), (int)(icon.getIconHeight() * progress));
278     return LayeredIcon.create(trunc, STARING_EYE_ICON);
279   }
280
281   private static double getOverallProgress(DaemonCodeAnalyzerStatus status) {
282     long advancement = 0;
283     long limit = 0;
284     for (ProgressableTextEditorHighlightingPass ps : status.passStati) {
285       advancement += ps.getProgressCount();
286       limit += ps.getProgressLimit();
287     }
288     return limit == 0 ? status.errorAnalyzingFinished ? 1 : 0 : advancement * 1.0 / limit;
289   }
290 }