optimization: reduce number od CASes
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / impl / UpdateHighlightersUtil.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.codeInsight.daemon.LineMarkerInfo;
20 import com.intellij.codeInsight.intention.impl.FileLevelIntentionComponent;
21 import com.intellij.lang.annotation.HighlightSeverity;
22 import com.intellij.lang.injection.InjectedLanguageManager;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.editor.Document;
25 import com.intellij.openapi.editor.Editor;
26 import com.intellij.openapi.editor.EditorFactory;
27 import com.intellij.openapi.editor.RangeMarker;
28 import com.intellij.openapi.editor.event.DocumentEvent;
29 import com.intellij.openapi.editor.ex.DocumentEx;
30 import com.intellij.openapi.editor.ex.EditorEx;
31 import com.intellij.openapi.editor.ex.MarkupModelEx;
32 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
33 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
34 import com.intellij.openapi.editor.impl.RangeMarkerTree;
35 import com.intellij.openapi.editor.impl.RedBlackTree;
36 import com.intellij.openapi.editor.markup.*;
37 import com.intellij.openapi.fileEditor.FileEditor;
38 import com.intellij.openapi.fileEditor.FileEditorManager;
39 import com.intellij.openapi.fileEditor.TextEditor;
40 import com.intellij.openapi.project.Project;
41 import com.intellij.openapi.util.Comparing;
42 import com.intellij.openapi.util.Key;
43 import com.intellij.openapi.util.Pair;
44 import com.intellij.openapi.util.TextRange;
45 import com.intellij.openapi.vfs.VirtualFile;
46 import com.intellij.psi.PsiDocumentManager;
47 import com.intellij.psi.PsiElement;
48 import com.intellij.psi.PsiFile;
49 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
50 import com.intellij.util.Processor;
51 import com.intellij.util.SmartList;
52 import com.intellij.util.containers.ContainerUtil;
53 import com.intellij.util.containers.MultiMap;
54 import gnu.trove.THashMap;
55 import org.jetbrains.annotations.NotNull;
56 import org.jetbrains.annotations.Nullable;
57 import org.jetbrains.annotations.TestOnly;
58
59 import java.awt.*;
60 import java.util.*;
61 import java.util.List;
62 import java.util.concurrent.CopyOnWriteArrayList;
63
64 public class UpdateHighlightersUtil {
65   private static final Comparator<HighlightInfo> BY_START_OFFSET_NODUPS = new Comparator<HighlightInfo>() {
66     public int compare(HighlightInfo o1, HighlightInfo o2) {
67       int d = o1.getActualStartOffset() - o2.getActualStartOffset();
68       if (d != 0) return d;
69       d = o1.getActualEndOffset() - o2.getActualEndOffset();
70       if (d != 0) return d;
71
72       d = Comparing.compare(o1.getSeverity(), o2.getSeverity());
73       if (d != 0) return -d; // higher severity first, to prevent warnings overlap errors
74
75       if (!Comparing.equal(o1.type, o2.type)) {
76         return String.valueOf(o1.type).compareTo(String.valueOf(o2.type));
77       }
78
79       if (!Comparing.equal(o1.getGutterIconRenderer(), o2.getGutterIconRenderer())) {
80         return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
81       }
82
83       if (!Comparing.equal(o1.forcedTextAttributes, o2.forcedTextAttributes)) {
84         return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
85       }
86
87       return Comparing.compare(o1.description, o2.description);
88     }
89   };
90
91   private static final Key<List<HighlightInfo>> FILE_LEVEL_HIGHLIGHTS = Key.create("FILE_LEVEL_HIGHLIGHTS");
92   private static final Comparator<TextRange> BY_START_OFFSET = new Comparator<TextRange>() {
93     public int compare(final TextRange o1, final TextRange o2) {
94       return o1.getStartOffset() - o2.getStartOffset();
95     }
96   };
97   private static final Comparator<TextRange> BY_START_OFFSET_OR_CONTAINS = new Comparator<TextRange>() {
98     public int compare(final TextRange o1, final TextRange o2) {
99       if (o1.contains(o2) || o2.contains(o1)) return 0;
100       return o1.getStartOffset() - o2.getStartOffset();
101     }
102   };
103
104   private static void cleanFileLevelHighlights(@NotNull Project project, final int group, PsiFile psiFile) {
105     if (psiFile == null || !psiFile.getViewProvider().isPhysical()) return;
106     VirtualFile vFile = psiFile.getViewProvider().getVirtualFile();
107     final FileEditorManager manager = FileEditorManager.getInstance(project);
108     for (FileEditor fileEditor : manager.getEditors(vFile)) {
109       final List<HighlightInfo> infos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS);
110       if (infos == null) continue;
111       List<HighlightInfo> infosToRemove = new ArrayList<HighlightInfo>();
112       for (HighlightInfo info : infos) {
113         if (info.group == group) {
114           manager.removeTopComponent(fileEditor, info.fileLevelComponent);
115           infosToRemove.add(info);
116         }
117       }
118       infos.removeAll(infosToRemove);
119     }
120   }
121
122   public static void setHighlightersToEditor(@NotNull Project project,
123                                              @NotNull Document document,
124                                              int startOffset,
125                                              int endOffset,
126                                              @NotNull Collection<HighlightInfo> highlights,
127                                              int group) {
128     setHighlightersToEditor(project, document, Collections.singletonMap(new TextRange(startOffset, endOffset), highlights), group);
129   }
130
131   static boolean hasInfo(Collection<HighlightInfo> infos, int start, int end, String desc) {
132     if (infos == null) return false;
133     for (HighlightInfo info : infos) {
134       if (info.startOffset == start && info.endOffset == end && info.description.equals(desc)) return true;
135     }
136     return false;
137   }
138
139   private static class HighlightersRecycler {
140     private final MultiMap<TextRange, RangeHighlighter> incinerator = new MultiMap<TextRange, RangeHighlighter>(){
141       @Override
142       protected Map<TextRange, Collection<RangeHighlighter>> createMap() {
143         return new THashMap<TextRange, Collection<RangeHighlighter>>();
144       }
145
146       @Override
147       protected Collection<RangeHighlighter> createCollection() {
148         return new SmartList<RangeHighlighter>();
149       }
150     };
151
152     void recycleHighlighter(RangeHighlighter highlighter) {
153       incinerator.putValue(InjectedLanguageUtil.toTextRange(highlighter), highlighter);
154     }
155
156     RangeHighlighter pickupHighlighterFromGarbageBin(int startOffset, int endOffset, int layer){
157       TextRange range = new TextRange(startOffset, endOffset);
158       Collection<RangeHighlighter> collection = incinerator.get(range);
159       for (RangeHighlighter highlighter : collection) {
160         if (highlighter.isValid() && highlighter.getLayer() == layer) {
161           incinerator.removeValue(range, highlighter);
162           return highlighter;
163         }
164       }
165       return null;
166     }
167     Collection<? extends RangeHighlighter> forAllInGarbageBin() {
168       return incinerator.values();
169     }
170   }
171
172   public static void addHighlighterToEditorIncrementally(@NotNull Project project,
173                                                          @NotNull Document document,
174                                                          @NotNull PsiFile file,
175                                                          int startOffset,
176                                                          int endOffset,
177                                                          @NotNull final HighlightInfo info,
178                                                          final int group) {
179     ApplicationManager.getApplication().assertIsDispatchThread();
180     if (info.isFileLevelAnnotation) return;
181
182     MarkupModel markup = document.getMarkupModel(project);
183     Processor<HighlightInfo> otherHighlightInTheWayProcessor = new Processor<HighlightInfo>() {
184       public boolean process(HighlightInfo oldInfo) {
185         return oldInfo.group != group || !oldInfo.equalsByActualOffset(info);
186       }
187     };
188     if (!DaemonCodeAnalyzerImpl.processHighlights(document, project,
189                                                   null, info.getActualStartOffset(), info.getActualEndOffset(),
190                                                   otherHighlightInTheWayProcessor)) {
191       return;
192     }
193
194     boolean success = createOrReuseHighlighterFor(info, document, group, file, (MarkupModelEx)markup, null, null, startOffset, endOffset,
195                                                   SeverityRegistrar.getInstance(project));
196     if (!success) {
197       return;
198     }
199
200     DaemonCodeAnalyzerImpl.addHighlight(markup, project, info);
201     clearWhiteSpaceOptimizationFlag(document);
202     assertMarkupConsistent(markup, project);
203   }
204
205   public static void setHighlightersToEditor(@NotNull Project project,
206                                              @NotNull Document document,
207                                              @NotNull Map<TextRange, Collection<HighlightInfo>> infos,
208                                              final int group) {
209     ApplicationManager.getApplication().assertIsDispatchThread();
210
211     PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
212     cleanFileLevelHighlights(project, group, psiFile);
213
214     final List<TextRange> ranges = new ArrayList<TextRange>(infos.keySet());
215     Collections.sort(ranges, BY_START_OFFSET);
216     //merge intersecting
217     for (int i = 1; i < ranges.size(); i++) {
218       TextRange range = ranges.get(i);
219       TextRange prev = ranges.get(i-1);
220       if (prev.intersects(range)) {
221         ranges.remove(i);
222         TextRange union = prev.union(range);
223
224         Collection<HighlightInfo> collection = infos.get(prev);
225         collection.addAll(infos.get(range));
226         infos.remove(prev);
227         infos.remove(range);
228         infos.put(union, collection);
229         ranges.set(i - 1, union);
230         i--;
231       }
232     }
233
234     MarkupModel markup = document.getMarkupModel(project);
235     assertMarkupConsistent(markup, project);
236
237     for (Map.Entry<TextRange, Collection<HighlightInfo>> entry : infos.entrySet()) {
238       TextRange range = entry.getKey();
239       Collection<HighlightInfo> highlights = entry.getValue();
240       setHighlightersInRange(range, highlights, (MarkupModelEx)markup, group, document, project);
241     }
242   }
243
244   private static void setHighlightersInRange(final TextRange range,
245                                              Collection<HighlightInfo> highlightsCo,
246                                              final MarkupModelEx markup,
247                                              final int group,
248                                              final Document document,
249                                              final Project project) {
250     final List<HighlightInfo> highlights = new ArrayList<HighlightInfo>(highlightsCo);
251
252     final SeverityRegistrar severityRegistrar = SeverityRegistrar.getInstance(project);
253     final HighlightersRecycler infosToRemove = new HighlightersRecycler();
254     DaemonCodeAnalyzerImpl.processHighlights(document, project, null, range.getStartOffset(), range.getEndOffset(), new Processor<HighlightInfo>() {
255       @Override
256       public boolean process(HighlightInfo info) {
257         if (info.group == group) {
258           RangeHighlighter highlighter = info.highlighter;
259           int endOffset = highlighter.getEndOffset();
260           int startOffset = highlighter.getStartOffset();
261           boolean willBeRemoved = endOffset == document.getTextLength() && range.getEndOffset() == document.getTextLength()
262                                   || range.contains(startOffset)
263                                   || range.containsRange(startOffset, endOffset);
264           if (willBeRemoved) {
265             infosToRemove.recycleHighlighter(highlighter);
266             info.highlighter = null;
267           }
268         }
269         return true;
270       }
271     });
272
273     Collections.sort(highlights, BY_START_OFFSET_NODUPS);
274     final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<TextRange, RangeMarker>(10);
275     final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
276     final boolean[] changed = {false};
277     RangeMarkerTree.sweep(new RangeMarkerTree.Generator<HighlightInfo>(){
278       @Override
279       public boolean generate(Processor<HighlightInfo> processor) {
280         return ContainerUtil.process(highlights, processor);
281       }
282     }, new MarkupModelEx.SweepProcessor<HighlightInfo>() {
283       @Override
284       public boolean process(int offset, HighlightInfo info, boolean atStart, Collection<HighlightInfo> overlappingIntervals) {
285         if (!atStart) {
286           return true;
287         }
288         if (info.isFileLevelAnnotation && psiFile != null && psiFile.getViewProvider().isPhysical()) {
289           addFileLevelHighlight(project, group, info, psiFile);
290           changed[0] = true;
291           return true;
292         }
293         if (isWarningCoveredByError(info, overlappingIntervals, severityRegistrar)) {
294           return true;
295         }
296         boolean success = createOrReuseHighlighterFor(info, document, group, psiFile, markup, infosToRemove,
297                                           ranges2markersCache, range.getStartOffset(), range.getEndOffset(),
298                                           severityRegistrar);
299         if (success) {
300           changed[0] = true;
301         }
302         return true;
303       }
304     });
305     for (RangeHighlighter highlighter : infosToRemove.forAllInGarbageBin()) {
306       markup.removeHighlighter(highlighter);
307       changed[0] = true;
308     }
309
310     if (changed[0]) {
311       clearWhiteSpaceOptimizationFlag(document);
312     }
313     assertMarkupConsistent(markup, project);
314   }
315
316   private static boolean isWarningCoveredByError(HighlightInfo info,
317                                                  Collection<HighlightInfo> overlappingIntervals,
318                                                  SeverityRegistrar severityRegistrar) {
319     if (!isError(info, severityRegistrar)) {
320       for (HighlightInfo overlapping : overlappingIntervals) {
321         boolean overlapIsError = isError(overlapping, severityRegistrar);
322         if (overlapIsError && DaemonCodeAnalyzerImpl.isCoveredBy(info, overlapping)) {
323           return true;
324         }
325       }
326     }
327     return false;
328   }
329
330   private static boolean isError(HighlightInfo info, SeverityRegistrar severityRegistrar) {
331     return severityRegistrar.compare(HighlightSeverity.ERROR, info.getSeverity()) <= 0;
332   }
333
334   // return true if changed
335   private static boolean createOrReuseHighlighterFor(@NotNull final HighlightInfo info,
336                                                      @NotNull final Document document,
337                                                      final int group,
338                                                      @NotNull final PsiFile psiFile,
339                                                      @NotNull MarkupModelEx markup,
340                                                      @Nullable HighlightersRecycler infosToRemove,
341                                                      @Nullable final Map<TextRange, RangeMarker> ranges2markersCache,
342                                                      int rangeStartOffset,
343                                                      int rangeEndOffset,
344                                                      SeverityRegistrar severityRegistrar) {
345     final int infoStartOffset = info.startOffset;
346     int infoEndOffset = info.endOffset;
347     if (infoStartOffset < rangeStartOffset || infoEndOffset > rangeEndOffset) return false;
348
349     if (infoEndOffset == infoStartOffset && !info.isAfterEndOfLine) {
350       infoEndOffset++; //show something in case of empty highlightinfo
351     }
352     final int docLength = document.getTextLength();
353     if (infoEndOffset > docLength) {
354       infoEndOffset = docLength;
355     }
356
357     info.text = document.getCharsSequence().subSequence(infoStartOffset, infoEndOffset).toString();
358     info.group = group;
359
360     int layer = getLayer(info, severityRegistrar);
361     RangeHighlighterEx highlighter = infosToRemove == null ? null : (RangeHighlighterEx)infosToRemove.pickupHighlighterFromGarbageBin(info.startOffset, info.endOffset, layer);
362     if (highlighter == null) {
363       highlighter = createRangeHighlighter(infoStartOffset, infoEndOffset, markup, layer);
364     }
365
366     final RangeHighlighterEx finalHighlighter = highlighter;
367     final int finalInfoEndOffset = infoEndOffset;
368     highlighter.changeAttributesInBatch(new Runnable() {
369       public void run() {
370         finalHighlighter.setTextAttributes(info.getTextAttributes(psiFile));
371
372         info.highlighter = finalHighlighter;
373         finalHighlighter.setAfterEndOfLine(info.isAfterEndOfLine);
374
375         Color color = info.getErrorStripeMarkColor(psiFile);
376         finalHighlighter.setErrorStripeMarkColor(color);
377         if (info != finalHighlighter.getErrorStripeTooltip()) {
378           finalHighlighter.setErrorStripeTooltip(info);
379         }
380         GutterIconRenderer renderer = info.getGutterIconRenderer();
381         finalHighlighter.setGutterIconRenderer(renderer);
382
383         if (ranges2markersCache != null) ranges2markersCache.put(new TextRange(infoStartOffset, finalInfoEndOffset), info.highlighter);
384         if (info.quickFixActionRanges != null) {
385           List<Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker>> list = new ArrayList<Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker>>(info.quickFixActionRanges.size());
386           for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair : info.quickFixActionRanges) {
387             TextRange textRange = pair.second;
388             RangeMarker marker = getOrCreate(document, ranges2markersCache, textRange);
389             list.add(Pair.create(pair.first, marker));
390           }
391           info.quickFixActionMarkers = new CopyOnWriteArrayList<Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker>>(list);
392         }
393         info.fixMarker = getOrCreate(document, ranges2markersCache, new TextRange(info.fixStartOffset, info.fixEndOffset));
394       }
395     });
396
397     assert Comparing.equal(info.getTextAttributes(psiFile), highlighter.getTextAttributes()) : "Info: " +
398                                                                                                info.getTextAttributes(psiFile) +
399                                                                                                "; highlighter:" +
400                                                                                                highlighter.getTextAttributes();
401     return true;
402   }
403
404   @NotNull
405   private static RangeHighlighterEx createRangeHighlighter(int infoStartOffset, int infoEndOffset, MarkupModelEx markup, int layer) {
406     return (RangeHighlighterEx)markup.addRangeHighlighter(infoStartOffset, infoEndOffset, layer, null, HighlighterTargetArea.EXACT_RANGE);
407   }
408
409   private static int getLayer(HighlightInfo info, SeverityRegistrar severityRegistrar) {
410     final HighlightSeverity severity = info.getSeverity();
411     int layer;
412     if (severity == HighlightSeverity.WARNING) {
413       layer = HighlighterLayer.WARNING;
414     }
415     else if (severityRegistrar.compare(severity, HighlightSeverity.ERROR) >= 0) {
416       layer = HighlighterLayer.ERROR;
417     }
418     else {
419       layer = HighlighterLayer.ADDITIONAL_SYNTAX;
420     }
421     return layer;
422   }
423
424   private static RangeMarker getOrCreate(@NotNull Document document, @Nullable Map<TextRange, RangeMarker> ranges2markersCache, @NotNull TextRange textRange) {
425     RangeMarker marker = ranges2markersCache == null ? null : ranges2markersCache.get(textRange);
426     if (marker == null) {
427       marker = document.createRangeMarker(textRange);
428       if (ranges2markersCache != null) {
429         ranges2markersCache.put(textRange, marker);
430       }
431     }
432     return marker;
433   }
434
435   private static void addFileLevelHighlight(final Project project, final int group, final HighlightInfo info, final PsiFile psiFile) {
436     VirtualFile vFile = psiFile.getViewProvider().getVirtualFile();
437     final FileEditorManager manager = FileEditorManager.getInstance(project);
438     for (FileEditor fileEditor : manager.getEditors(vFile)) {
439       if (fileEditor instanceof TextEditor) {
440         FileLevelIntentionComponent component = new FileLevelIntentionComponent(info.description, info.severity, info.quickFixActionRanges,
441                                                                                 project, psiFile, ((TextEditor)fileEditor).getEditor());
442         manager.addTopComponent(fileEditor, component);
443         List<HighlightInfo> fileLevelInfos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS);
444         if (fileLevelInfos == null) {
445           fileLevelInfos = new ArrayList<HighlightInfo>();
446           fileEditor.putUserData(FILE_LEVEL_HIGHLIGHTS, fileLevelInfos);
447         }
448         info.fileLevelComponent = component;
449         info.group = group;
450         fileLevelInfos.add(info);
451       }
452     }
453   }
454
455   public static void setLineMarkersToEditor(@NotNull Project project,
456                                             @NotNull Document document,
457                                             int startOffset,
458                                             int endOffset,
459                                             @NotNull Collection<LineMarkerInfo> markers,
460                                             int group) {
461     ApplicationManager.getApplication().assertIsDispatchThread();
462
463     List<LineMarkerInfo> oldMarkers = DaemonCodeAnalyzerImpl.getLineMarkers(document, project);
464     List<LineMarkerInfo> array = new ArrayList<LineMarkerInfo>(oldMarkers == null ? markers.size() : oldMarkers.size());
465     MarkupModel markupModel = document.getMarkupModel(project);
466     HighlightersRecycler toReuse = new HighlightersRecycler();
467     if (oldMarkers != null) {
468       for (LineMarkerInfo info : oldMarkers) {
469         RangeHighlighter highlighter = info.highlighter;
470         boolean toRemove = !highlighter.isValid() ||
471                            info.updatePass == group &&
472                            startOffset <= highlighter.getStartOffset() &&
473                            (highlighter.getEndOffset() < endOffset || highlighter.getEndOffset() == document.getTextLength());
474
475         if (toRemove) {
476           toReuse.recycleHighlighter(highlighter);
477         }
478         else {
479           array.add(info);
480         }
481       }
482     }
483
484     for (LineMarkerInfo info : markers) {
485       PsiElement element = info.getElement();
486       if (element == null) {
487         continue;
488       }
489
490       TextRange textRange = element.getTextRange();
491       if (textRange == null) continue;
492       TextRange elementRange = InjectedLanguageManager.getInstance(project).injectedToHost(element, textRange);
493       if (startOffset > elementRange.getStartOffset() || elementRange.getEndOffset() > endOffset) {
494         continue;
495       }
496       RangeHighlighter marker = toReuse.pickupHighlighterFromGarbageBin(info.startOffset, info.endOffset, HighlighterLayer.ADDITIONAL_SYNTAX);
497       if (marker == null) {
498         marker = markupModel.addRangeHighlighter(info.startOffset, info.endOffset, HighlighterLayer.ADDITIONAL_SYNTAX, null, HighlighterTargetArea.EXACT_RANGE);
499       }
500       LineMarkerInfo.LineMarkerGutterIconRenderer renderer = (LineMarkerInfo.LineMarkerGutterIconRenderer)info.createGutterRenderer();
501       LineMarkerInfo.LineMarkerGutterIconRenderer oldRenderer = marker.getGutterIconRenderer() instanceof LineMarkerInfo.LineMarkerGutterIconRenderer ? (LineMarkerInfo.LineMarkerGutterIconRenderer)marker.getGutterIconRenderer() : null;
502       if (oldRenderer == null || renderer == null || !renderer.looksTheSameAs(oldRenderer)) {
503         marker.setGutterIconRenderer(renderer);
504       }
505       if (!Comparing.equal(marker.getLineSeparatorColor(), info.separatorColor)) {
506         marker.setLineSeparatorColor(info.separatorColor);
507       }
508       if (!Comparing.equal(marker.getLineSeparatorPlacement(), info.separatorPlacement)) {
509         marker.setLineSeparatorPlacement(info.separatorPlacement);
510       }
511       info.highlighter = marker;
512       array.add(info);
513     }
514
515     for (RangeHighlighter highlighter : toReuse.forAllInGarbageBin()) {
516       markupModel.removeHighlighter(highlighter);
517     }
518
519     DaemonCodeAnalyzerImpl.setLineMarkers(document, array, project);
520   }
521
522   private static final Key<Boolean> TYPING_INSIDE_HIGHLIGHTER_OCCURRED = Key.create("TYPING_INSIDE_HIGHLIGHTER_OCCURRED");
523   public static boolean isWhitespaceOptimizationAllowed(final Document document) {
524     return document.getUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED) == null;
525   }
526   private static void disableWhiteSpaceOptimization(Document document) {
527     document.putUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED, Boolean.TRUE);
528   }
529   private static void clearWhiteSpaceOptimizationFlag(final Document document) {
530     document.putUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED, null);
531   }
532
533   public static void updateHighlightersByTyping(@NotNull Project project, @NotNull DocumentEvent e) {
534     ApplicationManager.getApplication().assertIsDispatchThread();
535
536     final Document document = e.getDocument();
537     if (document instanceof DocumentEx && ((DocumentEx)document).isInBulkUpdate()) return;
538
539     final MarkupModel markup = document.getMarkupModel(project);
540     assertMarkupConsistent(markup, project);
541
542     int offset = e.getOffset();
543     Editor[] editors = EditorFactory.getInstance().getEditors(document, project);
544     if (editors.length == 0) return;
545     Editor editor = editors[0]; // use any editor - just to fetch SelectInEditorManager
546     HighlighterIterator iterator = ((EditorEx)editor).getHighlighter().createIterator(Math.max(0, offset - 1));
547     if (iterator.atEnd()) return;
548     final int start = iterator.getStart();
549     while (iterator.getEnd() < e.getOffset() + Math.max(e.getOldLength(), e.getNewLength())) {
550       iterator.advance();
551       if (iterator.atEnd()) return;
552     }
553     final int end = iterator.getEnd();
554
555     final boolean[] highlightersChanged = {false};
556     final List<HighlightInfo> removed = new ArrayList<HighlightInfo>();
557     final boolean[] documentChangedInsideHighlighter = {false};
558     DaemonCodeAnalyzerImpl.processHighlights(document, project, null, start, end, new Processor<HighlightInfo>() {
559       public boolean process(HighlightInfo info) {
560         RangeHighlighter highlighter = info.highlighter;
561         boolean toRemove = false;
562         if (info.needUpdateOnTyping()) {
563           int highlighterStart = highlighter.getStartOffset();
564           int highlighterEnd = highlighter.getEndOffset();
565           if (info.isAfterEndOfLine) {
566             if (highlighterStart < document.getTextLength()) {
567               highlighterStart += 1;
568             }
569             if (highlighterEnd < document.getTextLength()) {
570               highlighterEnd += 1;
571             }
572           }
573
574           if (!highlighter.isValid()) {
575             toRemove = true;
576           }
577           else if (start < highlighterEnd && highlighterStart <= end) {
578             documentChangedInsideHighlighter[0] = true;
579             toRemove = true;
580           }
581         }
582
583         if (toRemove) {
584           //if (info.type.equals(HighlightInfoType.WRONG_REF)) {
585             /*
586             markup.removeHighlighter(highlighter);
587             */
588           //}
589           highlightersChanged[0] = true;
590           removed.add(info);
591         }
592
593         return true;
594       }
595     });
596
597     for (HighlightInfo info : removed) {
598       if (!info.highlighter.isValid() || info.type.equals(HighlightInfoType.WRONG_REF)) {
599         markup.removeHighlighter(info.highlighter);
600       }
601     }
602     
603     assertMarkupConsistent(markup, project);
604
605     if (highlightersChanged[0] || documentChangedInsideHighlighter[0]) {
606       disableWhiteSpaceOptimization(document);
607     }
608   }
609
610   @NotNull
611   @TestOnly
612   public static List<HighlightInfo> getFileLeveleHighlights(Project project, PsiFile file) {
613     VirtualFile vFile = file.getViewProvider().getVirtualFile();
614     final FileEditorManager manager = FileEditorManager.getInstance(project);
615     List<HighlightInfo> result = new ArrayList<HighlightInfo>();
616     for (FileEditor fileEditor : manager.getEditors(vFile)) {
617       final List<HighlightInfo> infos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS);
618       if (infos == null) continue;
619       for (HighlightInfo info : infos) {
620           result.add(info);
621       }
622     }
623     return result;
624   }
625
626   private static void assertMarkupConsistent(final MarkupModel markup, Project project) {
627     if (!RedBlackTree.VERIFY) {
628       return;
629     }
630     Document document = markup.getDocument();
631     DaemonCodeAnalyzerImpl.processHighlights(document, project, null, 0, document.getTextLength(), new Processor<HighlightInfo>() {
632       public boolean process(HighlightInfo info) {
633         assert ((MarkupModelEx)markup).containsHighlighter(info.highlighter);
634         return true;
635       }
636     });
637     RangeHighlighter[] allHighlighters = markup.getAllHighlighters();
638     for (RangeHighlighter highlighter : allHighlighters) {
639       if (!highlighter.isValid()) continue;
640       Object tooltip = highlighter.getErrorStripeTooltip();
641       if (!(tooltip instanceof HighlightInfo)) {
642         continue;
643       }
644       final HighlightInfo info = (HighlightInfo)tooltip;
645       boolean contains = !DaemonCodeAnalyzerImpl.processHighlights(document, project, null, info.getActualStartOffset(), info.getActualEndOffset(), new Processor<HighlightInfo>() {
646         public boolean process(HighlightInfo highlightInfo) {
647           return UpdateHighlightersUtil.BY_START_OFFSET_NODUPS.compare(highlightInfo, info) != 0;
648         }
649       });
650       assert contains: info;
651     }
652   }
653 }