1 19 package org.netbeans.modules.editor.hints; 20 21 import java.awt.Color ; 22 import java.awt.Container ; 23 import java.awt.Dimension ; 24 import java.awt.Font ; 25 import java.awt.Point ; 26 import java.beans.PropertyChangeEvent ; 27 import java.beans.PropertyChangeListener ; 28 import java.io.IOException ; 29 import java.lang.Runnable ; 30 import java.util.ArrayList ; 31 import java.util.Arrays ; 32 import java.util.Collection ; 33 import java.util.EnumMap ; 34 import java.util.HashMap ; 35 import java.util.HashSet ; 36 import java.util.Iterator ; 37 import java.util.List ; 38 import java.util.Map ; 39 import java.util.Set ; 40 import java.util.logging.Level ; 41 import java.util.logging.Logger ; 42 import javax.swing.JEditorPane ; 43 import javax.swing.JViewport ; 44 import javax.swing.SwingUtilities ; 45 import javax.swing.event.ChangeEvent ; 46 import javax.swing.event.ChangeListener ; 47 import javax.swing.text.BadLocationException ; 48 import javax.swing.text.Document ; 49 import javax.swing.text.Position ; 50 import javax.swing.text.StyledDocument ; 51 import javax.swing.text.StyledDocument ; 52 import org.netbeans.editor.Coloring; 53 import org.netbeans.modules.editor.highlights.spi.DefaultHighlight; 54 import org.netbeans.modules.editor.highlights.spi.Highlight; 55 import org.netbeans.modules.editor.highlights.spi.Highlighter; 56 import org.netbeans.spi.editor.hints.ErrorDescription; 57 import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; 58 import org.netbeans.spi.editor.hints.LazyFixList; 59 import org.netbeans.spi.editor.hints.Severity; 60 import org.openide.cookies.EditorCookie; 61 import org.openide.cookies.LineCookie; 62 import org.openide.filesystems.FileObject; 63 import org.openide.loaders.DataObject; 64 import org.openide.text.Annotation; 65 import org.openide.text.Line; 66 import org.openide.text.NbDocument; 67 import org.openide.util.RequestProcessor; 68 import org.openide.util.RequestProcessor; 69 import org.openide.util.WeakListeners; 70 71 import org.netbeans.api.timers.TimesCollector; 72 import org.openide.filesystems.FileUtil; 73 74 78 public class AnnotationHolder implements ChangeListener , PropertyChangeListener { 79 80 final static Map <Severity, Coloring> COLORINGS; 81 82 static { 83 COLORINGS = new EnumMap <Severity, Coloring>(Severity.class); 84 COLORINGS.put(Severity.DISABLED, new Coloring()); 85 COLORINGS.put(Severity.ERROR, new Coloring(null, 0, null, null, null, null, new Color (0xFF, 0x00, 0x00))); 86 COLORINGS.put(Severity.WARNING, new Coloring(null, 0, null, null, null, null, new Color (0xC0, 0xC0, 0x00))); 87 COLORINGS.put(Severity.VERIFIER, new Coloring(null, 0, null, null, null, null, new Color (0xFF, 0xD5, 0x55))); 88 COLORINGS.put(Severity.HINT, new Coloring()); 89 COLORINGS.put(Severity.TODO, new Coloring(Font.decode(null).deriveFont(Font.BOLD), Coloring.FONT_MODE_APPLY_STYLE, Color.BLUE, null)); 90 }; 91 92 private Map <ErrorDescription, List <Integer >> errors2Lines; 93 private Map <Integer , List <ErrorDescription>> line2Errors; 94 private Map <Integer , List <Highlight>> line2Highlights; 95 private Map <Integer , ParseErrorAnnotation> line2Annotations; 96 private Map <String , List <ErrorDescription>> layer2Errors; 97 98 private Set <JEditorPane > openedComponents; 99 private EditorCookie.Observable editorCookie; 100 private FileObject file; 101 private DataObject od; 102 private Document doc; 103 104 private static Map <FileObject, AnnotationHolder> file2Holder = new HashMap <FileObject, AnnotationHolder>(); 105 106 public static synchronized AnnotationHolder getInstance(FileObject file) { 107 if (file == null) 108 return null; 109 110 AnnotationHolder result = file2Holder.get(file); 111 112 if (result == null) { 113 try { 114 DataObject od = DataObject.find(file); 115 EditorCookie.Observable editorCookie = od.getCookie(EditorCookie.Observable.class); 116 117 if (editorCookie == null) { 118 Logger.getLogger("global").log(Level.WARNING, "No EditorCookie.Observable for file: " + FileUtil.getFileDisplayName(file)); 119 } else { 120 Document doc = editorCookie.getDocument(); 121 122 if (doc != null) { 123 file2Holder.put(file, result = new AnnotationHolder(file, od, doc, editorCookie)); 124 } 125 } 126 } catch (IOException e) { 127 Logger.getLogger("global").log(Level.INFO, null, e); 128 } 129 } 130 131 return result; 132 } 133 134 135 static synchronized Collection <FileObject> coveredFiles() { 136 return new ArrayList <FileObject>(file2Holder.keySet()); 137 } 138 139 private AnnotationHolder(FileObject file, DataObject od, Document doc, EditorCookie.Observable editorCookie) throws IOException { 140 if (file == null) 141 return ; 142 143 init(); 144 145 this.file = file; 146 this.od = od; 147 this.doc = doc; 148 editorCookie.addPropertyChangeListener(WeakListeners.propertyChange(this, editorCookie)); 149 this.editorCookie = editorCookie; 150 151 propertyChange(null); 152 153 TimesCollector.getDefault().reportReference(file, "annotation-holder", "[M] Annotation Holder", this); 154 } 155 156 private synchronized void init() { 157 errors2Lines = new HashMap <ErrorDescription, List <Integer >>(); 158 line2Errors = new HashMap <Integer , List <ErrorDescription>>(); 159 line2Highlights = new HashMap <Integer , List <Highlight>>(); 160 line2Annotations = new HashMap <Integer , ParseErrorAnnotation>(); 161 layer2Errors = new HashMap <String , List <ErrorDescription>>(); 162 openedComponents = new HashSet <JEditorPane >(); 163 } 164 165 public void stateChanged(ChangeEvent evt) { 166 updateVisibleRanges(); 167 } 168 169 Attacher attacher = new NbDocumentAttacher(); 170 171 void attachAnnotation(int line, ParseErrorAnnotation a) throws BadLocationException { 172 attacher.attachAnnotation(line, a); 173 } 174 175 void detachAnnotation(Annotation a) { 176 attacher.detachAnnotation(a); 177 } 178 179 static interface Attacher { 180 public void attachAnnotation(int line, ParseErrorAnnotation a) throws BadLocationException ; 181 public void detachAnnotation(Annotation a); 182 } 183 184 final class LineAttacher implements Attacher { 185 public void attachAnnotation(int line, ParseErrorAnnotation a) throws BadLocationException { 186 LineCookie lc = od.getCookie(LineCookie.class); 187 Line lineRef = lc.getLineSet().getCurrent(line); 188 189 a.attach(lineRef); 190 } 191 public void detachAnnotation(Annotation a) { 192 a.detach(); 193 } 194 } 195 196 final class NbDocumentAttacher implements Attacher { 197 public void attachAnnotation(int line, ParseErrorAnnotation a) throws BadLocationException { 198 Position lineStart = doc.createPosition(NbDocument.findLineOffset((StyledDocument ) doc, line)); 199 200 NbDocument.addAnnotation((StyledDocument ) doc, lineStart, -1, a); 201 } 202 public void detachAnnotation(Annotation a) { 203 if (doc != null) { 204 NbDocument.removeAnnotation((StyledDocument ) doc, a); 205 } 206 } 207 } 208 209 private synchronized void clearAll() { 210 for (ParseErrorAnnotation a : line2Annotations.values()) { 212 detachAnnotation(a); 213 } 214 215 file2Holder.remove(file); 216 } 217 218 public void propertyChange(PropertyChangeEvent evt) { 219 SwingUtilities.invokeLater(new Runnable () { 220 public void run() { 221 JEditorPane [] panes = editorCookie.getOpenedPanes(); 222 223 if (panes == null) { 224 clearAll(); 225 return ; 226 } 227 228 Set <JEditorPane > addedPanes = new HashSet <JEditorPane >(Arrays.asList(panes)); 229 Set <JEditorPane > removedPanes = new HashSet <JEditorPane >(openedComponents); 230 231 removedPanes.removeAll(addedPanes); 232 addedPanes.removeAll(openedComponents); 233 234 for (JEditorPane pane : addedPanes) { 235 Container parent = pane.getParent(); 236 237 if (parent instanceof JViewport ) { 238 JViewport viewport = (JViewport ) parent; 239 240 viewport.addChangeListener(WeakListeners.change(AnnotationHolder.this, viewport)); 241 } 242 } 243 244 openedComponents.removeAll(removedPanes); 245 openedComponents.addAll(addedPanes); 246 247 updateVisibleRanges(); 248 return ; 249 } 250 }); 251 } 252 253 private void updateVisibleRanges() { 254 SwingUtilities.invokeLater(new Runnable () { 255 public void run() { 256 final List <int[]> visibleRanges = new ArrayList <int[]>(); 257 258 synchronized(AnnotationHolder.this) { 259 for (JEditorPane pane : openedComponents) { 260 Container parent = pane.getParent(); 261 262 if (parent instanceof JViewport ) { 263 JViewport viewport = (JViewport ) parent; 264 Point start = viewport.getViewPosition(); 265 Dimension size = viewport.getExtentSize(); 266 Point end = new Point (start.x + size.width, start.y + size.height); 267 268 int startPosition = pane.viewToModel(start); 269 int endPosition = pane.viewToModel(end); 270 int startLine = NbDocument.findLineNumber((StyledDocument ) pane.getDocument(), startPosition); 271 int endLine = NbDocument.findLineNumber((StyledDocument ) pane.getDocument(), endPosition); 272 274 visibleRanges.add(new int[] {startLine, endLine}); 275 } 276 } 277 } 278 279 INSTANCE.post(new Runnable () { 280 public void run() { 281 for (int[] span : visibleRanges) { 282 updateAnnotations(span[0], span[1]); 283 } 284 } 285 }); 286 } 287 }); 288 } 289 290 private void updateAnnotations(int startLine, int endLine) { 291 List <ErrorDescription> errorsToUpdate = new ArrayList <ErrorDescription>(); 292 293 synchronized (this) { 294 for (int cntr = startLine; cntr <= endLine; cntr++) { 295 List <ErrorDescription> errors = line2Errors.get(cntr); 296 297 if (errors != null) { 298 errorsToUpdate.addAll(errors); 299 } 300 } 301 } 302 303 for (ErrorDescription e : errorsToUpdate) { 304 LazyFixList l = e.getFixes(); 305 306 if (l.probablyContainsFixes() && !l.isComputed()) 307 l.getFixes(); 308 } 309 } 310 311 private List <ErrorDescription> getErrorsForLayer(String layer) { 312 List <ErrorDescription> errors = layer2Errors.get(layer); 313 314 if (errors == null) { 315 layer2Errors.put(layer, errors = new ArrayList <ErrorDescription>()); 316 } 317 318 return errors; 319 } 320 321 private List <ErrorDescription> getErrorsForLine(Integer line) { 322 List <ErrorDescription> errors = line2Errors.get(line); 323 324 if (errors == null) { 325 line2Errors.put(line, errors = new ArrayList <ErrorDescription>()); 326 } 327 328 return errors; 329 } 330 331 private List <ErrorDescription> filter(List <ErrorDescription> errors, boolean onlyErrors) { 332 List <ErrorDescription> result = new ArrayList <ErrorDescription>(); 333 334 for (ErrorDescription e : errors) { 335 if (e.getSeverity() == Severity.ERROR) { 336 if (onlyErrors) 337 result.add(e); 338 } else { 339 if (!onlyErrors) 340 result.add(e); 341 } 342 } 343 344 return result; 345 } 346 347 private void concatDescription(List <ErrorDescription> errors, StringBuffer description) { 348 boolean first = true; 349 350 for (ErrorDescription e : errors) { 351 if (!first) { 352 description.append("\n\n"); 353 } 354 description.append(e.getDescription()); 355 first = false; 356 } 357 } 358 359 private List <LazyFixList> computeFixes(List <ErrorDescription> errors) { 360 List <LazyFixList> result = new ArrayList <LazyFixList>(); 361 362 for (ErrorDescription e : errors) { 363 result.add(e.getFixes()); 364 } 365 366 return result; 367 } 368 369 private void updateAnnotationOnLine(Integer line) throws BadLocationException { 370 List <ErrorDescription> errorDescriptions = getErrorsForLine(line); 371 372 if (errorDescriptions.isEmpty()) { 373 Annotation ann = line2Annotations.remove(line); 375 376 detachAnnotation(ann); 377 return; 378 } 379 380 List <ErrorDescription> trueErrors = filter(errorDescriptions, true); 381 List <ErrorDescription> others = filter(errorDescriptions, false); 382 boolean hasErrors = !trueErrors.isEmpty(); 383 384 StringBuffer description = new StringBuffer (); 386 387 concatDescription(trueErrors, description); 388 389 if (!trueErrors.isEmpty() && !others.isEmpty()) { 390 description.append("\n\n"); 391 } 392 393 concatDescription(others, description); 394 395 Severity mostImportantSeverity; 396 397 if (hasErrors) { 398 mostImportantSeverity = Severity.ERROR; 399 } else { 400 mostImportantSeverity = Severity.HINT; 401 402 for (ErrorDescription e : others) { 403 if (mostImportantSeverity.compareTo(e.getSeverity()) > 0) { 404 mostImportantSeverity = e.getSeverity(); 405 } 406 } 407 } 408 409 LazyFixList fixes = ErrorDescriptionFactory.lazyListForDelegates(computeFixes(hasErrors ? trueErrors : others)); 410 411 ParseErrorAnnotation pea = new ParseErrorAnnotation(mostImportantSeverity, fixes, description.toString(), line, this); 412 Annotation previous = line2Annotations.put(line, pea); 413 414 if (previous != null) { 415 detachAnnotation(previous); 416 } 417 418 attachAnnotation(line, pea); 419 } 420 421 private void doUpdateHighlight(FileObject file) { 422 List <Highlight> highlights = new ArrayList <Highlight>(); 423 424 for (List <Highlight> hls : line2Highlights.values()) { 425 highlights.addAll(hls); 426 } 427 428 Highlighter.getDefault().setHighlights(file, AnnotationHolder.class.getName(), highlights); 429 } 430 431 void updateHighlightsOnLine(Integer line) throws IOException { 432 List <ErrorDescription> errorDescriptions = getErrorsForLine(line); 433 434 if (errorDescriptions.isEmpty()) { 435 line2Highlights.remove(line); 437 438 return; 439 } 440 441 List <Highlight> highlights = new ArrayList <Highlight>(); 442 443 try { 444 computeHighlights(doc, line, errorDescriptions, highlights); 445 line2Highlights.put(line, highlights); 446 } catch (BadLocationException ex) { 447 throw (IOException ) new IOException ().initCause(ex); 448 } 449 } 450 451 static void computeHighlights(Document doc, Integer line, List <ErrorDescription> errorDescriptions, List <Highlight> highlights) throws IOException , BadLocationException { 452 for (Severity s : Arrays.asList(Severity.ERROR, Severity.WARNING, Severity.VERIFIER)) { 453 Coloring c = COLORINGS.get(s); 454 List <ErrorDescription> filteredDescriptions = new ArrayList <ErrorDescription>(); 455 456 for (ErrorDescription e : errorDescriptions) { 457 if (e.getSeverity() == s) { 458 filteredDescriptions.add(e); 459 } 460 } 461 462 List <Highlight> currentHighlights = new ArrayList <Highlight>(); 463 464 for (ErrorDescription e : filteredDescriptions) { 465 Highlight h = new DefaultHighlight(c, e.getRange().getBegin().getPosition(), e.getRange().getEnd().getPosition()); 466 467 OUT: for (Iterator <Highlight> it = currentHighlights.iterator(); it.hasNext() && h != null; ) { 468 Highlight hl = it.next(); 469 470 switch (detectCollisions(hl, h)) { 471 case 0: 472 break; 473 case 1: 474 it.remove(); 475 break; 476 case 2: 477 h = null; break OUT; 479 case 4: 480 case 3: 481 int start = Math.min(hl.getStart(), h.getStart()); 482 int end = Math.max(hl.getEnd(), h.getEnd()); 483 484 h = new DefaultHighlight(c, doc.createPosition(start), doc.createPosition(end)); 485 it.remove(); 486 break; 487 } 488 } 489 490 if (h != null) { 491 currentHighlights.add(h); 492 } 493 } 494 495 OUTER: while (!currentHighlights.isEmpty()) { 496 for (Iterator <Highlight> lowerIt = currentHighlights.iterator(); lowerIt.hasNext(); ) { 497 Highlight current = lowerIt.next(); 498 499 lowerIt.remove(); 500 501 for (Iterator <Highlight> higherIt = highlights.iterator(); higherIt.hasNext() && current != null; ) { 502 Highlight higher = higherIt.next(); 503 504 switch (detectCollisions(higher, current)) { 505 case 0: 506 break; 508 case 1: 509 { 510 int currentStart = higher.getEnd() + 1; 511 int currentEnd = higher.getStart() - 1; 512 513 if (currentStart < doc.getLength() && currentStart < current.getEnd()) { 514 currentHighlights.add(new DefaultHighlight(current.getColoring(), doc.createPosition(currentStart), doc.createPosition(current.getEnd()))); 515 } 516 517 if (currentEnd < doc.getLength() && current.getStart() < currentEnd) { 518 currentHighlights.add(new DefaultHighlight(current.getColoring(), doc.createPosition(current.getStart()), doc.createPosition(currentEnd))); 519 } 520 continue OUTER; 521 } 522 case 2: 523 current = null; 524 break; 525 case 3: 526 int currentStart = higher.getEnd() + 1; 527 528 if (currentStart < doc.getLength() && currentStart < current.getEnd()) { 529 current = new DefaultHighlight(current.getColoring(), doc.createPosition(currentStart), doc.createPosition(current.getEnd())); 530 } else { 531 current = null; 532 } 533 break; 534 case 4: 535 int currentEnd = higher.getStart() - 1; 536 537 if (currentEnd < doc.getLength() && current.getStart() < currentEnd) { 538 current = new DefaultHighlight(current.getColoring(), doc.createPosition(current.getStart()), doc.createPosition(currentEnd)); 539 } else { 540 current = null; 541 } 542 break; 543 } 544 } 545 546 if (current != null) { 547 highlights.add(current); 548 } 549 } 550 } 551 } 552 } 553 554 private static int detectCollisions(Highlight h1, Highlight h2) { 555 if (h2.getEnd() < h1.getStart()) 556 return 0; if (h1.getEnd() < h2.getStart()) 558 return 0; if (h2.getStart() < h1.getStart() && h2.getEnd() > h1.getEnd()) 560 return 1; if (h1.getStart() < h2.getStart() && h1.getEnd() > h2.getEnd()) 562 return 2; 564 if (h1.getStart() < h2.getStart()) 565 return 3; else 567 return 4; 568 } 569 570 public synchronized void setErrorDescriptions(final String layer, final Collection <? extends ErrorDescription> errors) { 571 doc.render(new Runnable () { 572 public void run() { 573 try { 574 setErrorDescriptionsImpl(file, layer, errors); 575 } catch (IOException e) { 576 Logger.getLogger("global").log(Level.WARNING, e.getMessage(), e); 577 } 578 } 579 }); 580 } 581 582 private synchronized void setErrorDescriptionsImpl(FileObject file, String layer, Collection <? extends ErrorDescription> errors) throws IOException { 583 long start = System.currentTimeMillis(); 584 585 try { 586 if (file == null) 587 return ; 588 589 List <ErrorDescription> layersErrors = getErrorsForLayer(layer); 590 591 Set <Integer > primaryLines = new HashSet <Integer >(); 592 Set <Integer > allLines = new HashSet <Integer >(); 593 594 for (ErrorDescription ed : layersErrors) { 595 List <Integer > lines = errors2Lines.remove(ed); 596 assert lines != null; 597 598 boolean first = true; 599 600 for (Integer line : lines) { 601 getErrorsForLine(line).remove(ed); 602 603 if (first) { 604 primaryLines.add(line); 605 } 606 607 allLines.add(line); 608 first = false; 609 } 610 } 611 612 List <ErrorDescription> validatedErrors = new ArrayList <ErrorDescription>(); 613 614 for (ErrorDescription ed : errors) { 615 if (ed.getRange() == null) 616 continue; 617 618 validatedErrors.add(ed); 619 620 List <Integer > lines = new ArrayList <Integer >(); 621 int startLine = ed.getRange().getBegin().getLine(); 622 int endLine = ed.getRange().getEnd().getLine(); 623 624 for (int cntr = startLine; cntr <= endLine; cntr++) { 625 lines.add(cntr); 626 } 627 628 errors2Lines.put(ed, lines); 629 630 boolean first = true; 631 632 for (Integer line : lines) { 633 getErrorsForLine(line).add(ed); 634 635 if (first) { 636 primaryLines.add(line); 637 } 638 639 allLines.add(line); 640 first = false; 641 } 642 } 643 644 layersErrors.clear(); 645 layersErrors.addAll(validatedErrors); 646 647 for (Integer line : primaryLines) { 648 updateAnnotationOnLine(line); 649 } 650 651 for (Integer line : allLines) { 652 updateHighlightsOnLine(line); 653 } 654 655 doUpdateHighlight(file); 656 657 updateVisibleRanges(); 658 } catch (BadLocationException ex) { 659 throw (IOException ) new IOException ().initCause(ex); 660 } finally { 661 long end = System.currentTimeMillis(); 662 TimesCollector.getDefault().reportTime(file, "annotation-holder-" + layer, "Errors update for " + layer, end - start); 663 } 664 } 665 666 public synchronized boolean hasErrors() { 667 for (ErrorDescription e : errors2Lines.keySet()) { 668 if (e.getSeverity() == Severity.ERROR) 669 return true; 670 } 671 672 return false; 673 } 674 675 public synchronized List <ErrorDescription> getErrors() { 676 return new ArrayList <ErrorDescription>(errors2Lines.keySet()); 677 } 678 679 public synchronized List <Annotation> getAnnotations() { 680 return new ArrayList <Annotation>(line2Annotations.values()); 681 } 682 683 private static final RequestProcessor INSTANCE = new RequestProcessor("AnnotationHolder"); 684 685 } 686 | Popular Tags |