cleanup (inspection "Java | Class structure | Utility class is not 'final'")
[idea/community.git] / platform / analysis-impl / src / com / intellij / codeInsight / daemon / impl / UpdateHighlightersUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2
3 package com.intellij.codeInsight.daemon.impl;
4
5 import com.intellij.codeInsight.daemon.GutterMark;
6 import com.intellij.lang.annotation.HighlightSeverity;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.editor.Document;
9 import com.intellij.openapi.editor.Editor;
10 import com.intellij.openapi.editor.RangeMarker;
11 import com.intellij.openapi.editor.colors.EditorColorsScheme;
12 import com.intellij.openapi.editor.colors.TextAttributesKey;
13 import com.intellij.openapi.editor.event.DocumentEvent;
14 import com.intellij.openapi.editor.ex.MarkupModelEx;
15 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
16 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
17 import com.intellij.openapi.editor.impl.RedBlackTree;
18 import com.intellij.openapi.editor.impl.SweepProcessor;
19 import com.intellij.openapi.editor.markup.*;
20 import com.intellij.openapi.extensions.ExtensionPointName;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.util.*;
23 import com.intellij.psi.PsiDocumentManager;
24 import com.intellij.psi.PsiFile;
25 import com.intellij.util.Consumer;
26 import com.intellij.util.Processor;
27 import com.intellij.util.containers.ContainerUtil;
28 import gnu.trove.THashMap;
29 import gnu.trove.THashSet;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import java.awt.*;
34 import java.util.List;
35 import java.util.*;
36
37 public final class UpdateHighlightersUtil {
38
39   private final static ExtensionPointName<HighlightInfoPostFilter> EP_NAME = ExtensionPointName.create("com.intellij.highlightInfoPostFilter");
40
41   private static final Comparator<HighlightInfo> BY_START_OFFSET_NODUPS = (o1, o2) -> {
42     int d = o1.getActualStartOffset() - o2.getActualStartOffset();
43     if (d != 0) return d;
44     d = o1.getActualEndOffset() - o2.getActualEndOffset();
45     if (d != 0) return d;
46
47     d = Comparing.compare(o1.getSeverity(), o2.getSeverity());
48     if (d != 0) return -d; // higher severity first, to prevent warnings overlap errors
49
50     if (!Comparing.equal(o1.type, o2.type)) {
51       return String.valueOf(o1.type).compareTo(String.valueOf(o2.type));
52     }
53
54     if (!Comparing.equal(o1.getGutterIconRenderer(), o2.getGutterIconRenderer())) {
55       return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
56     }
57
58     if (!Comparing.equal(o1.forcedTextAttributes, o2.forcedTextAttributes)) {
59       return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
60     }
61
62     if (!Comparing.equal(o1.forcedTextAttributesKey, o2.forcedTextAttributesKey)) {
63       return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
64     }
65
66     return Comparing.compare(o1.getDescription(), o2.getDescription());
67   };
68
69   private static boolean isCoveredByOffsets(@NotNull HighlightInfo info, @NotNull HighlightInfo coveredBy) {
70     return coveredBy.startOffset <= info.startOffset && info.endOffset <= coveredBy.endOffset && info.getGutterIconRenderer() == null;
71   }
72
73   static void addHighlighterToEditorIncrementally(@NotNull Project project,
74                                                   @NotNull Document document,
75                                                   @NotNull PsiFile file,
76                                                   int startOffset,
77                                                   int endOffset,
78                                                   @NotNull final HighlightInfo info,
79                                                   @Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
80                                                   final int group,
81                                                   @NotNull Map<TextRange, RangeMarker> ranges2markersCache) {
82     ApplicationManager.getApplication().assertIsDispatchThread();
83
84     if (!accept(project, info)) {
85       return;
86     }
87
88     if (isFileLevelOrGutterAnnotation(info)) return;
89     if (info.getStartOffset() < startOffset || info.getEndOffset() > endOffset) return;
90
91     MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
92     final SeverityRegistrar severityRegistrar = SeverityRegistrar.getSeverityRegistrar(project);
93     final boolean myInfoIsError = isSevere(info, severityRegistrar);
94     Processor<HighlightInfo> otherHighlightInTheWayProcessor = oldInfo -> {
95       if (!myInfoIsError && isCovered(info, severityRegistrar, oldInfo)) {
96         return false;
97       }
98
99       return oldInfo.getGroup() != group || !oldInfo.equalsByActualOffset(info);
100     };
101     boolean allIsClear = DaemonCodeAnalyzerEx.processHighlights(document, project,
102                                                                 null, info.getActualStartOffset(), info.getActualEndOffset(),
103                                                                 otherHighlightInTheWayProcessor);
104     if (allIsClear) {
105       createOrReuseHighlighterFor(info, colorsScheme, document, group, file, (MarkupModelEx)markup, null, ranges2markersCache, severityRegistrar);
106
107       clearWhiteSpaceOptimizationFlag(document);
108       assertMarkupConsistent(markup, project);
109     }
110   }
111
112   private static boolean accept(@NotNull Project project, @NotNull HighlightInfo info) {
113     for (HighlightInfoPostFilter filter : EP_NAME.getExtensions(project)) {
114       if (!filter.accept(info))
115         return false;
116     }
117
118     return true;
119   }
120
121   public static boolean isFileLevelOrGutterAnnotation(@NotNull HighlightInfo info) {
122     return info.isFileLevelAnnotation() || info.getGutterIconRenderer() != null;
123   }
124
125
126   public static void setHighlightersToSingleEditor(@NotNull Project project,
127                                                    @NotNull Editor editor,
128                                                    int startOffset,
129                                                    int endOffset,
130                                                    @NotNull Collection<? extends HighlightInfo> highlights,
131                                                    @Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
132                                                    int group) {
133     Document document = editor.getDocument();
134     MarkupModelEx markup = (MarkupModelEx)editor.getMarkupModel();
135     setHighlightersToEditor(project, document, startOffset, endOffset, highlights, colorsScheme, group, markup);
136   }
137
138   public static void setHighlightersToEditor(@NotNull Project project,
139                                              @NotNull Document document,
140                                              int startOffset,
141                                              int endOffset,
142                                              @NotNull Collection<? extends HighlightInfo> highlights,
143                                              @Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
144                                              int group) {
145     MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true);
146     setHighlightersToEditor(project, document, startOffset, endOffset, highlights, colorsScheme, group, markup);
147   }
148
149   private static void setHighlightersToEditor(@NotNull Project project,
150                                               @NotNull Document document,
151                                               int startOffset,
152                                               int endOffset,
153                                               @NotNull Collection<? extends HighlightInfo> highlights,
154                                               @Nullable final EditorColorsScheme colorsScheme, // if null, the global scheme will be used
155                                               int group,
156                                               @NotNull MarkupModelEx markup) {
157     TextRange range = new TextRange(startOffset, endOffset);
158     ApplicationManager.getApplication().assertIsDispatchThread();
159
160     PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
161     final DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project);
162     codeAnalyzer.cleanFileLevelHighlights(project, group, psiFile);
163
164     assertMarkupConsistent(markup, project);
165
166     setHighlightersInRange(project, document, range, colorsScheme, new ArrayList<>(highlights), markup, group);
167   }
168
169
170   @NotNull
171   private static List<HighlightInfo> applyPostFilter(@NotNull Project project, @NotNull List<? extends HighlightInfo> highlightInfos) {
172     List<HighlightInfo> result = new ArrayList<>(highlightInfos.size());
173     for (HighlightInfo info : highlightInfos) {
174       if (accept(project, info)) {
175         result.add(info);
176       }
177     }
178     return result;
179   }
180
181   // set highlights inside startOffset,endOffset but outside priorityRange
182   static void setHighlightersOutsideRange(@NotNull final Project project,
183                                           @NotNull final Document document,
184                                           @NotNull final PsiFile psiFile,
185                                           @NotNull final List<? extends HighlightInfo> infos,
186                                           @Nullable final EditorColorsScheme colorsScheme,
187                                           // if null global scheme will be used
188                                           final int startOffset,
189                                           final int endOffset,
190                                           @NotNull final ProperTextRange priorityRange,
191                                           final int group) {
192     List<HighlightInfo> filteredInfos = applyPostFilter(project, infos);
193     ApplicationManager.getApplication().assertIsDispatchThread();
194
195     final DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project);
196     if (startOffset == 0 && endOffset == document.getTextLength()) {
197       codeAnalyzer.cleanFileLevelHighlights(project, group, psiFile);
198     }
199
200     final MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
201     assertMarkupConsistent(markup, project);
202
203     final SeverityRegistrar severityRegistrar = SeverityRegistrar.getSeverityRegistrar(project);
204     final HighlightersRecycler infosToRemove = new HighlightersRecycler();
205     ContainerUtil.quickSort(filteredInfos, BY_START_OFFSET_NODUPS);
206     Set<HighlightInfo> infoSet = new THashSet<>(filteredInfos);
207
208     Processor<HighlightInfo> processor = info -> {
209       if (info.getGroup() == group) {
210         RangeHighlighter highlighter = info.getHighlighter();
211         int hiStart = highlighter.getStartOffset();
212         int hiEnd = highlighter.getEndOffset();
213         if (!info.isFromInjection() && hiEnd < document.getTextLength() && (hiEnd != 0 && hiEnd <= startOffset || hiStart >= endOffset)) {
214           return true; // injections are oblivious to restricting range
215         }
216         boolean toRemove = infoSet.contains(info) ||
217                            !priorityRange.containsRange(hiStart, hiEnd) &&
218                            (hiEnd != document.getTextLength() || priorityRange.getEndOffset() != document.getTextLength());
219         if (toRemove) {
220           infosToRemove.recycleHighlighter(highlighter);
221           info.setHighlighter(null);
222         }
223       }
224       return true;
225     };
226     DaemonCodeAnalyzerEx.processHighlightsOverlappingOutside(document, project, null, priorityRange.getStartOffset(), priorityRange.getEndOffset(), processor);
227
228     final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<>(10);
229     final boolean[] changed = {false};
230     SweepProcessor.Generator<HighlightInfo> generator = proc -> ContainerUtil.process(filteredInfos, proc);
231     SweepProcessor.sweep(generator, (offset, info, atStart, overlappingIntervals) -> {
232       if (!atStart) return true;
233       if (!info.isFromInjection() && info.getEndOffset() < document.getTextLength() && (info.getEndOffset() <= startOffset || info.getStartOffset()>=endOffset)) return true; // injections are oblivious to restricting range
234
235       if (info.isFileLevelAnnotation()) {
236         codeAnalyzer.addFileLevelHighlight(project, group, info, psiFile);
237         changed[0] = true;
238         return true;
239       }
240       if (isWarningCoveredByError(info, overlappingIntervals, severityRegistrar)) {
241         return true;
242       }
243       if (info.getStartOffset() < priorityRange.getStartOffset() || info.getEndOffset() > priorityRange.getEndOffset()) {
244         createOrReuseHighlighterFor(info, colorsScheme, document, group, psiFile, (MarkupModelEx)markup, infosToRemove,
245                                       ranges2markersCache, severityRegistrar);
246         changed[0] = true;
247       }
248       return true;
249     });
250     for (RangeHighlighter highlighter : infosToRemove.forAllInGarbageBin()) {
251       highlighter.dispose();
252       changed[0] = true;
253     }
254
255     if (changed[0]) {
256       clearWhiteSpaceOptimizationFlag(document);
257     }
258     assertMarkupConsistent(markup, project);
259   }
260
261   static void setHighlightersInRange(@NotNull final Project project,
262                                      @NotNull final Document document,
263                                      @NotNull final TextRange range,
264                                      @Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
265                                      @NotNull final List<? extends HighlightInfo> infos,
266                                      @NotNull final MarkupModelEx markup,
267                                      final int group) {
268     ApplicationManager.getApplication().assertIsDispatchThread();
269
270     final SeverityRegistrar severityRegistrar = SeverityRegistrar.getSeverityRegistrar(project);
271     final HighlightersRecycler infosToRemove = new HighlightersRecycler();
272     DaemonCodeAnalyzerEx.processHighlights(markup, project, null, range.getStartOffset(), range.getEndOffset(), info -> {
273         if (info.getGroup() == group) {
274           RangeHighlighter highlighter = info.getHighlighter();
275           int hiStart = highlighter.getStartOffset();
276           int hiEnd = highlighter.getEndOffset();
277           boolean willBeRemoved = hiEnd == document.getTextLength() && range.getEndOffset() == document.getTextLength()
278                                 /*|| range.intersectsStrict(hiStart, hiEnd)*/ || range.containsRange(hiStart, hiEnd) /*|| hiStart <= range.getStartOffset() && hiEnd >= range.getEndOffset()*/;
279           if (willBeRemoved) {
280             infosToRemove.recycleHighlighter(highlighter);
281             info.setHighlighter(null);
282           }
283         }
284         return true;
285       });
286
287     List<HighlightInfo> filteredInfos = applyPostFilter(project, infos);
288     ContainerUtil.quickSort(filteredInfos, BY_START_OFFSET_NODUPS);
289     final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<>(10);
290     final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
291     final DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project);
292     final boolean[] changed = {false};
293     SweepProcessor.Generator<HighlightInfo> generator = processor -> ContainerUtil.process(filteredInfos, processor);
294     SweepProcessor.sweep(generator, (offset, info, atStart, overlappingIntervals) -> {
295       if (!atStart) {
296         return true;
297       }
298       if (info.isFileLevelAnnotation() && psiFile != null) {
299         codeAnalyzer.addFileLevelHighlight(project, group, info, psiFile);
300         changed[0] = true;
301         return true;
302       }
303       if (isWarningCoveredByError(info, overlappingIntervals, severityRegistrar)) {
304         return true;
305       }
306       if (info.getStartOffset() >= range.getStartOffset() && info.getEndOffset() <= range.getEndOffset() && psiFile != null) {
307         createOrReuseHighlighterFor(info, colorsScheme, document, group, psiFile, markup, infosToRemove, ranges2markersCache, severityRegistrar);
308         changed[0] = true;
309       }
310       return true;
311     });
312     for (RangeHighlighter highlighter : infosToRemove.forAllInGarbageBin()) {
313       highlighter.dispose();
314       changed[0] = true;
315     }
316
317     if (changed[0]) {
318       clearWhiteSpaceOptimizationFlag(document);
319     }
320     assertMarkupConsistent(markup, project);
321   }
322
323   private static boolean isWarningCoveredByError(@NotNull HighlightInfo info,
324                                                  @NotNull Collection<? extends HighlightInfo> overlappingIntervals,
325                                                  @NotNull SeverityRegistrar severityRegistrar) {
326     if (!isSevere(info, severityRegistrar)) {
327       for (HighlightInfo overlapping : overlappingIntervals) {
328         if (isCovered(info, severityRegistrar, overlapping)) return true;
329       }
330     }
331     return false;
332   }
333
334   private static boolean isCovered(@NotNull HighlightInfo warning, @NotNull SeverityRegistrar severityRegistrar, @NotNull HighlightInfo candidate) {
335     if (!isCoveredByOffsets(warning, candidate)) return false;
336     HighlightSeverity severity = candidate.getSeverity();
337     if (severity == HighlightInfoType.SYMBOL_TYPE_SEVERITY) return false; // syntax should not interfere with warnings
338     return isSevere(candidate, severityRegistrar);
339   }
340
341   private static boolean isSevere(@NotNull HighlightInfo info, @NotNull SeverityRegistrar severityRegistrar) {
342     HighlightSeverity severity = info.getSeverity();
343     return severityRegistrar.compare(HighlightSeverity.ERROR, severity) <= 0 || severity == HighlightInfoType.SYMBOL_TYPE_SEVERITY;
344   }
345
346   private static void createOrReuseHighlighterFor(@NotNull final HighlightInfo info,
347                                                   @Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
348                                                   @NotNull final Document document,
349                                                   final int group,
350                                                   @NotNull final PsiFile psiFile,
351                                                   @NotNull MarkupModelEx markup,
352                                                   @Nullable HighlightersRecycler infosToRemove,
353                                                   @NotNull final Map<TextRange, RangeMarker> ranges2markersCache,
354                                                   @NotNull SeverityRegistrar severityRegistrar) {
355     int infoStartOffset = info.startOffset;
356     int infoEndOffset = info.endOffset;
357
358     final int docLength = document.getTextLength();
359     if (infoEndOffset > docLength) {
360       infoEndOffset = docLength;
361       infoStartOffset = Math.min(infoStartOffset, infoEndOffset);
362     }
363     if (infoEndOffset == infoStartOffset && !info.isAfterEndOfLine()) {
364       if (infoEndOffset == docLength) return;  // empty highlighter beyond file boundaries
365       infoEndOffset++; //show something in case of empty highlightinfo
366     }
367
368     info.setGroup(group);
369
370     int layer = getLayer(info, severityRegistrar);
371     RangeHighlighterEx highlighter = infosToRemove == null ? null : (RangeHighlighterEx)infosToRemove.pickupHighlighterFromGarbageBin(infoStartOffset, infoEndOffset, layer);
372
373     final TextRange finalInfoRange = new TextRange(infoStartOffset, infoEndOffset);
374     final TextAttributes infoAttributes = info.getTextAttributes(psiFile, colorsScheme);
375     Consumer<RangeHighlighterEx> changeAttributes = finalHighlighter -> {
376       TextAttributesKey textAttributesKey = info.forcedTextAttributesKey == null ? info.type.getAttributesKey() : info.forcedTextAttributesKey;
377       finalHighlighter.setTextAttributesKey(textAttributesKey);
378
379       if (infoAttributes != null && !infoAttributes.equals(finalHighlighter.getTextAttributes(colorsScheme))) {
380         finalHighlighter.setTextAttributes(infoAttributes);
381       }
382
383       info.setHighlighter(finalHighlighter);
384       finalHighlighter.setAfterEndOfLine(info.isAfterEndOfLine());
385
386       Color infoErrorStripeColor = info.getErrorStripeMarkColor(psiFile, colorsScheme);
387       TextAttributes attributes = finalHighlighter.getTextAttributes(colorsScheme);
388       Color attributesErrorStripeColor = attributes != null ? attributes.getErrorStripeColor() : null;
389       if (infoErrorStripeColor != null && !infoErrorStripeColor.equals(attributesErrorStripeColor)) {
390         finalHighlighter.setErrorStripeMarkColor(infoErrorStripeColor);
391       }
392
393       if (info != finalHighlighter.getErrorStripeTooltip()) {
394         finalHighlighter.setErrorStripeTooltip(info);
395       }
396       GutterMark renderer = info.getGutterIconRenderer();
397       finalHighlighter.setGutterIconRenderer((GutterIconRenderer)renderer);
398
399       ranges2markersCache.put(finalInfoRange, info.getHighlighter());
400       if (info.quickFixActionRanges != null) {
401         List<Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker>> list =
402           new ArrayList<>(info.quickFixActionRanges.size());
403         for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair : info.quickFixActionRanges) {
404           TextRange textRange = pair.second;
405           RangeMarker marker = getOrCreate(document, ranges2markersCache, textRange);
406           list.add(Pair.create(pair.first, marker));
407         }
408         info.quickFixActionMarkers = ContainerUtil.createLockFreeCopyOnWriteList(list);
409       }
410       ProperTextRange fixRange = info.getFixTextRange();
411       if (finalInfoRange.equals(fixRange)) {
412         info.fixMarker = null; // null means it the same as highlighter'
413       }
414       else {
415         info.fixMarker = getOrCreate(document, ranges2markersCache, fixRange);
416       }
417     };
418
419     if (highlighter == null) {
420       highlighter = markup.addRangeHighlighterAndChangeAttributes(null, infoStartOffset, infoEndOffset, layer,
421                                                                   HighlighterTargetArea.EXACT_RANGE, false, changeAttributes);
422       if (HighlightInfoType.VISIBLE_IF_FOLDED.contains(info.type)) {
423         highlighter.setVisibleIfFolded(true);
424       }
425     }
426     else {
427       markup.changeAttributesInBatch(highlighter, changeAttributes);
428     }
429
430     if (infoAttributes != null) {
431       boolean attributesSet = Comparing.equal(infoAttributes, highlighter.getTextAttributes(colorsScheme));
432       assert attributesSet : "Info: " + infoAttributes +
433                              "; colorsScheme: " + (colorsScheme == null ? "[global]" : colorsScheme.getName()) +
434                              "; highlighter:" + highlighter.getTextAttributes(colorsScheme);
435     }
436   }
437
438   private static int getLayer(@NotNull HighlightInfo info, @NotNull SeverityRegistrar severityRegistrar) {
439     final HighlightSeverity severity = info.getSeverity();
440     int layer;
441     if (severity == HighlightSeverity.WARNING) {
442       layer = HighlighterLayer.WARNING;
443     }
444     else if (severity == HighlightSeverity.WEAK_WARNING) {
445       layer = HighlighterLayer.WEAK_WARNING;
446     }
447     else if (severityRegistrar.compare(severity, HighlightSeverity.ERROR) >= 0) {
448       layer = HighlighterLayer.ERROR;
449     }
450     else if (severity == HighlightInfoType.INJECTED_FRAGMENT_SEVERITY || severity == HighlightInfoType.HIGHLIGHTED_REFERENCE_SEVERITY) {
451       layer = HighlighterLayer.CARET_ROW - 1;
452     }
453     else if (severity == HighlightInfoType.INJECTED_FRAGMENT_SYNTAX_SEVERITY) {
454       layer = HighlighterLayer.CARET_ROW - 2;
455     }
456     else if (severity == HighlightInfoType.ELEMENT_UNDER_CARET_SEVERITY) {
457       layer = HighlighterLayer.ELEMENT_UNDER_CARET;
458     }
459     else {
460       layer = HighlighterLayer.ADDITIONAL_SYNTAX;
461     }
462     return layer;
463   }
464
465   @NotNull
466   private static RangeMarker getOrCreate(@NotNull Document document, @NotNull Map<TextRange, RangeMarker> ranges2markersCache, @NotNull TextRange textRange) {
467     return ranges2markersCache.computeIfAbsent(textRange, __ -> document.createRangeMarker(textRange));
468   }
469
470   private static final Key<Boolean> TYPING_INSIDE_HIGHLIGHTER_OCCURRED = Key.create("TYPING_INSIDE_HIGHLIGHTER_OCCURRED");
471   static boolean isWhitespaceOptimizationAllowed(@NotNull Document document) {
472     return document.getUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED) == null;
473   }
474   private static void disableWhiteSpaceOptimization(@NotNull Document document) {
475     document.putUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED, Boolean.TRUE);
476   }
477   private static void clearWhiteSpaceOptimizationFlag(@NotNull Document document) {
478     document.putUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED, null);
479   }
480
481   static void updateHighlightersByTyping(@NotNull Project project, @NotNull DocumentEvent e) {
482     ApplicationManager.getApplication().assertIsDispatchThread();
483
484     final Document document = e.getDocument();
485     if (document.isInBulkUpdate()) return;
486
487     final MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
488     assertMarkupConsistent(markup, project);
489
490     final int start = e.getOffset() - 1;
491     final int end = start + e.getOldLength();
492
493     final List<HighlightInfo> toRemove = new ArrayList<>();
494     DaemonCodeAnalyzerEx.processHighlights(document, project, null, start, end, info -> {
495       if (!info.needUpdateOnTyping()) return true;
496
497       RangeHighlighter highlighter = info.getHighlighter();
498       int highlighterStart = highlighter.getStartOffset();
499       int highlighterEnd = highlighter.getEndOffset();
500       if (info.isAfterEndOfLine()) {
501         if (highlighterStart < document.getTextLength()) {
502           highlighterStart += 1;
503         }
504         if (highlighterEnd < document.getTextLength()) {
505           highlighterEnd += 1;
506         }
507       }
508       if (!highlighter.isValid() || start < highlighterEnd && highlighterStart <= end) {
509         toRemove.add(info);
510       }
511       return true;
512     });
513
514     for (HighlightInfo info : toRemove) {
515       if (!info.getHighlighter().isValid() || info.type.equals(HighlightInfoType.WRONG_REF)) {
516         info.getHighlighter().dispose();
517       }
518     }
519
520     assertMarkupConsistent(markup, project);
521
522     if (!toRemove.isEmpty()) {
523       disableWhiteSpaceOptimization(document);
524     }
525   }
526
527   private static void assertMarkupConsistent(@NotNull final MarkupModel markup, @NotNull Project project) {
528     if (!RedBlackTree.VERIFY) {
529       return;
530     }
531     Document document = markup.getDocument();
532     DaemonCodeAnalyzerEx.processHighlights(document, project, null, 0, document.getTextLength(), info -> {
533       assert ((MarkupModelEx)markup).containsHighlighter(info.getHighlighter());
534       return true;
535     });
536     RangeHighlighter[] allHighlighters = markup.getAllHighlighters();
537     for (RangeHighlighter highlighter : allHighlighters) {
538       if (!highlighter.isValid()) continue;
539       HighlightInfo info = HighlightInfo.fromRangeHighlighter(highlighter);
540       if (info == null) continue;
541       boolean contains = !DaemonCodeAnalyzerEx
542         .processHighlights(document, project, null, info.getActualStartOffset(), info.getActualEndOffset(),
543                            highlightInfo -> BY_START_OFFSET_NODUPS.compare(highlightInfo, info) != 0);
544       assert contains: info;
545     }
546   }
547 }