001/*
002 * JEditTextArea.java - jEdit's text component
003 * Copyright (C) 1999 Slava Pestov
004 * Copyright (C) 2000-2010 The Gridarta Developers.
005 *
006 * You may use and modify this package for any purpose. Redistribution is
007 * permitted, in both source and binary form, provided that this notice
008 * remains intact in all source distributions of this package.
009 */
010
011package net.sf.gridarta.textedit.textarea;
012
013import java.awt.AWTEvent;
014import java.awt.Adjustable;
015import java.awt.Font;
016import java.awt.FontMetrics;
017import java.awt.datatransfer.Clipboard;
018import java.awt.datatransfer.DataFlavor;
019import java.awt.datatransfer.StringSelection;
020import java.awt.datatransfer.UnsupportedFlavorException;
021import java.awt.event.ActionEvent;
022import java.awt.event.ActionListener;
023import java.awt.event.AdjustmentEvent;
024import java.awt.event.AdjustmentListener;
025import java.awt.event.ComponentAdapter;
026import java.awt.event.ComponentEvent;
027import java.awt.event.FocusEvent;
028import java.awt.event.FocusListener;
029import java.awt.event.InputEvent;
030import java.awt.event.KeyEvent;
031import java.awt.event.MouseAdapter;
032import java.awt.event.MouseEvent;
033import java.awt.event.MouseMotionListener;
034import java.awt.event.MouseWheelEvent;
035import java.io.IOException;
036import java.lang.reflect.Field;
037import java.lang.reflect.InvocationTargetException;
038import java.lang.reflect.Method;
039import java.util.HashSet;
040import java.util.List;
041import java.util.Set;
042import javax.swing.JComponent;
043import javax.swing.JPopupMenu;
044import javax.swing.JScrollBar;
045import javax.swing.KeyStroke;
046import javax.swing.SwingUtilities;
047import javax.swing.Timer;
048import javax.swing.event.CaretEvent;
049import javax.swing.event.DocumentEvent;
050import javax.swing.event.DocumentListener;
051import javax.swing.text.BadLocationException;
052import javax.swing.text.Element;
053import javax.swing.text.Segment;
054import javax.swing.text.Utilities;
055import javax.swing.undo.AbstractUndoableEdit;
056import javax.swing.undo.CannotRedoException;
057import javax.swing.undo.CannotUndoException;
058import javax.swing.undo.UndoableEdit;
059import net.sf.gridarta.textedit.textarea.tokenmarker.TokenMarker;
060import org.apache.log4j.Category;
061import org.apache.log4j.Logger;
062import org.jetbrains.annotations.NotNull;
063import org.jetbrains.annotations.Nullable;
064
065/**
066 * jEdit's text area component. It is more suited for editing program source
067 * code than JEditorPane, because it drops the unnecessary features (images,
068 * variable-width lines, and so on) and adds a whole bunch of useful goodies
069 * such as: <ul> <li>More flexible key binding scheme <li>Supports macro
070 * recorders <li>Rectangular selection <li>Bracket highlighting <li>Syntax
071 * highlighting <li>Command repetition <li>Block caret can be enabled </ul> It
072 * is also faster and doesn't have as many problems. It can be used in other
073 * applications; the only other part of jEdit it depends on is the syntax
074 * package.<p> <p/> To use it in your app, treat it like any other component,
075 * for example:
076 * <pre>JEditTextArea ta = new JEditTextArea();
077 * ta.setTokenMarker(new JavaTokenMarker());
078 * ta.setText("public class Test {\n"
079 *     + "    public static void main(String[] args) {\n"
080 *     + "        System.err.println(\"Hello World\");\n"
081 *     + "    }\n"
082 *     + "}");</pre>
083 * @author Slava Pestov
084 * @author <a href="mailto:andi.vogl@gmx.net">Andreas Vogl</a>
085 * @author Andreas Kirschbaum
086 */
087public class JEditTextArea extends JComponent {
088
089    /**
090     * The Logger for printing log messages.
091     */
092    private static final Category log = Logger.getLogger(JEditTextArea.class);
093
094    /**
095     * Serial Version UID.
096     */
097    private static final long serialVersionUID = 1L;
098
099    /**
100     * The text contents in the last "unmodified" state.
101     */
102    @NotNull
103    private String unmodifiedText = "";
104
105    @Nullable
106    private static JEditTextArea focusedComponent;
107
108    @NotNull
109    private static final Timer caretTimer = new Timer(500, new CaretBlinker());
110
111    static {
112        caretTimer.setInitialDelay(500);
113        caretTimer.start();
114    }
115
116    @NotNull
117    private final TextAreaPainter painter;
118
119    @Nullable
120    private final JPopupMenu popup;
121
122    private final boolean caretBlinks;
123
124    private boolean caretVisible;
125
126    private boolean blink;
127
128    private final boolean editable;
129
130    private int firstLine;
131
132    private int visibleLines;
133
134    private final int electricScroll;
135
136    private int horizontalOffset;
137
138    @NotNull
139    private final JScrollBar vertical = new JScrollBar(Adjustable.VERTICAL);
140
141    @NotNull
142    private final JScrollBar horizontal = new JScrollBar(Adjustable.HORIZONTAL);
143
144    private boolean scrollBarsInitialized;
145
146    @NotNull
147    private final InputHandler inputHandler;
148
149    @Nullable
150    private SyntaxDocument document;
151
152    @NotNull
153    private final DocumentListener documentHandler;
154
155    @NotNull
156    private final Segment lineSegment;
157
158    private int selectionStart;
159
160    private int selectionStartLine;
161
162    private int selectionEnd;
163
164    private int selectionEndLine;
165
166    private boolean biasLeft;
167
168    private int bracketPosition;
169
170    private int bracketLine;
171
172    private int magicCaret;
173
174    private boolean overwrite;
175
176    private boolean rectangleSelect;
177
178    /**
179     * Creates a new JEditTextArea with the specified settings.
180     * @param defaults the default settings
181     */
182    public JEditTextArea(@NotNull final TextAreaDefaults defaults) {
183        // Enable the necessary events
184        enableEvents(AWTEvent.KEY_EVENT_MASK);
185
186        // Initialize some misc. stuff
187        painter = new TextAreaPainter(this, defaults);
188        documentHandler = new DocumentHandler();
189        lineSegment = new Segment();
190        bracketLine = -1;
191        bracketPosition = -1;
192        blink = true;
193
194        // Initialize the GUI
195        setLayout(new ScrollLayout(this));
196        add(ScrollLayout.CENTER, painter);
197        add(ScrollLayout.RIGHT, vertical);
198        add(ScrollLayout.BOTTOM, horizontal);
199
200        // Add some event listeners
201        vertical.addAdjustmentListener(new AdjustHandler());
202        horizontal.addAdjustmentListener(new AdjustHandler());
203        painter.addComponentListener(new ComponentHandler());
204        final MouseHandler mouseHandler = new MouseHandler();
205        painter.addMouseListener(mouseHandler);
206        painter.addMouseWheelListener(mouseHandler);
207        painter.addMouseMotionListener(new DragHandler());
208        addFocusListener(new FocusHandler());
209
210        // Load the defaults
211        inputHandler = defaults.getInputHandler();
212        setDocument(defaults.getDocument());
213        editable = defaults.getEditable();
214        caretVisible = defaults.getCaretVisible();
215        caretBlinks = defaults.getCaretBlinks();
216        electricScroll = defaults.getElectricScroll();
217
218        popup = defaults.getPopup();
219
220        // free tab key from the focus traversal manager
221        freeTabKeyFromFocusTraversal();
222
223        // We don't seem to get the initial focus event?
224        focusedComponent = this;
225    }
226
227    /**
228     * In JDKs above 1.4, the tab key is used for focus traversal. That means
229     * the tab key normally does not "work" inside the text area. But that would
230     * be a pity, because we need the tab key for indentation. <p/> So what this
231     * method does is setting the focus traversal to "tab + &lt;control&gt;", in
232     * order to "free" the tab key from focus traversal events. In JDKs above
233     * 1.4, this task can be accomplished simply by three lines of code: <code>
234     * Set forwardTraversalKeys = new HashSet(); forwardTraversalKeys.add(KeyStroke.getKeyStroke("control
235     * TAB")); setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
236     * forwardTraversalKeys); </code> <p/> Now, what's the big deal there? The
237     * problem is that the class "KeyboardFocusManager" as well as the method
238     * "setFocusTraversalKeys" are undefined in JDKs 1.3.* and below. Hence, if
239     * above code was inserted as-is, this application would no longer compile
240     * with java 1.3. <p/> The solution to this problem is implemented here: The
241     * critical classes and methods are accessed through the reflection
242     * interface, which allows to execute them *if defined* but still compile
243     * *if undefined*.
244     */
245    private void freeTabKeyFromFocusTraversal() {
246        try {
247            // preparing the key set first, this should be harmless
248            final Set<KeyStroke> forwardTraversalKeys = new HashSet<KeyStroke>();
249            forwardTraversalKeys.add(KeyStroke.getKeyStroke("control TAB"));
250
251            // here we try to access java.awt.KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS
252            final Field field = Class.forName("java.awt.KeyboardFocusManager").getField("FORWARD_TRAVERSAL_KEYS");
253            final Integer value = field.getInt(field); // store the value of this field
254
255            for (final Method method : getClass().getMethods()) {
256                // here we try to find the method "setFocusTraversalKeys", and execute it if found
257                if (method.getName().equalsIgnoreCase("setFocusTraversalKeys")) {
258                    method.invoke(this, value, forwardTraversalKeys);
259                    // System.err.println("freeTabKeyFromFocusTraversal() succeeded!");
260                }
261            }
262        } catch (final ClassNotFoundException ignored) {
263            // ignore
264        } catch (final IllegalAccessException ignored) {
265            // ignore
266        } catch (final InvocationTargetException ignored) {
267            // ignore
268        } catch (final NoSuchFieldException ignored) {
269            // ignore
270        }
271    }
272
273    /**
274     * Set the TextArea font
275     * @param font font
276     */
277    @Override
278    public void setFont(@NotNull final Font font) {
279        painter.setFont(font);
280    }
281
282    /**
283     * Returns the object responsible for painting this text area.
284     */
285    @NotNull
286    public TextAreaPainter getPainter() {
287        return painter;
288    }
289
290    /**
291     * Returns the input handler.
292     */
293    @NotNull
294    public InputHandler getInputHandler() {
295        return inputHandler;
296    }
297
298    /**
299     * Returns true if the caret is visible, false otherwise.
300     */
301    public boolean isCaretVisible() {
302        return (!caretBlinks || blink) && caretVisible;
303    }
304
305    /**
306     * Sets if the caret should be visible.
307     * @param caretVisible true if the caret should be visible, false otherwise
308     */
309    public void setCaretVisible(final boolean caretVisible) {
310        this.caretVisible = caretVisible;
311        blink = true;
312
313        painter.invalidateSelectedLines();
314    }
315
316    /**
317     * Blinks the caret.
318     */
319    public void blinkCaret() {
320        if (caretBlinks) {
321            blink = !blink;
322            painter.invalidateSelectedLines();
323        } else {
324            blink = true;
325        }
326    }
327
328    /**
329     * Returns the number of lines from the top and button of the text area that
330     * are always visible.
331     */
332    public int getElectricScroll() {
333        return electricScroll;
334    }
335
336    /**
337     * Updates the state of the scroll bars. This should be called if the number
338     * of lines in the document changes, or when the size of the text are
339     * changes.
340     */
341    public void updateScrollBars() {
342        if (visibleLines != 0) {
343            vertical.setValues(firstLine, visibleLines, 0, getLineCount());
344            //vertical.setUnitIncrement(2);
345            vertical.setUnitIncrement(1); // scroll one line per click
346            vertical.setBlockIncrement(visibleLines);
347        }
348
349        final int width = painter.getWidth();
350        if (width != 0) {
351            horizontal.setValues(-horizontalOffset, width, 0, width * 5);
352            //horizontal.setUnitIncrement(painter.getFontMetrics().charWidth('w'));
353            horizontal.setUnitIncrement(painter.getDefaultCharWidth());
354            horizontal.setBlockIncrement(width / 2);
355        }
356    }
357
358    /**
359     * Returns the line displayed at the text area's origin.
360     */
361    public int getFirstLine() {
362        return firstLine;
363    }
364
365    /**
366     * Sets the line displayed at the text area's origin without updating the
367     * scroll bars.
368     */
369    public void setFirstLine(final int firstLine) {
370        if (firstLine == this.firstLine) {
371            return;
372        }
373
374        this.firstLine = firstLine;
375        if (firstLine != vertical.getValue()) {
376            updateScrollBars();
377        }
378        painter.repaint();
379    }
380
381    /**
382     * Returns the number of lines visible in this text area.
383     */
384    public int getVisibleLines() {
385        return visibleLines;
386    }
387
388    /**
389     * Recalculates the number of visible lines. This should not be called
390     * directly.
391     */
392    public void recalculateVisibleLines() {
393        if (painter == null) {
394            return;
395        }
396
397        final int height = painter.getHeight();
398
399        // get line height
400        final int lineHeight;
401        if (painter.getFontMetrics() == null) {
402            lineHeight = painter.getDefaultLineHeight(); // default height might be wrong, take it only when needed
403        } else {
404            lineHeight = painter.getFontMetrics().getHeight();
405        }
406
407        visibleLines = height / lineHeight;
408        updateScrollBars();
409    }
410
411    /**
412     * Returns the horizontal offset of drawn lines.
413     */
414    public int getHorizontalOffset() {
415        return horizontalOffset;
416    }
417
418    /**
419     * Sets the horizontal offset of drawn lines. This can be used to implement
420     * horizontal scrolling.
421     * @param horizontalOffset offset The new horizontal offset
422     */
423    public void setHorizontalOffset(final int horizontalOffset) {
424        if (horizontalOffset == this.horizontalOffset) {
425            return;
426        }
427
428        this.horizontalOffset = horizontalOffset;
429        if (horizontalOffset != horizontal.getValue()) {
430            updateScrollBars();
431        }
432
433        painter.repaint();
434    }
435
436    /**
437     * A fast way of changing both the first line and horizontal offset.
438     * @param firstLine the new first line
439     * @param horizontalOffset the new horizontal offset
440     */
441    public void setOrigin(final int firstLine, final int horizontalOffset) {
442        boolean changed = false;
443
444        if (horizontalOffset != this.horizontalOffset) {
445            this.horizontalOffset = horizontalOffset;
446            changed = true;
447        }
448
449        if (firstLine != this.firstLine) {
450            this.firstLine = firstLine;
451            changed = true;
452        }
453
454        if (changed) {
455            updateScrollBars();
456            painter.repaint();
457        }
458    }
459
460    /**
461     * Ensures that the caret is visible by scrolling the text area if
462     * necessary.
463     */
464    public void scrollToCaret() {
465        final int line = getCaretLine();
466        final int lineStart = getLineStartOffset(line);
467        final int offset = Math.max(0, Math.min(getLineLength(line) - 1, getCaretPosition() - lineStart));
468
469        scrollTo(line, offset);
470    }
471
472    /**
473     * Sets the focus to this TextArea, so this component is instantly
474     * registered for key press events. The graphics context must be fully
475     * initialized before calling this method.
476     */
477    public void setEditingFocus() {
478        try {
479            requestFocus();
480            setCaretVisible(true);
481            focusedComponent = this;
482            setCaretPosition(0); // set caret to 0, 0 coordinates
483        } catch (final NullPointerException e) {
484            log.error("Null Pointer Exception in JEditTextArea.setEditingFocus()");
485        }
486    }
487
488    /**
489     * Ensures that the specified line and offset is visible by scrolling the
490     * text area if necessary.
491     * @param line the line to scroll to
492     * @param offset the offset in the line to scroll to
493     */
494    public void scrollTo(final int line, final int offset) {
495        // visibleLines == 0 before the component is realized
496        // we can't do any proper scrolling then, so we have
497        // this hack...
498        if (visibleLines == 0) {
499            setFirstLine(Math.max(0, line - electricScroll));
500            return;
501        }
502
503        int newFirstLine = firstLine;
504        int newHorizontalOffset = horizontalOffset;
505
506        if (line < firstLine + electricScroll) {
507            newFirstLine = Math.max(0, line - electricScroll);
508        } else if (line + electricScroll >= firstLine + visibleLines) {
509            newFirstLine = (line - visibleLines) + electricScroll + 1;
510            if (newFirstLine + visibleLines >= getLineCount()) {
511                newFirstLine = getLineCount() - visibleLines;
512            }
513            if (newFirstLine < 0) {
514                newFirstLine = 0;
515            }
516        }
517
518        final int x = offsetToX2(line, offset);
519        final int width = painter.getFontMetrics().charWidth('w');
520
521        if (x < 0) {
522            newHorizontalOffset = Math.min(0, horizontalOffset - x + width + 5);
523        } else if (x + width >= painter.getWidth()) {
524            newHorizontalOffset = horizontalOffset + (painter.getWidth() - x) - width - 5;
525        }
526
527        setOrigin(newFirstLine, newHorizontalOffset);
528    }
529
530    /**
531     * Converts a line index to a y co-ordinate.
532     * @param line the line
533     */
534    public int lineToY(final int line) {
535        final FontMetrics fm = painter.getFontMetrics();
536        return (line - firstLine) * fm.getHeight() - (fm.getLeading() + fm.getMaxDescent());
537    }
538
539    /**
540     * Converts a y co-ordinate to a line index.
541     * @param y the y co-ordinate
542     */
543    public int yToLine(final int y) {
544        final FontMetrics fm = painter.getFontMetrics();
545        final int height = fm.getHeight();
546        return Math.max(0, Math.min(getLineCount() - 1, y / height + firstLine));
547    }
548
549    /**
550     * Converts an offset in a line into an x co-ordinate. This is a slow
551     * version that can be used any time.
552     * @param line the line
553     * @param offset the offset, from the start of the line
554     */
555    public int offsetToX(final int line, final int offset) {
556        // don't use cached tokens
557        painter.setCurrentLineTokens(null);
558        return offsetToX2(line, offset);
559    }
560
561    /**
562     * Converts an offset in a line into an x co-ordinate. This is a fast
563     * version that should only be used if no changes were made to the text
564     * since the last repaint.
565     * @param line the line
566     * @param offset the offset, from the start of the line
567     */
568    public int offsetToX2(final int line, final int offset) {
569        final TokenMarker tokenMarker = getTokenMarker();
570
571        /* Use painter's cached info for speed */
572        FontMetrics fm = painter.getFontMetrics();
573
574        getLineText(line, lineSegment);
575
576        final int segmentOffset = lineSegment.offset;
577        int x = horizontalOffset;
578
579        /* If syntax coloring is disabled, do simple translation */
580        if (tokenMarker == null) {
581            lineSegment.count = offset;
582            return x + Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0);
583        } else {
584            /* If syntax coloring is enabled, we have to do this because
585      * tokens can vary in width */
586            final List<Token> tokens;
587            if (painter.getCurrentLineIndex() == line && painter.getCurrentLineTokens() != null) {
588                tokens = painter.getCurrentLineTokens();
589            } else {
590                painter.setCurrentLineIndex(line);
591                tokens = tokenMarker.markTokens(lineSegment, line);
592                painter.setCurrentLineTokens(tokens);
593            }
594
595            final Font defaultFont = painter.getFont();
596            final SyntaxStyles styles = painter.getStyles();
597
598            for (final Token token : tokens) {
599                final byte id = token.getId();
600                if (id == Token.NULL) {
601                    fm = painter.getFontMetrics();
602                } else {
603                    fm = styles.getStyle(id).getFontMetrics(defaultFont, painter.getGraphics());
604                }
605
606                final int length = token.getLength();
607
608                if (offset + segmentOffset < lineSegment.offset + length) {
609                    lineSegment.count = offset - (lineSegment.offset - segmentOffset);
610                    return x + Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0);
611                }
612                lineSegment.count = length;
613                x += Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0);
614                lineSegment.offset += length;
615            }
616            return x;
617        }
618    }
619
620    /**
621     * Converts an x co-ordinate to an offset within a line.
622     * @param line the line
623     * @param x the x co-ordinate
624     */
625    public int xToOffset(final int line, final int x) {
626        final TokenMarker tokenMarker = getTokenMarker();
627
628        /* Use painter's cached info for speed */
629        FontMetrics fm = painter.getFontMetrics();
630
631        getLineText(line, lineSegment);
632
633        final char[] segmentArray = lineSegment.array;
634        final int segmentOffset = lineSegment.offset;
635        final int segmentCount = lineSegment.count;
636
637        int width = horizontalOffset;
638
639        if (tokenMarker == null) {
640            for (int i = 0; i < segmentCount; i++) {
641                final char c = segmentArray[i + segmentOffset];
642                final int charWidth;
643                if (c == '\t') {
644                    charWidth = (int) painter.nextTabStop((float) width, i) - width;
645                } else {
646                    charWidth = fm.charWidth(c);
647                }
648
649                if (painter.isBlockCaretEnabled()) {
650                    if (x - charWidth <= width) {
651                        return i;
652                    }
653                } else {
654                    if (x - charWidth / 2 <= width) {
655                        return i;
656                    }
657                }
658
659                width += charWidth;
660            }
661
662            return segmentCount;
663        } else {
664            final List<Token> tokens;
665            if (painter.getCurrentLineIndex() == line && painter.getCurrentLineTokens() != null) {
666                tokens = painter.getCurrentLineTokens();
667            } else {
668                painter.setCurrentLineIndex(line);
669                tokens = tokenMarker.markTokens(lineSegment, line);
670                painter.setCurrentLineTokens(tokens);
671            }
672
673            int offset = 0;
674            final Font defaultFont = painter.getFont();
675            final SyntaxStyles styles = painter.getStyles();
676
677            for (final Token token : tokens) {
678                final byte id = token.getId();
679                if (id == Token.NULL) {
680                    fm = painter.getFontMetrics();
681                } else {
682                    fm = styles.getStyle(id).getFontMetrics(defaultFont, painter.getGraphics());
683                }
684
685                final int length = token.getLength();
686
687                for (int i = 0; i < length; i++) {
688                    final char c = segmentArray[segmentOffset + offset + i];
689                    final int charWidth;
690                    if (c == '\t') {
691                        charWidth = (int) painter.nextTabStop((float) width, offset + i) - width;
692                    } else {
693                        charWidth = fm.charWidth(c);
694                    }
695
696                    if (painter.isBlockCaretEnabled()) {
697                        if (x - charWidth <= width) {
698                            return offset + i;
699                        }
700                    } else {
701                        if (x - charWidth / 2 <= width) {
702                            return offset + i;
703                        }
704                    }
705
706                    width += charWidth;
707                }
708
709                offset += length;
710            }
711            return offset;
712        }
713    }
714
715    /**
716     * Converts a point to an offset, from the start of the text.
717     * @param x the x co-ordinate of the point
718     * @param y the y co-ordinate of the point
719     */
720    public int xyToOffset(final int x, final int y) {
721        final int line = yToLine(y);
722        final int start = getLineStartOffset(line);
723        return start + xToOffset(line, x);
724    }
725
726    /**
727     * Returns the document this text area is editing.
728     */
729    @NotNull
730    @SuppressWarnings("NullableProblems")
731    public SyntaxDocument getDocument() {
732        if (document == null) {
733            throw new IllegalStateException();
734        }
735        return document;
736    }
737
738    /**
739     * Sets the document this text area is editing.
740     * @param document the document
741     */
742    public final void setDocument(@Nullable final SyntaxDocument document) {
743        if (this.document == document) {
744            return;
745        }
746
747        if (this.document != null) {
748            this.document.removeDocumentListener(documentHandler);
749        }
750
751        this.document = document;
752
753        document.addDocumentListener(documentHandler);
754
755        select(0, 0);
756        updateScrollBars();
757        painter.repaint();
758    }
759
760    /**
761     * Returns the document's token marker. Equivalent to calling
762     * <code>getDocument().getTokenMarker()</code>.
763     */
764    @Nullable
765    public TokenMarker getTokenMarker() {
766        return document.getTokenMarker();
767    }
768
769    /**
770     * Returns the length of the document. Equivalent to calling
771     * <code>getDocument().getLength()</code>.
772     */
773    public int getDocumentLength() {
774        return document.getLength();
775    }
776
777    /**
778     * Returns the number of lines in the document.
779     */
780    public int getLineCount() {
781        return document.getDefaultRootElement().getElementCount();
782    }
783
784    /**
785     * Returns the line containing the specified offset.
786     * @param offset the offset
787     */
788    public int getLineOfOffset(final int offset) {
789        return document.getDefaultRootElement().getElementIndex(offset);
790    }
791
792    /**
793     * Returns the start offset of the specified line.
794     * @param line the line
795     * @return the start offset of the specified line, or -1 if the line is
796     *         invalid
797     */
798    public int getLineStartOffset(final int line) {
799        final Element lineElement = document.getDefaultRootElement().getElement(line);
800        if (lineElement == null) {
801            return -1;
802        } else {
803            return lineElement.getStartOffset();
804        }
805    }
806
807    /**
808     * Returns the end offset of the specified line.
809     * @param line the line
810     * @return the end offset of the specified line, or -1 if the line is
811     *         invalid
812     */
813    public int getLineEndOffset(final int line) {
814        final Element lineElement = document.getDefaultRootElement().getElement(line);
815        if (lineElement == null) {
816            return -1;
817        } else {
818            return lineElement.getEndOffset();
819        }
820    }
821
822    /**
823     * Returns the length of the specified line.
824     * @param line the line
825     */
826    public int getLineLength(final int line) {
827        final Element lineElement = document.getDefaultRootElement().getElement(line);
828        if (lineElement == null) {
829            return -1;
830        } else {
831            return lineElement.getEndOffset() - lineElement.getStartOffset() - 1;
832        }
833    }
834
835    /**
836     * Returns the entire text of this text area.
837     */
838    @NotNull
839    public String getText() {
840        try {
841            return document.getText(0, document.getLength());
842        } catch (final BadLocationException bl) {
843            bl.printStackTrace();
844            return "";
845        }
846    }
847
848    /**
849     * Sets the entire text of this text area.
850     */
851    public void setText(@NotNull final String text) {
852        try {
853            SyntaxDocument.beginCompoundEdit();
854            document.remove(0, document.getLength());
855            document.insertString(0, text, null);
856        } catch (final BadLocationException bl) {
857            bl.printStackTrace();
858        } finally {
859            SyntaxDocument.endCompoundEdit();
860        }
861    }
862
863    /**
864     * Returns the specified substring of the document.
865     * @param start the start offset
866     * @param len the length of the substring
867     * @return the substring, or null if the offsets are invalid
868     */
869    @Nullable
870    public String getText(final int start, final int len) {
871        try {
872            return document.getText(start, len);
873        } catch (final BadLocationException bl) {
874            bl.printStackTrace();
875            return null;
876        }
877    }
878
879    /**
880     * Copies the specified substring of the document into a segment. If the
881     * offsets are invalid, the segment will contain a null string.
882     * @param start the start offset
883     * @param len the length of the substring
884     * @param segment the segment
885     */
886    public void getText(final int start, final int len, @NotNull final Segment segment) {
887        try {
888            document.getText(start, len, segment);
889        } catch (final BadLocationException bl) {
890            bl.printStackTrace();
891            segment.offset = 0;
892            segment.count = 0;
893        }
894    }
895
896    /**
897     * Returns the text on the specified line.
898     * @param lineIndex the line
899     * @return the text, or null if the line is invalid
900     */
901    @NotNull
902    public CharSequence getLineText(final int lineIndex) {
903        final int start = getLineStartOffset(lineIndex);
904        return getText(start, getLineEndOffset(lineIndex) - start - 1);
905    }
906
907    /**
908     * Copies the text on the specified line into a segment. If the line is
909     * invalid, the segment will contain a null string.
910     * @param lineIndex the line
911     */
912    public void getLineText(final int lineIndex, @NotNull final Segment segment) {
913        final int start = getLineStartOffset(lineIndex);
914        getText(start, getLineEndOffset(lineIndex) - start - 1, segment);
915    }
916
917    /**
918     * Returns the selection start offset.
919     */
920    public int getSelectionStart() {
921        return selectionStart;
922    }
923
924    /**
925     * Returns the selection start line.
926     */
927    public int getSelectionStartLine() {
928        return selectionStartLine;
929    }
930
931    /**
932     * Returns the selection end offset.
933     */
934    public int getSelectionEnd() {
935        return selectionEnd;
936    }
937
938    /**
939     * Returns the selection end line.
940     */
941    public int getSelectionEndLine() {
942        return selectionEndLine;
943    }
944
945    /**
946     * Returns the caret position. This will either be the selection start or
947     * the selection end, depending on which direction the selection was made
948     * in.
949     */
950    public int getCaretPosition() {
951        return biasLeft ? selectionStart : selectionEnd;
952    }
953
954    /**
955     * Returns the caret line.
956     */
957    public int getCaretLine() {
958        return biasLeft ? selectionStartLine : selectionEndLine;
959    }
960
961    /**
962     * Returns the mark position. This will be the opposite selection bound to
963     * the caret position.
964     * @see #getCaretPosition()
965     */
966    public int getMarkPosition() {
967        return biasLeft ? selectionEnd : selectionStart;
968    }
969
970    /**
971     * Sets the caret position. The new selection will consist of the caret
972     * position only (hence no text will be selected).
973     * @param caret the caret position
974     * @see #select(int, int)
975     */
976    public void setCaretPosition(final int caret) {
977        select(caret, caret);
978    }
979
980    /**
981     * Selects all text in the document.
982     */
983    public void selectAll() {
984        select(0, getDocumentLength());
985    }
986
987    /**
988     * Selects from the start offset to the end offset. This is the general
989     * selection method used by all other selecting methods. The caret position
990     * will be start if start &lt; end, and end if end &gt; start.
991     * @param start the start offset
992     * @param end the end offset
993     */
994    public void select(final int start, final int end) {
995        final int newStart;
996        final int newEnd;
997        final boolean newBias;
998        if (start <= end) {
999            newStart = start;
1000            newEnd = end;
1001            newBias = false;
1002        } else {
1003            newStart = end;
1004            newEnd = start;
1005            newBias = true;
1006        }
1007
1008        if (newStart < 0 || newEnd > getDocumentLength()) {
1009            throw new IllegalArgumentException("Bounds out of range: " + newStart + ", " + newEnd);
1010        }
1011
1012        // If the new position is the same as the old, we don't
1013        // do all this crap, however we still do the stuff at
1014        // the end (clearing magic position, scrolling)
1015        if (newStart != selectionStart || newEnd != selectionEnd || newBias != biasLeft) {
1016            final int newStartLine = getLineOfOffset(newStart);
1017            final int newEndLine = getLineOfOffset(newEnd);
1018
1019            if (painter.isBracketHighlightEnabled()) {
1020                if (bracketLine != -1) {
1021                    painter.invalidateLine(bracketLine);
1022                }
1023
1024                updateBracketHighlight(end);
1025                if (bracketLine != -1) {
1026                    painter.invalidateLine(bracketLine);
1027                }
1028            }
1029
1030            painter.invalidateLineRange(selectionStartLine, selectionEndLine);
1031            painter.invalidateLineRange(newStartLine, newEndLine);
1032
1033            SyntaxDocument.addUndoableEdit(new CaretUndo(selectionStart, selectionEnd));
1034
1035            selectionStart = newStart;
1036            selectionEnd = newEnd;
1037            selectionStartLine = newStartLine;
1038            selectionEndLine = newEndLine;
1039            biasLeft = newBias;
1040        }
1041
1042        // When the user is typing, etc, we don't want the caret
1043        // to blink
1044        blink = true;
1045        caretTimer.restart();
1046
1047        // Disable rectangle select if selection start = selection end
1048        if (selectionStart == selectionEnd) {
1049            rectangleSelect = false;
1050        }
1051
1052        // Clear the `magic' caret position used by up/down
1053        magicCaret = -1;
1054
1055        scrollToCaret();
1056    }
1057
1058    /**
1059     * Returns the selected text, or null if no selection is active.
1060     */
1061    @Nullable
1062    public String getSelectedText() {
1063        if (selectionStart == selectionEnd) {
1064            return null;
1065        }
1066
1067        if (rectangleSelect) {
1068            // Return each row of the selection on a new line
1069
1070            final Element map = document.getDefaultRootElement();
1071
1072            int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
1073            int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();
1074
1075            // Certain rectangles satisfy this condition...
1076            if (end < start) {
1077                final int tmp = end;
1078                end = start;
1079                start = tmp;
1080            }
1081
1082            final StringBuilder buf = new StringBuilder();
1083            final Segment seg = new Segment();
1084
1085            for (int i = selectionStartLine; i <= selectionEndLine; i++) {
1086                final Element lineElement = map.getElement(i);
1087                int lineStart = lineElement.getStartOffset();
1088                final int lineEnd = lineElement.getEndOffset() - 1;
1089
1090                lineStart = Math.min(lineStart + start, lineEnd);
1091                final int lineLen = Math.min(end - start, lineEnd - lineStart);
1092
1093                getText(lineStart, lineLen, seg);
1094                buf.append(seg.array, seg.offset, seg.count);
1095
1096                if (i != selectionEndLine) {
1097                    buf.append('\n');
1098                }
1099            }
1100
1101            return buf.toString();
1102        } else {
1103            return getText(selectionStart, selectionEnd - selectionStart);
1104        }
1105    }
1106
1107    /**
1108     * Replaces the selection with the specified text.
1109     * @param selectedText the replacement text for the selection
1110     */
1111    public void setSelectedText(@NotNull final String selectedText) {
1112        if (!editable) {
1113            throw new InternalError("Text component read only");
1114        }
1115
1116        SyntaxDocument.beginCompoundEdit();
1117
1118        try {
1119            if (rectangleSelect) {
1120                final Element map = document.getDefaultRootElement();
1121
1122                int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
1123                int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();
1124
1125                // Certain rectangles satisfy this condition...
1126                if (end < start) {
1127                    final int tmp = end;
1128                    end = start;
1129                    start = tmp;
1130                }
1131
1132                int lastNewline = 0;
1133                int currNewline = 0;
1134
1135                for (int i = selectionStartLine; i <= selectionEndLine; i++) {
1136                    final Element lineElement = map.getElement(i);
1137                    final int lineStart = lineElement.getStartOffset();
1138                    final int lineEnd = lineElement.getEndOffset() - 1;
1139                    final int rectangleStart = Math.min(lineEnd, lineStart + start);
1140
1141                    document.remove(rectangleStart, Math.min(lineEnd - rectangleStart, end - start));
1142
1143                    if (selectedText == null) {
1144                        continue;
1145                    }
1146
1147                    currNewline = selectedText.indexOf('\n', lastNewline);
1148                    if (currNewline == -1) {
1149                        currNewline = selectedText.length();
1150                    }
1151
1152                    document.insertString(rectangleStart, selectedText.substring(lastNewline, currNewline), null);
1153
1154                    lastNewline = Math.min(selectedText.length(), currNewline + 1);
1155                }
1156
1157                if (selectedText != null && currNewline != selectedText.length()) {
1158                    final int offset = map.getElement(selectionEndLine).getEndOffset() - 1;
1159                    document.insertString(offset, "\n", null);
1160                    document.insertString(offset + 1, selectedText.substring(currNewline + 1), null);
1161                }
1162            } else {
1163                document.remove(selectionStart, selectionEnd - selectionStart);
1164                if (selectedText != null) {
1165                    document.insertString(selectionStart, selectedText, null);
1166                }
1167            }
1168        } catch (final BadLocationException bl) {
1169            bl.printStackTrace();
1170            throw new InternalError("Cannot replace selection");
1171        } finally {
1172            // No matter what happens... stops us from leaving document
1173            // in a bad state
1174            SyntaxDocument.endCompoundEdit();
1175        }
1176
1177        setCaretPosition(selectionEnd);
1178    }
1179
1180    /**
1181     * Returns true if this text area is editable, false otherwise.
1182     */
1183    public boolean isEditable() {
1184        return editable;
1185    }
1186
1187    /**
1188     * Returns the `magic' caret position. This can be used to preserve the
1189     * column position when moving up and down lines.
1190     */
1191    public int getMagicCaretPosition() {
1192        return magicCaret;
1193    }
1194
1195    /**
1196     * Sets the `magic' caret position. This can be used to preserve the column
1197     * position when moving up and down lines.
1198     * @param magicCaret the magic caret position
1199     */
1200    public void setMagicCaretPosition(final int magicCaret) {
1201        this.magicCaret = magicCaret;
1202    }
1203
1204    /**
1205     * Similar to <code>setSelectedText()</code>, but overstrikes the
1206     * appropriate number of characters if overwrite mode is enabled.
1207     * @param str the string
1208     * @see #setSelectedText(String)
1209     * @see #isOverwriteEnabled()
1210     */
1211    public void overwriteSetSelectedText(@NotNull final String str) {
1212        // Don't overstrike if there is a selection
1213        if (!overwrite || selectionStart != selectionEnd) {
1214            setSelectedText(str);
1215            return;
1216        }
1217
1218        // Don't overstrike if we're on the end of
1219        // the line
1220        final int caret = getCaretPosition();
1221        final int caretLineEnd = getLineEndOffset(getCaretLine());
1222        if (caretLineEnd - caret <= str.length()) {
1223            setSelectedText(str);
1224            return;
1225        }
1226
1227        SyntaxDocument.beginCompoundEdit();
1228
1229        try {
1230            document.remove(caret, str.length());
1231            document.insertString(caret, str, null);
1232        } catch (final BadLocationException bl) {
1233            bl.printStackTrace();
1234        } finally {
1235            SyntaxDocument.endCompoundEdit();
1236        }
1237    }
1238
1239    /**
1240     * Returns true if overwrite mode is enabled, false otherwise.
1241     */
1242    public boolean isOverwriteEnabled() {
1243        return overwrite;
1244    }
1245
1246    /**
1247     * Sets if overwrite mode should be enabled.
1248     * @param overwrite true if overwrite mode should be enabled, false
1249     * otherwise
1250     */
1251    public void setOverwriteEnabled(final boolean overwrite) {
1252        this.overwrite = overwrite;
1253        painter.invalidateSelectedLines();
1254    }
1255
1256    /**
1257     * Returns true if the selection is rectangular, false otherwise.
1258     */
1259    public boolean isSelectionRectangular() {
1260        return rectangleSelect;
1261    }
1262
1263    /**
1264     * Sets if the selection should be rectangular.
1265     * @param rectangleSelect true if the selection should be rectangular, false
1266     * otherwise
1267     */
1268    public void setSelectionRectangular(final boolean rectangleSelect) {
1269        this.rectangleSelect = rectangleSelect;
1270        painter.invalidateSelectedLines();
1271    }
1272
1273    /**
1274     * Returns the position of the highlighted bracket (the bracket matching the
1275     * one before the caret).
1276     */
1277    public int getBracketPosition() {
1278        return bracketPosition;
1279    }
1280
1281    /**
1282     * Returns the line of the highlighted bracket (the bracket matching the one
1283     * before the caret).
1284     */
1285    public int getBracketLine() {
1286        return bracketLine;
1287    }
1288
1289    /**
1290     * Deletes the selected text from the text area and places it into the
1291     * clipboard.
1292     */
1293    public void cut() {
1294        if (editable) {
1295            copy();
1296            setSelectedText("");
1297        }
1298    }
1299
1300    /**
1301     * Places the selected text into the clipboard.
1302     */
1303    public void copy() {
1304        if (selectionStart != selectionEnd) {
1305            final Clipboard clipboard = getToolkit().getSystemClipboard();
1306
1307            final String selection = getSelectedText();
1308
1309            final int repeatCount = inputHandler.getRepeatCount();
1310            final StringBuilder buf = new StringBuilder();
1311            for (int i = 0; i < repeatCount; i++) {
1312                buf.append(selection);
1313            }
1314
1315            clipboard.setContents(new StringSelection(buf.toString()), null);
1316        }
1317    }
1318
1319    /**
1320     * Inserts the clipboard contents into the text.
1321     */
1322    public void paste() {
1323        if (editable) {
1324            final Clipboard clipboard = getToolkit().getSystemClipboard();
1325            try {
1326                // The MacOS MRJ doesn't convert \r to \n,
1327                // so do it here
1328                final String selection = ((String) clipboard.getContents(this).getTransferData(DataFlavor.stringFlavor)).replace('\r', '\n');
1329
1330                final int repeatCount = inputHandler.getRepeatCount();
1331                final StringBuilder buf = new StringBuilder();
1332                for (int i = 0; i < repeatCount; i++) {
1333                    buf.append(selection);
1334                }
1335                setSelectedText(buf.toString());
1336            } catch (final IOException e) {
1337                getToolkit().beep();
1338                log.error("Clipboard does not contain a string");
1339            } catch (final UnsupportedFlavorException e) {
1340                getToolkit().beep();
1341                log.error("Clipboard does not contain a string");
1342            }
1343        }
1344    }
1345
1346    /**
1347     * Called by the AWT when this component is removed from it's parent. This
1348     * stops clears the currently focused component.
1349     */
1350    @Override
1351    public void removeNotify() {
1352        super.removeNotify();
1353        if (focusedComponent == this) {
1354            focusedComponent = null;
1355        }
1356    }
1357
1358    /**
1359     * Forwards key events directly to the input handler. This is slightly
1360     * faster than using a KeyListener because some Swing overhead is avoided.
1361     */
1362    @Override
1363    public void processKeyEvent(@NotNull final KeyEvent e) {
1364        if (inputHandler == null) {
1365            return;
1366        }
1367
1368        switch (e.getID()) {
1369        case KeyEvent.KEY_TYPED:
1370            inputHandler.keyTyped(e);
1371            break;
1372
1373        case KeyEvent.KEY_PRESSED:
1374            inputHandler.keyPressed(e);
1375            break;
1376
1377        case KeyEvent.KEY_RELEASED:
1378            inputHandler.keyReleased(e);
1379            break;
1380        }
1381
1382        super.processKeyEvent(e);
1383    }
1384
1385    void updateBracketHighlight(final int newCaretPosition) {
1386        if (newCaretPosition == 0) {
1387            bracketPosition = -1;
1388            bracketLine = -1;
1389            return;
1390        }
1391
1392        try {
1393            final int offset = TextUtilities.findMatchingBracket(document, newCaretPosition - 1);
1394            if (offset != -1) {
1395                bracketLine = getLineOfOffset(offset);
1396                bracketPosition = offset - getLineStartOffset(bracketLine);
1397                return;
1398            }
1399        } catch (final BadLocationException bl) {
1400            bl.printStackTrace();
1401        }
1402
1403        bracketLine = -1;
1404        bracketPosition = -1;
1405    }
1406
1407    void documentChanged(@NotNull final DocumentEvent evt) {
1408        final DocumentEvent.ElementChange ch = evt.getChange(document.getDefaultRootElement());
1409
1410        final int count;
1411        if (ch == null) {
1412            count = 0;
1413        } else {
1414            count = ch.getChildrenAdded().length - ch.getChildrenRemoved().length;
1415        }
1416
1417        final int line = getLineOfOffset(evt.getOffset());
1418        if (count == 0) {
1419            painter.invalidateLine(line);
1420        } else if (line < firstLine) {
1421            // do magic stuff
1422            setFirstLine(firstLine + count);
1423            // end of magic stuff
1424        } else {
1425            painter.invalidateLineRange(line, firstLine + visibleLines);
1426            updateScrollBars();
1427        }
1428    }
1429
1430    /**
1431     * Return whether the text content has been modified from the "unmodified"
1432     * state.
1433     * @return <code>true</code> if the text content has been modified, or
1434     *         <code>false</code> if it is unmodified
1435     */
1436    public boolean isModified() {
1437        return !unmodifiedText.equals(getText());
1438    }
1439
1440    /**
1441     * Reset the "modified" state.
1442     */
1443    public void resetModified() {
1444        unmodifiedText = getText();
1445    }
1446
1447    private static class CaretBlinker implements ActionListener {
1448
1449        @Override
1450        public void actionPerformed(@NotNull final ActionEvent e) {
1451            if (focusedComponent != null && focusedComponent.hasFocus()) {
1452                focusedComponent.blinkCaret();
1453            }
1454        }
1455
1456    }
1457
1458    private class MutableCaretEvent extends CaretEvent {
1459
1460        /**
1461         * Serial Version UID.
1462         */
1463        private static final long serialVersionUID = 1L;
1464
1465        MutableCaretEvent() {
1466            super(JEditTextArea.this);
1467        }
1468
1469        @Override
1470        public int getDot() {
1471            return getCaretPosition();
1472        }
1473
1474        @Override
1475        public int getMark() {
1476            return getMarkPosition();
1477        }
1478
1479    }
1480
1481    private class AdjustHandler implements AdjustmentListener {
1482
1483        @Override
1484        public void adjustmentValueChanged(@NotNull final AdjustmentEvent e) {
1485            if (!scrollBarsInitialized) {
1486                return;
1487            }
1488
1489            // If this is not done, mousePressed events accumulate
1490            // and the result is that scrolling doesn't stop after
1491            // the mouse is released
1492            SwingUtilities.invokeLater(new Runnable() {
1493
1494                @Override
1495                public void run() {
1496                    if (e.getAdjustable() == vertical) {
1497                        setFirstLine(vertical.getValue());
1498                    } else {
1499                        setHorizontalOffset(-horizontal.getValue());
1500                    }
1501                }
1502
1503            });
1504        }
1505
1506    }
1507
1508    private class ComponentHandler extends ComponentAdapter {
1509
1510        @Override
1511        public void componentResized(@NotNull final ComponentEvent e) {
1512            recalculateVisibleLines();
1513            scrollBarsInitialized = true;
1514        }
1515
1516    }
1517
1518    private class DocumentHandler implements DocumentListener {
1519
1520        @Override
1521        public void insertUpdate(@NotNull final DocumentEvent e) {
1522            documentChanged(e);
1523
1524            final int offset = e.getOffset();
1525            final int length = e.getLength();
1526
1527            final int newStart;
1528            if (selectionStart > offset || (selectionStart == selectionEnd && selectionStart == offset)) {
1529                newStart = selectionStart + length;
1530            } else {
1531                newStart = selectionStart;
1532            }
1533
1534            final int newEnd;
1535            if (selectionEnd >= offset) {
1536                newEnd = selectionEnd + length;
1537            } else {
1538                newEnd = selectionEnd;
1539            }
1540
1541            select(newStart, newEnd);
1542        }
1543
1544        @Override
1545        public void removeUpdate(@NotNull final DocumentEvent e) {
1546            documentChanged(e);
1547
1548            final int offset = e.getOffset();
1549            final int length = e.getLength();
1550
1551            final int newStart;
1552            if (selectionStart > offset) {
1553                if (selectionStart > offset + length) {
1554                    newStart = selectionStart - length;
1555                } else {
1556                    newStart = offset;
1557                }
1558            } else {
1559                newStart = selectionStart;
1560            }
1561
1562            final int newEnd;
1563            if (selectionEnd > offset) {
1564                if (selectionEnd > offset + length) {
1565                    newEnd = selectionEnd - length;
1566                } else {
1567                    newEnd = offset;
1568                }
1569            } else {
1570                newEnd = selectionEnd;
1571            }
1572
1573            select(newStart, newEnd);
1574        }
1575
1576        @Override
1577        public void changedUpdate(@NotNull final DocumentEvent e) {
1578        }
1579
1580    }
1581
1582    private class DragHandler implements MouseMotionListener {
1583
1584        @Override
1585        public void mouseDragged(@NotNull final MouseEvent e) {
1586            if (popup != null && popup.isVisible()) {
1587                return;
1588            }
1589
1590            setSelectionRectangular((e.getModifiers() & InputEvent.CTRL_MASK) != 0);
1591            select(getMarkPosition(), xyToOffset(e.getX(), e.getY()));
1592        }
1593
1594        @Override
1595        public void mouseMoved(@NotNull final MouseEvent e) {
1596        }
1597
1598    }
1599
1600    private class FocusHandler implements FocusListener {
1601
1602        @Override
1603        public void focusGained(@NotNull final FocusEvent e) {
1604            setCaretVisible(true);
1605            focusedComponent = JEditTextArea.this;
1606        }
1607
1608        @Override
1609        public void focusLost(@NotNull final FocusEvent e) {
1610            setCaretVisible(false);
1611            focusedComponent = null;
1612        }
1613
1614    }
1615
1616    /**
1617     * @noinspection RefusedBequest
1618     */
1619    private class MouseHandler extends MouseAdapter {
1620
1621        @Override
1622        public void mousePressed(@NotNull final MouseEvent e) {
1623            requestFocus();
1624
1625            // Focus events not fired sometimes?
1626            setCaretVisible(true);
1627            focusedComponent = JEditTextArea.this;
1628
1629            if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0 && popup != null) {
1630                popup.show(painter, e.getX(), e.getY());
1631                return;
1632            }
1633
1634            final int line = yToLine(e.getY());
1635            final int offset = xToOffset(line, e.getX());
1636            final int dot = getLineStartOffset(line) + offset;
1637
1638            switch (e.getClickCount()) {
1639            case 1:
1640                doSingleClick(e, dot);
1641                break;
1642            case 2:
1643                doDoubleClick(line, offset, dot);
1644                break;
1645            case 3:
1646                doTripleClick(line);
1647                break;
1648            }
1649        }
1650
1651        @Override
1652        public void mouseWheelMoved(@NotNull final MouseWheelEvent e) {
1653            final int diff;
1654            if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
1655                diff = e.getUnitsToScroll() * vertical.getUnitIncrement(1);
1656            } else {
1657                diff = e.getWheelRotation() * vertical.getBlockIncrement();
1658            }
1659            vertical.setValue(vertical.getValue() + diff);
1660        }
1661
1662        private void doSingleClick(@NotNull final InputEvent evt, final int dot) {
1663            if ((evt.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
1664                setCaretPosition(dot);
1665            } else {
1666                rectangleSelect = (evt.getModifiers() & InputEvent.CTRL_MASK) != 0;
1667                select(getMarkPosition(), dot);
1668            }
1669        }
1670
1671        private void doDoubleClick(final int line, final int offset, final int dot) {
1672            // Ignore empty lines
1673            if (getLineLength(line) == 0) {
1674                return;
1675            }
1676
1677            try {
1678                int bracket = TextUtilities.findMatchingBracket(document, Math.max(0, dot - 1));
1679                if (bracket != -1) {
1680                    int mark = getMarkPosition();
1681                    // Hack
1682                    if (bracket > mark) {
1683                        bracket++;
1684                        mark--;
1685                    }
1686                    select(mark, bracket);
1687                    return;
1688                }
1689            } catch (final BadLocationException bl) {
1690                bl.printStackTrace();
1691            }
1692
1693            // Ok, it's not a bracket... select the word
1694            final CharSequence lineText = getLineText(line);
1695            char ch = lineText.charAt(Math.max(0, offset - 1));
1696
1697            String noWordSep = (String) document.getProperty("noWordSep");
1698            if (noWordSep == null) {
1699                noWordSep = "";
1700            }
1701
1702            // If the user clicked on a non-letter char,
1703            // we select the surrounding non-letters
1704            final boolean selectNoLetter = !Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1;
1705
1706            int wordStart = 0;
1707
1708            for (int i = offset - 1; i >= 0; i--) {
1709                ch = lineText.charAt(i);
1710                if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) {
1711                    wordStart = i + 1;
1712                    break;
1713                }
1714            }
1715
1716            int wordEnd = lineText.length();
1717            for (int i = offset; i < lineText.length(); i++) {
1718                ch = lineText.charAt(i);
1719                if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) {
1720                    wordEnd = i;
1721                    break;
1722                }
1723            }
1724
1725            final int lineStart = getLineStartOffset(line);
1726            select(lineStart + wordStart, lineStart + wordEnd);
1727
1728            /*
1729            String lineText = getLineText(line);
1730            String noWordSep = (String)document.getProperty("noWordSep");
1731            int wordStart = TextUtilities.findWordStart(lineText, offset, noWordSep);
1732            int wordEnd = TextUtilities.findWordEnd(lineText, offset, noWordSep);
1733
1734            int lineStart = getLineStartOffset(line);
1735            select(lineStart + wordStart, lineStart + wordEnd);
1736            */
1737        }
1738
1739        private void doTripleClick(final int line) {
1740            select(getLineStartOffset(line), getLineEndOffset(line) - 1);
1741        }
1742
1743    }
1744
1745    private class CaretUndo extends AbstractUndoableEdit {
1746
1747        /**
1748         * Serial Version UID.
1749         */
1750        private static final long serialVersionUID = 1L;
1751
1752        private int start;
1753
1754        private int end;
1755
1756        private CaretUndo(final int start, final int end) {
1757            this.start = start;
1758            this.end = end;
1759        }
1760
1761        @Override
1762        public boolean isSignificant() {
1763            return false;
1764        }
1765
1766        @NotNull
1767        @Override
1768        public String getPresentationName() {
1769            return "caret move";
1770        }
1771
1772        @Override
1773        public void undo() throws CannotUndoException {
1774            super.undo();
1775
1776            select(start, end);
1777        }
1778
1779        @Override
1780        public void redo() throws CannotRedoException {
1781            super.redo();
1782
1783            select(start, end);
1784        }
1785
1786        @Override
1787        public boolean addEdit(@NotNull final UndoableEdit anEdit) {
1788            if (anEdit instanceof CaretUndo) {
1789                final CaretUndo caretUndo = (CaretUndo) anEdit;
1790                start = caretUndo.start;
1791                end = caretUndo.end;
1792                caretUndo.die();
1793
1794                return true;
1795            } else {
1796                return false;
1797            }
1798        }
1799
1800    }
1801
1802}