source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/ParserManager.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 18.6 KB
Line 
1/*
2 * 09/26/2005
3 *
4 * ParserManager.java - Manages the parsing of an RSyntaxTextArea's document,
5 * if necessary.
6 *
7 * This library is distributed under a modified BSD license. See the included
8 * RSyntaxTextArea.License.txt file for details.
9 */
10package org.fife.ui.rsyntaxtextarea;
11
12import java.awt.Color;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.awt.event.MouseEvent;
16import java.net.URL;
17import java.security.AccessControlException;
18import java.util.ArrayList;
19import java.util.Iterator;
20import java.util.List;
21import javax.swing.Timer;
22import javax.swing.ToolTipManager;
23import javax.swing.event.DocumentEvent;
24import javax.swing.event.DocumentListener;
25import javax.swing.event.HyperlinkEvent;
26import javax.swing.event.HyperlinkListener;
27import javax.swing.text.BadLocationException;
28import javax.swing.text.Element;
29import javax.swing.text.Position;
30
31import org.fife.ui.rsyntaxtextarea.focusabletip.FocusableTip;
32import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
33import org.fife.ui.rsyntaxtextarea.parser.Parser;
34import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
35import org.fife.ui.rsyntaxtextarea.parser.ToolTipInfo;
36
37
38
39/**
40 * Manages running a parser object for an <code>RSyntaxTextArea</code>.
41 *
42 * @author Robert Futrell
43 * @version 0.9
44 */
45class ParserManager implements DocumentListener, ActionListener,
46 HyperlinkListener {
47
48 private RSyntaxTextArea textArea;
49 private List parsers;
50 private Timer timer;
51 private boolean running;
52 private Parser parserForTip;
53 private Position firstOffsetModded;
54 private Position lastOffsetModded;
55
56 /**
57 * Mapping of notices to their highlights in the editor. Can't use a Map
58 * since parsers could return two <code>ParserNotice</code>s that compare
59 * equally via <code>equals()</code>. Real-world example: The Perl
60 * compiler will return 2+ identical error messages if the same error is
61 * committed in a single line more than once.
62 */
63 private List noticeHighlightPairs;
64
65 /**
66 * Painter used to underline errors.
67 */
68 private SquiggleUnderlineHighlightPainter parserErrorHighlightPainter =
69 new SquiggleUnderlineHighlightPainter(Color.RED);
70
71 /**
72 * If this system property is set to <code>true</code>, debug messages
73 * will be printed to stdout to help diagnose parsing issues.
74 */
75 private static final String PROPERTY_DEBUG_PARSING = "rsta.debugParsing";
76
77 /**
78 * Whether to print debug messages while running parsers.
79 */
80 private static final boolean DEBUG_PARSING;
81
82 /**
83 * The default delay between the last key press and when the document
84 * is parsed, in milliseconds.
85 */
86 private static final int DEFAULT_DELAY_MS = 1250;
87
88
89 /**
90 * Constructor.
91 *
92 * @param textArea The text area whose document the parser will be
93 * parsing.
94 */
95 public ParserManager(RSyntaxTextArea textArea) {
96 this(DEFAULT_DELAY_MS, textArea);
97 }
98
99
100 /**
101 * Constructor.
102 *
103 * @param delay The delay between the last key press and when the document
104 * is parsed.
105 * @param textArea The text area whose document the parser will be
106 * parsing.
107 */
108 public ParserManager(int delay, RSyntaxTextArea textArea) {
109 this.textArea = textArea;
110 textArea.getDocument().addDocumentListener(this);
111 parsers = new ArrayList(1); // Usually small
112 timer = new Timer(delay, this);
113 timer.setRepeats(false);
114 running = true;
115 }
116
117
118 /**
119 * Called when the timer fires (e.g. it's time to parse the document).
120 *
121 * @param e The event.
122 */
123 public void actionPerformed(ActionEvent e) {
124
125 // Sanity check - should have >1 parser if event is fired.
126 int parserCount = getParserCount();
127 if (parserCount==0) {
128 return;
129 }
130
131 long begin = 0;
132 if (DEBUG_PARSING) {
133 begin = System.currentTimeMillis();
134 }
135
136 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
137
138 Element root = doc.getDefaultRootElement();
139 int firstLine = firstOffsetModded==null ? 0 : root.getElementIndex(firstOffsetModded.getOffset());
140 int lastLine = lastOffsetModded==null ? root.getElementCount()-1 : root.getElementIndex(lastOffsetModded.getOffset());
141 firstOffsetModded = lastOffsetModded = null;
142 if (DEBUG_PARSING) {
143 System.out.println("[DEBUG]: Minimum lines to parse: " + firstLine + "-" + lastLine);
144 }
145
146 String style = textArea.getSyntaxEditingStyle();
147 doc.readLock();
148 try {
149 for (int i=0; i<parserCount; i++) {
150 Parser parser = getParser(i);
151 if (parser.isEnabled()) {
152 ParseResult res = parser.parse(doc, style);
153 addParserNoticeHighlights(res);
154 }
155 else {
156 clearParserNoticeHighlights(parser);
157 }
158 }
159 textArea.fireParserNoticesChange();
160 } finally {
161 doc.readUnlock();
162 }
163
164 if (DEBUG_PARSING) {
165 float time = (System.currentTimeMillis()-begin)/1000f;
166 System.out.println("Total parsing time: " + time + " seconds");
167 }
168
169 }
170
171
172 /**
173 * Adds a parser for the text area.
174 *
175 * @param parser The new parser. If this is <code>null</code>, nothing
176 * happens.
177 * @see #getParser(int)
178 * @see #removeParser(Parser)
179 */
180 public void addParser(Parser parser) {
181 if (parser!=null && !parsers.contains(parser)) {
182 if (running) {
183 timer.stop();
184 }
185 parsers.add(parser);
186 if (parsers.size()==1) {
187 // Okay to call more than once.
188 ToolTipManager.sharedInstance().registerComponent(textArea);
189 }
190 if (running) {
191 timer.restart();
192 }
193 }
194 }
195
196
197 /**
198 * Adds highlights for a list of parser notices. Any current notices
199 * from the same Parser, in the same parsed range, are removed.
200 *
201 * @param res The result of a parsing.
202 * @see #clearParserNoticeHighlights()
203 */
204 private void addParserNoticeHighlights(ParseResult res) {
205
206 // Parsers are supposed to return at least empty ParseResults, but
207 // we'll be defensive here.
208 if (res==null) {
209 return;
210 }
211
212 if (DEBUG_PARSING) {
213 System.out.println("[DEBUG]: Adding parser notices from " +
214 res.getParser());
215 }
216
217 if (noticeHighlightPairs==null) {
218 noticeHighlightPairs = new ArrayList();
219 }
220
221 removeParserNotices(res);
222
223 List notices = res.getNotices();
224 if (notices.size()>0) { // Guaranteed non-null
225
226 RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
227 textArea.getHighlighter();
228
229 for (Iterator i=notices.iterator(); i.hasNext(); ) {
230 ParserNotice notice = (ParserNotice)i.next();
231 if (DEBUG_PARSING) {
232 System.out.println("[DEBUG]: ... adding: " + notice);
233 }
234 try {
235 Object highlight = null;
236 if (notice.getShowInEditor()) {
237 highlight = h.addParserHighlight(notice,
238 parserErrorHighlightPainter);
239 }
240 noticeHighlightPairs.add(new NoticeHighlightPair(notice, highlight));
241 } catch (BadLocationException ble) { // Never happens
242 ble.printStackTrace();
243 }
244 }
245
246 }
247
248 if (DEBUG_PARSING) {
249 System.out.println("[DEBUG]: Done adding parser notices from " +
250 res.getParser());
251 }
252
253 }
254
255
256 /**
257 * Called when the document is modified.
258 *
259 * @param e The document event.
260 */
261 public void changedUpdate(DocumentEvent e) {
262 }
263
264
265 private void clearParserNoticeHighlights() {
266 RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
267 textArea.getHighlighter();
268 if (h!=null) {
269 h.clearParserHighlights();
270 }
271 if (noticeHighlightPairs!=null) {
272 noticeHighlightPairs.clear();
273 }
274 }
275
276
277 /**
278 * Removes all parser notice highlights for a specific parser.
279 *
280 * @param parser The parser whose highlights to remove.
281 */
282 private void clearParserNoticeHighlights(Parser parser) {
283 RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
284 textArea.getHighlighter();
285 if (h!=null) {
286 h.clearParserHighlights(parser);
287 }
288 if (noticeHighlightPairs!=null) {
289 for (Iterator i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
290 NoticeHighlightPair pair = (NoticeHighlightPair)i.next();
291 if (pair.notice.getParser()==parser) {
292 i.remove();
293 }
294 }
295 }
296 }
297
298
299 /**
300 * Removes all parsers and any highlights they have created.
301 *
302 * @see #addParser(Parser)
303 */
304 public void clearParsers() {
305 timer.stop();
306 clearParserNoticeHighlights();
307 parsers.clear();
308 textArea.fireParserNoticesChange();
309 }
310
311
312 /**
313 * Forces the given {@link Parser} to re-parse the content of this text
314 * area.<p>
315 *
316 * This method can be useful when a <code>Parser</code> can be configured
317 * as to what notices it returns. For example, if a Java language parser
318 * can be configured to set whether no serialVersionUID is a warning,
319 * error, or ignored, this method can be called after changing the expected
320 * notice type to have the document re-parsed.
321 *
322 * @param parser The index of the <code>Parser</code> to re-run.
323 * @see #getParser(int)
324 */
325 public void forceReparsing(int parser) {
326 Parser p = getParser(parser);
327 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
328 String style = textArea.getSyntaxEditingStyle();
329 doc.readLock();
330 try {
331 if (p.isEnabled()) {
332 ParseResult res = p.parse(doc, style);
333 addParserNoticeHighlights(res);
334 }
335 else {
336 clearParserNoticeHighlights(p);
337 }
338 textArea.fireParserNoticesChange();
339 } finally {
340 doc.readUnlock();
341 }
342 }
343
344
345 /**
346 * Returns the delay between the last "concurrent" edit and when the
347 * document is re-parsed.
348 *
349 * @return The delay, in milliseconds.
350 * @see #setDelay(int)
351 */
352 public int getDelay() {
353 return timer.getDelay();
354 }
355
356
357 /**
358 * Returns the specified parser.
359 *
360 * @param index The index of the parser.
361 * @return The parser.
362 * @see #getParserCount()
363 * @see #addParser(Parser)
364 * @see #removeParser(Parser)
365 */
366 public Parser getParser(int index) {
367 return (Parser)parsers.get(index);
368 }
369
370
371 /**
372 * Returns the number of registered parsers.
373 *
374 * @return The number of registered parsers.
375 */
376 public int getParserCount() {
377 return parsers.size();
378 }
379
380
381 /**
382 * Returns a list of the current parser notices for this text area.
383 * This method (like most Swing methods) should only be called on the
384 * EDT.
385 *
386 * @return The list of notices. This will be an empty list if there are
387 * none.
388 */
389 public List getParserNotices() {
390 List notices = new ArrayList();
391 if (noticeHighlightPairs!=null) {
392 for (Iterator i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
393 NoticeHighlightPair pair = (NoticeHighlightPair)i.next();
394 notices.add(pair.notice);
395 }
396 }
397 return notices;
398 }
399
400
401 /**
402 * Returns the tool tip to display for a mouse event at the given
403 * location. This method is overridden to give a registered parser a
404 * chance to display a tool tip (such as an error description when the
405 * mouse is over an error highlight).
406 *
407 * @param e The mouse event.
408 * @return The tool tip to display, and possibly a hyperlink event handler.
409 */
410 public ToolTipInfo getToolTipText(MouseEvent e) {
411
412 String tip = null;
413 HyperlinkListener listener = null;
414 parserForTip = null;
415
416// try {
417 int pos = textArea.viewToModel(e.getPoint());
418 /*
419 Highlighter.Highlight[] highlights = textArea.getHighlighter().
420 getHighlights();
421 for (int i=0; i<highlights.length; i++) {
422 Highlighter.Highlight h = highlights[i];
423 //if (h instanceof ParserNoticeHighlight) {
424 // ParserNoticeHighlight pnh = (ParserNoticeHighlight)h;
425 int start = h.getStartOffset();
426 int end = h.getEndOffset();
427 if (start<=pos && end>=pos) {
428 //return pnh.getMessage();
429 return textArea.getText(start, end-start);
430 }
431 //}
432 }
433 */
434 if (noticeHighlightPairs!=null) {
435 for (int j=0; j<noticeHighlightPairs.size(); j++) {
436 NoticeHighlightPair pair =
437 (NoticeHighlightPair)noticeHighlightPairs.get(j);
438 ParserNotice notice = pair.notice;
439 if (notice.containsPosition(pos)) {
440 tip = notice.getToolTipText();
441 parserForTip = notice.getParser();
442 if (parserForTip instanceof HyperlinkListener) {
443 listener = (HyperlinkListener)parserForTip;
444 }
445 break;
446 }
447 }
448 }
449// } catch (BadLocationException ble) {
450// ble.printStackTrace(); // Should never happen.
451// }
452
453 URL imageBase = parserForTip==null ? null : parserForTip.getImageBase();
454 return new ToolTipInfo(tip, listener, imageBase);
455
456 }
457
458
459 /**
460 * Called when the document is modified.
461 *
462 * @param e The document event.
463 */
464 public void handleDocumentEvent(DocumentEvent e) {
465 if (running && parsers.size()>0) {
466 timer.restart();
467 }
468 }
469
470
471 /**
472 * Called when the user clicks a hyperlink in a {@link FocusableTip}.
473 *
474 * @param e The event.
475 */
476 public void hyperlinkUpdate(HyperlinkEvent e) {
477 if (parserForTip!=null && parserForTip.getHyperlinkListener()!=null) {
478 parserForTip.getHyperlinkListener().linkClicked(textArea, e);
479 }
480 }
481
482
483 /**
484 * Called when the document is modified.
485 *
486 * @param e The document event.
487 */
488 public void insertUpdate(DocumentEvent e) {
489
490 // Keep track of the first and last offset modified. Some parsers are
491 // smart and will only re-parse this section of the file.
492 try {
493 int offs = e.getOffset();
494 if (firstOffsetModded==null || offs<firstOffsetModded.getOffset()) {
495 firstOffsetModded = e.getDocument().createPosition(offs);
496 }
497 offs = e.getOffset() + e.getLength();
498 if (lastOffsetModded==null || offs>lastOffsetModded.getOffset()) {
499 lastOffsetModded = e.getDocument().createPosition(offs);
500 }
501 } catch (BadLocationException ble) {
502 ble.printStackTrace(); // Shouldn't happen
503 }
504
505 handleDocumentEvent(e);
506
507 }
508
509
510 /**
511 * Removes a parser.
512 *
513 * @param parser The parser to remove.
514 * @return Whether the parser was found.
515 * @see #addParser(Parser)
516 * @see #getParser(int)
517 */
518 public boolean removeParser(Parser parser) {
519 removeParserNotices(parser);
520 boolean removed = parsers.remove(parser);
521 if (removed) {
522 textArea.fireParserNoticesChange();
523 }
524 return removed;
525 }
526
527
528 /**
529 * Removes all parser notices (and clears highlights in the editor) from
530 * a particular parser.
531 *
532 * @param parser The parser.
533 */
534 private void removeParserNotices(Parser parser) {
535 if (noticeHighlightPairs!=null) {
536 RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
537 textArea.getHighlighter();
538 for (Iterator i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
539 NoticeHighlightPair pair = (NoticeHighlightPair)i.next();
540 if (pair.notice.getParser()==parser && pair.highlight!=null) {
541 h.removeParserHighlight(pair.highlight);
542 i.remove();
543 }
544 }
545 }
546 }
547
548
549 /**
550 * Removes any currently stored notices (and the corresponding highlights
551 * from the editor) from the same Parser, and in the given line range,
552 * as in the results.
553 *
554 * @param res The results.
555 */
556 private void removeParserNotices(ParseResult res) {
557 if (noticeHighlightPairs!=null) {
558 RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
559 textArea.getHighlighter();
560 for (Iterator i=noticeHighlightPairs.iterator(); i.hasNext(); ) {
561 NoticeHighlightPair pair = (NoticeHighlightPair)i.next();
562 boolean removed = false;
563 if (shouldRemoveNotice(pair.notice, res)) {
564 if (pair.highlight!=null) {
565 h.removeParserHighlight(pair.highlight);
566 }
567 i.remove();
568 removed = true;
569 }
570 if (DEBUG_PARSING) {
571 String text = removed ? "[DEBUG]: ... notice removed: " :
572 "[DEBUG]: ... notice not removed: ";
573 System.out.println(text + pair.notice);
574 }
575 }
576
577 }
578
579 }
580
581
582 /**
583 * Called when the document is modified.
584 *
585 * @param e The document event.
586 */
587 public void removeUpdate(DocumentEvent e) {
588
589 // Keep track of the first and last offset modified. Some parsers are
590 // smart and will only re-parse this section of the file. Note that
591 // for removals, only the line at the removal start needs to be
592 // re-parsed.
593 try {
594 int offs = e.getOffset();
595 if (firstOffsetModded==null || offs<firstOffsetModded.getOffset()) {
596 firstOffsetModded = e.getDocument().createPosition(offs);
597 }
598 if (lastOffsetModded==null || offs>lastOffsetModded.getOffset()) {
599 lastOffsetModded = e.getDocument().createPosition(offs);
600 }
601 } catch (BadLocationException ble) { // Never happens
602 ble.printStackTrace();
603 }
604
605 handleDocumentEvent(e);
606
607 }
608
609
610 /**
611 * Restarts parsing the document.
612 *
613 * @see #stopParsing()
614 */
615 public void restartParsing() {
616 timer.restart();
617 running = true;
618 }
619
620
621 /**
622 * Sets the delay between the last "concurrent" edit and when the document
623 * is re-parsed.
624 *
625 * @param millis The new delay, in milliseconds. This must be greater
626 * than <code>0</code>.
627 * @see #getDelay()
628 */
629 public void setDelay(int millis) {
630 if (running) {
631 timer.stop();
632 }
633 timer.setDelay(millis);
634 if (running) {
635 timer.start();
636 }
637 }
638
639
640 /**
641 * Returns whether a parser notice should be removed, based on a parse
642 * result.
643 *
644 * @param notice The notice in question.
645 * @param res The result.
646 * @return Whether the notice should be removed.
647 */
648 private final boolean shouldRemoveNotice(ParserNotice notice,
649 ParseResult res) {
650
651 if (DEBUG_PARSING) {
652 System.out.println("[DEBUG]: ... ... shouldRemoveNotice " +
653 notice + ": " + (notice.getParser()==res.getParser()));
654 }
655
656 // NOTE: We must currently remove all notices for the parser. Parser
657 // implementors are required to parse the entire document each parsing
658 // request, as RSTA is not yet sophisticated enough to determine the
659 // minimum range of text to parse (and ParserNotices' locations aren't
660 // updated when the Document is mutated, which would be a requirement
661 // for this as well).
662 // return same_parser && (in_reparsed_range || in_deleted_end_of_doc)
663 return notice.getParser()==res.getParser();
664
665 }
666
667
668 /**
669 * Stops parsing the document.
670 *
671 * @see #restartParsing()
672 */
673 public void stopParsing() {
674 timer.stop();
675 running = false;
676 }
677
678
679 /**
680 * Mapping of a parser notice to its highlight in the editor.
681 */
682 private static class NoticeHighlightPair {
683
684 public ParserNotice notice;
685 public Object highlight;
686
687 public NoticeHighlightPair(ParserNotice notice, Object highlight) {
688 this.notice = notice;
689 this.highlight = highlight;
690 }
691
692 }
693
694
695 static {
696 boolean debugParsing = false;
697 try {
698 debugParsing = Boolean.getBoolean(PROPERTY_DEBUG_PARSING);
699 } catch (AccessControlException ace) {
700 // Likely an applet's security manager.
701 debugParsing = false; // FindBugs
702 }
703 DEBUG_PARSING = debugParsing;
704 }
705
706
707}
Note: See TracBrowser for help on using the repository browser.