source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextAreaEditorKit.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: 54.4 KB
Line 
1/*
2 * 08/29/2004
3 *
4 * RSyntaxTextAreaEditorKit.java - The editor kit used by RSyntaxTextArea.
5 *
6 * This library is distributed under a modified BSD license. See the included
7 * RSyntaxTextArea.License.txt file for details.
8 */
9package org.fife.ui.rsyntaxtextarea;
10
11import java.awt.*;
12import java.awt.event.*;
13import java.util.ResourceBundle;
14import java.util.Stack;
15import javax.swing.*;
16import javax.swing.text.*;
17
18import org.fife.ui.rsyntaxtextarea.folding.Fold;
19import org.fife.ui.rsyntaxtextarea.folding.FoldCollapser;
20import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
21import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate;
22import org.fife.ui.rtextarea.Gutter;
23import org.fife.ui.rtextarea.IconRowHeader;
24import org.fife.ui.rtextarea.RecordableTextAction;
25import org.fife.ui.rtextarea.RTextArea;
26import org.fife.ui.rtextarea.RTextAreaEditorKit;
27
28
29/**
30 * An extension of <code>RTextAreaEditorKit</code> that adds functionality for
31 * programming-specific stuff. There are currently subclasses to handle:
32 *
33 * <ul>
34 * <li>Toggling code folds.</li>
35 * <li>Aligning "closing" curly braces with their matches, if the current
36 * programming language uses curly braces to identify code blocks.</li>
37 * <li>Copying the current selection as RTF.</li>
38 * <li>Block indentation (increasing the indent of one or multiple lines)</li>
39 * <li>Block un-indentation (decreasing the indent of one or multiple lines)
40 * </li>
41 * <li>Inserting a "code template" when a configurable key (e.g. a space) is
42 * pressed</li>
43 * <li>Decreasing the point size of all fonts in the text area</li>
44 * <li>Increasing the point size of all fonts in the text area</li>
45 * <li>Moving the caret to the "matching bracket" of the one at the current
46 * caret position</li>
47 * <li>Toggling whether the currently selected lines are commented out.</li>
48 * <li>Better selection of "words" on mouse double-clicks for programming
49 * languages.</li>
50 * <li>Better keyboard navigation via Ctrl+arrow keys for programming
51 * languages.</li>
52 * </ul>
53 *
54 * @author Robert Futrell
55 * @version 0.5
56 */
57public class RSyntaxTextAreaEditorKit extends RTextAreaEditorKit {
58
59 private static final long serialVersionUID = 1L;
60
61 public static final String rstaCloseCurlyBraceAction = "RSTA.CloseCurlyBraceAction";
62 public static final String rstaCloseMarkupTagAction = "RSTA.CloseMarkupTagAction";
63 public static final String rstaCollapseAllFoldsAction = "RSTA.CollapseAllFoldsAction";
64 public static final String rstaCollapseAllCommentFoldsAction = "RSTA.CollapseAllCommentFoldsAction";
65 public static final String rstaCollapseFoldAction = "RSTA.CollapseFoldAction";
66 public static final String rstaCopyAsRtfAction = "RSTA.CopyAsRtfAction";
67 public static final String rstaDecreaseIndentAction = "RSTA.DecreaseIndentAction";
68 public static final String rstaExpandAllFoldsAction = "RSTA.ExpandAllFoldsAction";
69 public static final String rstaExpandFoldAction = "RSTA.ExpandFoldAction";
70 public static final String rstaGoToMatchingBracketAction = "RSTA.GoToMatchingBracketAction";
71 public static final String rstaPossiblyInsertTemplateAction = "RSTA.TemplateAction";
72 public static final String rstaToggleCommentAction = "RSTA.ToggleCommentAction";
73 public static final String rstaToggleCurrentFoldAction = "RSTA.ToggleCurrentFoldAction";
74
75 private static final String MSG = "org.fife.ui.rsyntaxtextarea.RSyntaxTextArea";
76 private static final ResourceBundle msg = ResourceBundle.getBundle(MSG);
77
78
79 /**
80 * The actions that <code>RSyntaxTextAreaEditorKit</code> adds to those of
81 * <code>RTextAreaEditorKit</code>.
82 */
83 private static final Action[] defaultActions = {
84 new CloseCurlyBraceAction(),
85 new CloseMarkupTagAction(),
86 new BeginWordAction(beginWordAction, false),
87 new BeginWordAction(selectionBeginWordAction, true),
88 new ChangeFoldStateAction(rstaCollapseFoldAction, true),
89 new ChangeFoldStateAction(rstaExpandFoldAction, false),
90 new CollapseAllFoldsAction(),
91 new CopyAsRtfAction(),
92 //new DecreaseFontSizeAction(),
93 new DecreaseIndentAction(),
94 new DeletePrevWordAction(),
95 new EndAction(endAction, false),
96 new EndAction(selectionEndAction, true),
97 new EndWordAction(endWordAction, false),
98 new EndWordAction(endWordAction, true),
99 new ExpandAllFoldsAction(),
100 new GoToMatchingBracketAction(),
101 new InsertBreakAction(),
102 //new IncreaseFontSizeAction(),
103 new InsertTabAction(),
104 new NextWordAction(nextWordAction, false),
105 new NextWordAction(selectionNextWordAction, true),
106 new PossiblyInsertTemplateAction(),
107 new PreviousWordAction(previousWordAction, false),
108 new PreviousWordAction(selectionPreviousWordAction, true),
109 new SelectWordAction(),
110 new ToggleCommentAction(),
111 };
112
113
114 /**
115 * Constructor.
116 */
117 public RSyntaxTextAreaEditorKit() {
118 }
119
120
121 /**
122 * Returns the default document used by <code>RSyntaxTextArea</code>s.
123 *
124 * @return The document.
125 */
126 public Document createDefaultDocument() {
127 return new RSyntaxDocument(SyntaxConstants.SYNTAX_STYLE_NONE);
128 }
129
130
131 /**
132 * Overridden to return a row header that is aware of folding.
133 *
134 * @param textArea The text area.
135 * @return The icon row header.
136 */
137 public IconRowHeader createIconRowHeader(RTextArea textArea) {
138 return new FoldingAwareIconRowHeader((RSyntaxTextArea)textArea);
139 }
140
141
142 /**
143 * Fetches the set of commands that can be used
144 * on a text component that is using a model and
145 * view produced by this kit.
146 *
147 * @return the command list
148 */
149 public Action[] getActions() {
150 return TextAction.augmentList(super.getActions(),
151 RSyntaxTextAreaEditorKit.defaultActions);
152 }
153
154
155 /**
156 * Returns localized text for an action. There's definitely a better place
157 * for this functionality.
158 *
159 * @param key The key into the action resource bundle.
160 * @return The localized text.
161 */
162 public static String getString(String key) {
163 return msg.getString(key);
164 }
165
166
167 /**
168 * Positions the caret at the beginning of the word. This class is here
169 * to better handle finding the "beginning of the word" for programming
170 * languages.
171 */
172 protected static class BeginWordAction
173 extends RTextAreaEditorKit.BeginWordAction {
174
175 private Segment seg;
176
177 protected BeginWordAction(String name, boolean select) {
178 super(name, select);
179 seg = new Segment();
180 }
181
182 protected int getWordStart(RTextArea textArea, int offs)
183 throws BadLocationException {
184
185 if (offs==0) {
186 return offs;
187 }
188
189 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
190 int line = textArea.getLineOfOffset(offs);
191 int start = textArea.getLineStartOffset(line);
192 if (offs==start) {
193 return start;
194 }
195 int end = textArea.getLineEndOffset(line);
196 if (line!=textArea.getLineCount()-1) {
197 end--;
198 }
199 doc.getText(start, end-start, seg);
200
201 // Determine the "type" of char at offs - lower case, upper case,
202 // whitespace or other. We take special care here as we're starting
203 // in the middle of the Segment to check whether we're already at
204 // the "beginning" of a word.
205 int firstIndex = seg.getBeginIndex() + (offs-start) - 1;
206 seg.setIndex(firstIndex);
207 char ch = seg.current();
208 char nextCh = offs==end ? 0 : seg.array[seg.getIndex() + 1];
209
210 // The "word" is a group of letters and/or digits
211 if (Character.isLetterOrDigit(ch)) {
212 if (offs!=end && !Character.isLetterOrDigit(nextCh)) {
213 return offs;
214 }
215 do {
216 ch = seg.previous();
217 } while (Character.isLetterOrDigit(ch));
218 }
219
220 // The "word" is whitespace
221 else if (Character.isWhitespace(ch)) {
222 if (offs!=end && !Character.isWhitespace(nextCh)) {
223 return offs;
224 }
225 do {
226 ch = seg.previous();
227 } while (Character.isWhitespace(ch));
228 }
229
230 // Otherwise, the "word" a single "something else" char (operator,
231 // etc.).
232
233 offs -= firstIndex - seg.getIndex() + 1;//seg.getEndIndex() - seg.getIndex();
234 if (ch!=Segment.DONE && nextCh!='\n') {
235 offs++;
236 }
237
238 return offs;
239
240 }
241
242 }
243
244
245 /**
246 * Expands or collapses the nearest fold.
247 */
248 public static class ChangeFoldStateAction extends FoldRelatedAction {
249
250 private boolean collapse;
251
252 public ChangeFoldStateAction(String name, boolean collapse) {
253 super(name);
254 this.collapse = collapse;
255 }
256
257 public ChangeFoldStateAction(String name, Icon icon,
258 String desc, Integer mnemonic, KeyStroke accelerator) {
259 super(name, icon, desc, mnemonic, accelerator);
260 }
261
262 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
263 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
264 if (rsta.isCodeFoldingEnabled()) {
265 Fold fold = getClosestFold(rsta);
266 if (fold!=null) {
267 fold.setCollapsed(collapse);
268 }
269 possiblyRepaintGutter(textArea);
270 }
271 else {
272 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
273 }
274 }
275
276 public final String getMacroID() {
277 return getName();
278 }
279
280 }
281
282
283 /**
284 * Action that (optionally) aligns a closing curly brace with the line
285 * containing its matching opening curly brace.
286 */
287 public static class CloseCurlyBraceAction extends RecordableTextAction {
288
289 private static final long serialVersionUID = 1L;
290
291 private Segment seg;
292
293 public CloseCurlyBraceAction() {
294 super(rstaCloseCurlyBraceAction);
295 seg = new Segment();
296 }
297
298 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
299
300 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
301 RSyntaxDocument doc = (RSyntaxDocument)rsta.getDocument();
302 boolean alignCurlyBraces = rsta.isAutoIndentEnabled() &&
303 doc.getCurlyBracesDenoteCodeBlocks();
304
305 if (alignCurlyBraces) {
306 textArea.beginAtomicEdit();
307 }
308
309 try {
310
311 textArea.replaceSelection("}");
312
313 // If the user wants to align curly braces...
314 if (alignCurlyBraces) {
315
316 Element root = doc.getDefaultRootElement();
317 int dot = rsta.getCaretPosition() - 1; // Start before '{'
318 int line = root.getElementIndex(dot);
319 Element elem = root.getElement(line);
320 int start = elem.getStartOffset();
321
322 // Get the current line's text up to the '}' entered.
323 try {
324 doc.getText(start, dot-start, seg);
325 } catch (BadLocationException ble) { // Never happens
326 ble.printStackTrace();
327 return;
328 }
329
330 // Only attempt to align if there's only whitespace up to
331 // the '}' entered.
332 for (int i=0; i<seg.count; i++) {
333 char ch = seg.array[seg.offset+i];
334 if (!Character.isWhitespace(ch)) {
335 return;
336 }
337 }
338
339 // Locate the matching '{' bracket, and replace the leading
340 // whitespace for the '}' to match that of the '{' char's line.
341 int match = RSyntaxUtilities.getMatchingBracketPosition(rsta);
342 if (match>-1) {
343 elem = root.getElement(root.getElementIndex(match));
344 int start2 = elem.getStartOffset();
345 int end = elem.getEndOffset() - 1;
346 String text = null;
347 try {
348 text = doc.getText(start2, end-start2);
349 } catch (BadLocationException ble) { // Never happens
350 ble.printStackTrace();
351 return;
352 }
353 String ws = RSyntaxUtilities.getLeadingWhitespace(text);
354 rsta.replaceRange(ws, start, dot);
355 }
356
357 }
358
359 } finally {
360 if (alignCurlyBraces) {
361 textArea.endAtomicEdit();
362 }
363 }
364
365 }
366
367 public final String getMacroID() {
368 return rstaCloseCurlyBraceAction;
369 }
370
371 }
372
373
374 /**
375 * (Optionally) completes a closing markup tag.
376 */
377 public static class CloseMarkupTagAction extends RecordableTextAction {
378
379 private static final long serialVersionUID = 1L;
380
381 public CloseMarkupTagAction() {
382 super(rstaCloseMarkupTagAction);
383 }
384
385 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
386
387 if (!textArea.isEditable() || !textArea.isEnabled()) {
388 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
389 return;
390 }
391
392 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
393 RSyntaxDocument doc = (RSyntaxDocument)rsta.getDocument();
394
395 Caret c = rsta.getCaret();
396 boolean selection = c.getDot()!=c.getMark();
397 rsta.replaceSelection("/");
398
399 // Don't automatically complete a tag if there was a selection
400 int dot = c.getDot();
401
402 if (doc.getLanguageIsMarkup() &&
403 doc.getCompleteMarkupCloseTags() &&
404 !selection && rsta.getCloseMarkupTags() && dot>1) {
405
406 try {
407
408 // Check actual char before token type, since it's quicker
409 char ch = doc.charAt(dot-2);
410 if (ch=='<' || ch=='[') {
411
412 Token t = doc.getTokenListForLine(
413 rsta.getCaretLineNumber());
414 t = RSyntaxUtilities.getTokenAtOffset(t, dot-1);
415 if (t!=null && t.type==Token.MARKUP_TAG_DELIMITER) {
416 //System.out.println("Huzzah - closing tag!");
417 String tagName = discoverTagName(doc, dot);
418 if (tagName!=null) {
419 rsta.replaceSelection(tagName + (char)(ch+2));
420 }
421 }
422
423 }
424
425 } catch (BadLocationException ble) { // Never happens
426 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
427 ble.printStackTrace();
428 }
429
430 }
431
432 }
433
434 /**
435 * Discovers the name of the tag being closed. Assumes standard
436 * SGML-style markup tags.
437 *
438 * @param doc The document to parse.
439 * @param dot The location of the caret. This should be right after
440 * the start of a closing tag token (e.g. "<code>&lt;/</code>"
441 * or "<code>[</code>" in the case of BBCode).
442 * @return The name of the tag to close, or <code>null</code> if it
443 * could not be determined.
444 */
445 private String discoverTagName(RSyntaxDocument doc, int dot) {
446
447 Stack stack = new Stack();
448
449 Element root = doc.getDefaultRootElement();
450 int curLine = root.getElementIndex(dot);
451
452 for (int i=0; i<=curLine; i++) {
453
454 Token t = doc.getTokenListForLine(i);
455 while (t!=null && t.isPaintable()) {
456
457 if (t.type==Token.MARKUP_TAG_DELIMITER) {
458 if (t.isSingleChar('<') || t.isSingleChar('[')) {
459 t = t.getNextToken();
460 while (t!=null && t.isPaintable()) {
461 if (t.type==Token.MARKUP_TAG_NAME ||
462 // Being lenient here and also checking
463 // for attributes, in case they
464 // (incorrectly) have whitespace between
465 // the '<' char and the element name.
466 t.type==Token.MARKUP_TAG_ATTRIBUTE) {
467 stack.push(t.getLexeme());
468 break;
469 }
470 t = t.getNextToken();
471 }
472 }
473 else if (t.textCount==2 && t.text[t.textOffset]=='/' &&
474 (t.text[t.textOffset+1]=='>' ||
475 t.text[t.textOffset+1]==']')) {
476 if (!stack.isEmpty()) { // Always true for valid XML
477 stack.pop();
478 }
479 }
480 else if (t.textCount==2 &&
481 (t.text[t.textOffset]=='<' || t.text[t.textOffset]=='[') &&
482 t.text[t.textOffset+1]=='/') {
483 String tagName = null;
484 if (!stack.isEmpty()) { // Always true for valid XML
485 tagName = (String)stack.pop();
486 }
487 if (t.offset+t.textCount>=dot) {
488 return tagName;
489 }
490 }
491 }
492
493 t = t.getNextToken();
494
495 }
496
497 }
498
499 return null; // Should never happen
500
501 }
502
503 public String getMacroID() {
504 return getName();
505 }
506
507 }
508
509
510 /**
511 * Collapses all comment folds.
512 */
513 public static class CollapseAllCommentFoldsAction extends FoldRelatedAction{
514
515 private static final long serialVersionUID = 1L;
516
517 public CollapseAllCommentFoldsAction() {
518 super(rstaCollapseAllCommentFoldsAction);
519 setProperties(msg, "Action.CollapseCommentFolds");
520 }
521
522 public CollapseAllCommentFoldsAction(String name, Icon icon,
523 String desc, Integer mnemonic, KeyStroke accelerator) {
524 super(name, icon, desc, mnemonic, accelerator);
525 }
526
527 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
528 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
529 if (rsta.isCodeFoldingEnabled()) {
530 FoldCollapser collapser = new FoldCollapser();
531 collapser.collapseFolds(rsta.getFoldManager());
532 possiblyRepaintGutter(textArea);
533 }
534 else {
535 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
536 }
537 }
538
539 public final String getMacroID() {
540 return rstaCollapseAllCommentFoldsAction;
541 }
542
543 }
544
545
546 /**
547 * Collapses all folds.
548 */
549 public static class CollapseAllFoldsAction extends FoldRelatedAction {
550
551 private static final long serialVersionUID = 1L;
552
553 public CollapseAllFoldsAction() {
554 this(false);
555 }
556
557 public CollapseAllFoldsAction(boolean localizedName) {
558 super(rstaCollapseAllFoldsAction);
559 if (localizedName) {
560 setProperties(msg, "Action.CollapseAllFolds");
561 }
562 }
563
564 public CollapseAllFoldsAction(String name, Icon icon,
565 String desc, Integer mnemonic, KeyStroke accelerator) {
566 super(name, icon, desc, mnemonic, accelerator);
567 }
568
569 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
570 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
571 if (rsta.isCodeFoldingEnabled()) {
572 FoldCollapser collapser = new FoldCollapser() {
573 public boolean getShouldCollapse(Fold fold) {
574 return true;
575 }
576 };
577 collapser.collapseFolds(rsta.getFoldManager());
578 possiblyRepaintGutter(textArea);
579 }
580 else {
581 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
582 }
583 }
584
585 public final String getMacroID() {
586 return rstaCollapseAllFoldsAction;
587 }
588
589 }
590
591
592 /**
593 * Action for copying text as RTF.
594 */
595 public static class CopyAsRtfAction extends RecordableTextAction {
596
597 private static final long serialVersionUID = 1L;
598
599 public CopyAsRtfAction() {
600 super(rstaCopyAsRtfAction);
601 }
602
603 public CopyAsRtfAction(String name, Icon icon, String desc,
604 Integer mnemonic, KeyStroke accelerator) {
605 super(name, icon, desc, mnemonic, accelerator);
606 }
607
608 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
609 ((RSyntaxTextArea)textArea).copyAsRtf();
610 textArea.requestFocusInWindow();
611 }
612
613 public final String getMacroID() {
614 return getName();
615 }
616
617 }
618
619
620 /**
621 * Action for decreasing the font size of all fonts in the text area.
622 */
623 public static class DecreaseFontSizeAction
624 extends RTextAreaEditorKit.DecreaseFontSizeAction {
625
626 private static final long serialVersionUID = 1L;
627
628 public DecreaseFontSizeAction() {
629 super();
630 }
631
632 public DecreaseFontSizeAction(String name, Icon icon, String desc,
633 Integer mnemonic, KeyStroke accelerator) {
634 super(name, icon, desc, mnemonic, accelerator);
635 }
636
637 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
638
639 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
640 SyntaxScheme scheme = rsta.getSyntaxScheme();
641
642 // All we need to do is update all of the fonts in syntax
643 // schemes, then call setSyntaxHighlightingColorScheme with the
644 // same scheme already being used. This relies on the fact that
645 // that method does not check whether the new scheme is different
646 // from the old scheme before updating.
647
648 boolean changed = false;
649 int count = scheme.getStyleCount();
650 for (int i=0; i<count; i++) {
651 Style ss = scheme.getStyle(i);
652 if (ss!=null) {
653 Font font = ss.font;
654 if (font!=null) {
655 float oldSize = font.getSize2D();
656 float newSize = oldSize - decreaseAmount;
657 if (newSize>=MINIMUM_SIZE) {
658 // Shrink by decreaseAmount.
659 ss.font = font.deriveFont(newSize);
660 changed = true;
661 }
662 else if (oldSize>MINIMUM_SIZE) {
663 // Can't shrink by full decreaseAmount, but
664 // can shrink a little bit.
665 ss.font = font.deriveFont(MINIMUM_SIZE);
666 changed = true;
667 }
668 }
669 }
670 }
671
672 // Do the text area's font also.
673 Font font = rsta.getFont();
674 float oldSize = font.getSize2D();
675 float newSize = oldSize - decreaseAmount;
676 if (newSize>=MINIMUM_SIZE) {
677 // Shrink by decreaseAmount.
678 rsta.setFont(font.deriveFont(newSize));
679 changed = true;
680 }
681 else if (oldSize>MINIMUM_SIZE) {
682 // Can't shrink by full decreaseAmount, but
683 // can shrink a little bit.
684 rsta.setFont(font.deriveFont(MINIMUM_SIZE));
685 changed = true;
686 }
687
688 // If we updated at least one font, update the screen. If
689 // all of the fonts were already the minimum size, beep.
690 if (changed) {
691 rsta.setSyntaxScheme(scheme);
692 // NOTE: This is a hack to get an encompassing
693 // RTextScrollPane to repaint its line numbers to account
694 // for a change in line height due to a font change. I'm
695 // not sure why we need to do this here but not when we
696 // change the syntax highlighting color scheme via the
697 // Options dialog... setSyntaxHighlightingColorScheme()
698 // calls revalidate() which won't repaint the scroll pane
699 // if scrollbars don't change, which is why we need this.
700 Component parent = rsta.getParent();
701 if (parent instanceof javax.swing.JViewport) {
702 parent = parent.getParent();
703 if (parent instanceof JScrollPane) {
704 parent.repaint();
705 }
706 }
707 }
708 else
709 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
710
711 }
712
713 }
714
715
716 /**
717 * Action for when un-indenting lines (either the current line if there is
718 * selection, or all selected lines if there is one).
719 */
720 public static class DecreaseIndentAction extends RecordableTextAction {
721
722 private static final long serialVersionUID = 1L;
723
724 private Segment s;
725
726 public DecreaseIndentAction() {
727 this(rstaDecreaseIndentAction);
728 }
729
730 public DecreaseIndentAction(String name) {
731 super(name);
732 s = new Segment();
733 }
734
735 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
736
737 if (!textArea.isEditable() || !textArea.isEnabled()) {
738 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
739 return;
740 }
741
742 Document document = textArea.getDocument();
743 Element map = document.getDefaultRootElement();
744 Caret c = textArea.getCaret();
745 int dot = c.getDot();
746 int mark = c.getMark();
747 int line1 = map.getElementIndex(dot);
748 int tabSize = textArea.getTabSize();
749
750 // If there is a selection, indent all lines in the selection.
751 // Otherwise, indent the line the caret is on.
752 if (dot!=mark) {
753 // Note that we cheaply reuse variables here, so don't
754 // take their names to mean what they are.
755 int line2 = map.getElementIndex(mark);
756 dot = Math.min(line1, line2);
757 mark = Math.max(line1, line2);
758 Element elem;
759 try {
760 for (line1=dot; line1<mark; line1++) {
761 elem = map.getElement(line1);
762 handleDecreaseIndent(elem, document, tabSize);
763 }
764 // Don't do the last line if the caret is at its
765 // beginning. We must call getDot() again and not just
766 // use 'dot' as the caret's position may have changed
767 // due to the insertion of the tabs above.
768 elem = map.getElement(mark);
769 int start = elem.getStartOffset();
770 if (Math.max(c.getDot(),c.getMark())!=start) {
771 handleDecreaseIndent(elem, document, tabSize);
772 }
773 } catch (BadLocationException ble) {
774 ble.printStackTrace();
775 UIManager.getLookAndFeel().
776 provideErrorFeedback(textArea);
777 }
778 }
779 else {
780 Element elem = map.getElement(line1);
781 try {
782 handleDecreaseIndent(elem, document, tabSize);
783 } catch (BadLocationException ble) {
784 ble.printStackTrace();
785 UIManager.getLookAndFeel().
786 provideErrorFeedback(textArea);
787 }
788 }
789
790 }
791
792 public final String getMacroID() {
793 return rstaDecreaseIndentAction;
794 }
795
796 /**
797 * Actually does the "de-indentation." This method finds where the
798 * given element's leading whitespace ends, then, if there is indeed
799 * leading whitespace, removes either the last char in it (if it is a
800 * tab), or removes up to the number of spaces equal to a tab in the
801 * specified document (i.e., if the tab size was 5 and there were 3
802 * spaces at the end of the leading whitespace, the three will be
803 * removed; if there were 8 spaces, only the first 5 would be
804 * removed).
805 *
806 * @param elem The element to "de-indent."
807 * @param doc The document containing the specified element.
808 * @param tabSize The size of a tab, in spaces.
809 */
810 private final void handleDecreaseIndent(Element elem, Document doc,
811 int tabSize)
812 throws BadLocationException {
813 int start = elem.getStartOffset();
814 int end = elem.getEndOffset() - 1; // Why always true??
815 doc.getText(start,end-start, s);
816 int i = s.offset;
817 end = i+s.count;
818 if (end>i) {
819 // If the first character is a tab, remove it.
820 if (s.array[i]=='\t') {
821 doc.remove(start, 1);
822 }
823 // Otherwise, see if the first character is a space. If it
824 // is, remove all contiguous whitespaces at the beginning of
825 // this line, up to the tab size.
826 else if (s.array[i]==' ') {
827 i++;
828 int toRemove = 1;
829 while (i<end && s.array[i]==' ' && toRemove<tabSize) {
830 i++;
831 toRemove++;
832 }
833 doc.remove(start, toRemove);
834 }
835 }
836 }
837
838 }
839
840
841 /**
842 * Deletes the previous word, but differentiates symbols from "words" to
843 * match the behavior of code editors.
844 */
845 public static class DeletePrevWordAction
846 extends RTextAreaEditorKit.DeletePrevWordAction {
847
848 private Segment seg = new Segment();
849
850 protected int getPreviousWordStart(RTextArea textArea, int offs)
851 throws BadLocationException {
852
853 if (offs==0) {
854 return offs;
855 }
856
857 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
858 int line = textArea.getLineOfOffset(offs);
859 int start = textArea.getLineStartOffset(line);
860 if (offs==start) {
861 return start-1; // Just delete the newline
862 }
863 int end = textArea.getLineEndOffset(line);
864 if (line!=textArea.getLineCount()-1) {
865 end--;
866 }
867 doc.getText(start, end-start, seg);
868
869 // Determine the "type" of char at offs - lower case, upper case,
870 // whitespace or other. We take special care here as we're starting
871 // in the middle of the Segment to check whether we're already at
872 // the "beginning" of a word.
873 int firstIndex = seg.getBeginIndex() + (offs-start) - 1;
874 seg.setIndex(firstIndex);
875 char ch = seg.current();
876
877 // Always strip off whitespace first
878 if (Character.isWhitespace(ch)) {
879 do {
880 ch = seg.previous();
881 } while (Character.isWhitespace(ch));
882 }
883
884 // The "word" is a group of letters and/or digits
885 if (Character.isLetterOrDigit(ch)) {
886 do {
887 ch = seg.previous();
888 } while (Character.isLetterOrDigit(ch));
889 }
890
891 // The "word" is a series of symbols.
892 else {
893 while (!Character.isWhitespace(ch) &&
894 !Character.isLetterOrDigit(ch)
895 && ch!=Segment.DONE) {
896 ch = seg.previous();
897 }
898 }
899
900 if (ch==Segment.DONE) {
901 return start; // Removed last "token" of the line
902 }
903 offs -= firstIndex - seg.getIndex();
904 return offs;
905
906 }
907
908 }
909
910
911 /**
912 * Moves the caret to the end of the document, taking into account code
913 * folding.
914 */
915 public static class EndAction extends RTextAreaEditorKit.EndAction {
916
917 public EndAction(String name, boolean select) {
918 super(name, select);
919 }
920
921 protected int getVisibleEnd(RTextArea textArea) {
922 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
923 return rsta.getLastVisibleOffset();
924 }
925
926 }
927
928
929 /**
930 * Positions the caret at the end of the word. This class is here to
931 * better handle finding the "end of the word" in programming languages.
932 */
933 protected static class EndWordAction
934 extends RTextAreaEditorKit.EndWordAction {
935
936 private Segment seg;
937
938 protected EndWordAction(String name, boolean select) {
939 super(name, select);
940 seg = new Segment();
941 }
942
943 protected int getWordEnd(RTextArea textArea, int offs)
944 throws BadLocationException {
945
946 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
947 if (offs==doc.getLength()) {
948 return offs;
949 }
950
951 int line = textArea.getLineOfOffset(offs);
952 int end = textArea.getLineEndOffset(line);
953 if (line!=textArea.getLineCount()-1) {
954 end--; // Hide newline
955 }
956 if (offs==end) {
957 return end;
958 }
959 doc.getText(offs, end-offs, seg);
960
961 // Determine the "type" of char at offs - letter/digit,
962 // whitespace or other
963 char ch = seg.first();
964
965 // The "word" is a group of letters and/or digits
966 if (Character.isLetterOrDigit(ch)) {
967 do {
968 ch = seg.next();
969 } while (Character.isLetterOrDigit(ch));
970 }
971
972 // The "word" is whitespace.
973 else if (Character.isWhitespace(ch)) {
974
975 do {
976 ch = seg.next();
977 } while (Character.isWhitespace(ch));
978 }
979
980 // Otherwise, the "word" is a single character of some other type
981 // (operator, etc.).
982
983 offs += seg.getIndex() - seg.getBeginIndex();
984 return offs;
985
986 }
987
988 }
989
990
991 /**
992 * Expands all folds.
993 */
994 public static class ExpandAllFoldsAction extends FoldRelatedAction {
995
996 private static final long serialVersionUID = 1L;
997
998 public ExpandAllFoldsAction() {
999 this(false);
1000 }
1001
1002 public ExpandAllFoldsAction(boolean localizedName) {
1003 super(rstaExpandAllFoldsAction);
1004 if (localizedName) {
1005 setProperties(msg, "Action.ExpandAllFolds");
1006 }
1007 }
1008
1009 public ExpandAllFoldsAction(String name, Icon icon,
1010 String desc, Integer mnemonic, KeyStroke accelerator) {
1011 super(name, icon, desc, mnemonic, accelerator);
1012 }
1013
1014 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1015 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1016 if (rsta.isCodeFoldingEnabled()) {
1017 FoldManager fm = rsta.getFoldManager();
1018 for (int i=0; i<fm.getFoldCount(); i++) {
1019 expand(fm.getFold(i));
1020 }
1021 possiblyRepaintGutter(rsta);
1022 }
1023 else {
1024 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
1025 }
1026 }
1027
1028 private void expand(Fold fold) {
1029 fold.setCollapsed(false);
1030 for (int i=0; i<fold.getChildCount(); i++) {
1031 expand(fold.getChild(i));
1032 }
1033 }
1034
1035 public final String getMacroID() {
1036 return rstaExpandAllFoldsAction;
1037 }
1038
1039 }
1040
1041
1042 /**
1043 * Base class for folding-related actions.
1044 */
1045 static abstract class FoldRelatedAction extends RecordableTextAction {
1046
1047 public FoldRelatedAction(String name) {
1048 super(name);
1049 }
1050
1051 public FoldRelatedAction(String name, Icon icon,
1052 String desc, Integer mnemonic, KeyStroke accelerator) {
1053 super(name, icon, desc, mnemonic, accelerator);
1054 }
1055
1056 protected Fold getClosestFold(RSyntaxTextArea textArea) {
1057 int offs = textArea.getCaretPosition();
1058 int line = textArea.getCaretLineNumber();
1059 FoldManager fm = textArea.getFoldManager();
1060 Fold fold = fm.getFoldForLine(line);
1061 if (fold==null) {
1062 fold = fm.getDeepestOpenFoldContaining(offs);
1063 }
1064 return fold;
1065 }
1066
1067 /**
1068 * Repaints the gutter in a text area's scroll pane, if necessary.
1069 *
1070 * @param textArea The text area.
1071 */
1072 protected void possiblyRepaintGutter(RTextArea textArea) {
1073 Gutter gutter = RSyntaxUtilities.getGutter(textArea);
1074 if (gutter!=null) {
1075 gutter.repaint();
1076 }
1077 }
1078
1079 }
1080
1081
1082 /**
1083 * Action for moving the caret to the "matching bracket" of the bracket
1084 * at the caret position (either before or after).
1085 */
1086 public static class GoToMatchingBracketAction
1087 extends RecordableTextAction {
1088
1089 private static final long serialVersionUID = 1L;
1090
1091 public GoToMatchingBracketAction() {
1092 super(rstaGoToMatchingBracketAction);
1093 }
1094
1095 public GoToMatchingBracketAction(String name, Icon icon, String desc,
1096 Integer mnemonic, KeyStroke accelerator) {
1097 super(name, icon, desc, mnemonic, accelerator);
1098 }
1099
1100 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1101 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1102 int pos = RSyntaxUtilities.getMatchingBracketPosition(rsta);
1103 if (pos>-1) {
1104 // Go to the position AFTER the bracket so the previous
1105 // bracket (which we were just on) is highlighted.
1106 rsta.setCaretPosition(pos+1);
1107 }
1108 else {
1109 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
1110 }
1111 }
1112
1113 public final String getMacroID() {
1114 return rstaGoToMatchingBracketAction;
1115 }
1116
1117 }
1118
1119
1120 /**
1121 * Action for increasing the font size of all fonts in the text area.
1122 */
1123 public static class IncreaseFontSizeAction
1124 extends RTextAreaEditorKit.IncreaseFontSizeAction {
1125
1126 private static final long serialVersionUID = 1L;
1127
1128 public IncreaseFontSizeAction() {
1129 super();
1130 }
1131
1132 public IncreaseFontSizeAction(String name, Icon icon, String desc,
1133 Integer mnemonic, KeyStroke accelerator) {
1134 super(name, icon, desc, mnemonic, accelerator);
1135 }
1136
1137 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1138
1139 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1140 SyntaxScheme scheme = rsta.getSyntaxScheme();
1141
1142 // All we need to do is update all of the fonts in syntax
1143 // schemes, then call setSyntaxHighlightingColorScheme with the
1144 // same scheme already being used. This relies on the fact that
1145 // that method does not check whether the new scheme is different
1146 // from the old scheme before updating.
1147
1148 boolean changed = false;
1149 int count = scheme.getStyleCount();
1150 for (int i=0; i<count; i++) {
1151 Style ss = scheme.getStyle(i);
1152 if (ss!=null) {
1153 Font font = ss.font;
1154 if (font!=null) {
1155 float oldSize = font.getSize2D();
1156 float newSize = oldSize + increaseAmount;
1157 if (newSize<=MAXIMUM_SIZE) {
1158 // Grow by increaseAmount.
1159 ss.font = font.deriveFont(newSize);
1160 changed = true;
1161 }
1162 else if (oldSize<MAXIMUM_SIZE) {
1163 // Can't grow by full increaseAmount, but
1164 // can grow a little bit.
1165 ss.font = font.deriveFont(MAXIMUM_SIZE);
1166 changed = true;
1167 }
1168 }
1169 }
1170 }
1171
1172 // Do the text area's font also.
1173 Font font = rsta.getFont();
1174 float oldSize = font.getSize2D();
1175 float newSize = oldSize + increaseAmount;
1176 if (newSize<=MAXIMUM_SIZE) {
1177 // Grow by increaseAmount.
1178 rsta.setFont(font.deriveFont(newSize));
1179 changed = true;
1180 }
1181 else if (oldSize<MAXIMUM_SIZE) {
1182 // Can't grow by full increaseAmount, but
1183 // can grow a little bit.
1184 rsta.setFont(font.deriveFont(MAXIMUM_SIZE));
1185 changed = true;
1186 }
1187
1188 // If we updated at least one font, update the screen. If
1189 // all of the fonts were already the minimum size, beep.
1190 if (changed) {
1191 rsta.setSyntaxScheme(scheme);
1192 // NOTE: This is a hack to get an encompassing
1193 // RTextScrollPane to repaint its line numbers to account
1194 // for a change in line height due to a font change. I'm
1195 // not sure why we need to do this here but not when we
1196 // change the syntax highlighting color scheme via the
1197 // Options dialog... setSyntaxHighlightingColorScheme()
1198 // calls revalidate() which won't repaint the scroll pane
1199 // if scrollbars don't change, which is why we need this.
1200 Component parent = rsta.getParent();
1201 if (parent instanceof javax.swing.JViewport) {
1202 parent = parent.getParent();
1203 if (parent instanceof JScrollPane) {
1204 parent.repaint();
1205 }
1206 }
1207 }
1208 else
1209 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
1210
1211 }
1212
1213 }
1214
1215
1216 /**
1217 * Action for when the user presses the Enter key. This is here so we can
1218 * be smart and "auto-indent" for programming languages.
1219 */
1220 public static class InsertBreakAction
1221 extends RTextAreaEditorKit.InsertBreakAction {
1222
1223 private static final long serialVersionUID = 1L;
1224
1225 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1226
1227 if (!textArea.isEditable() || !textArea.isEnabled()) {
1228 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
1229 return;
1230 }
1231
1232 RSyntaxTextArea sta = (RSyntaxTextArea)textArea;
1233 boolean noSelection= sta.getSelectionStart()==sta.getSelectionEnd();
1234
1235 // First, see if this language wants to handle inserting newlines
1236 // itself.
1237 boolean handled = false;
1238 if (noSelection) {
1239 RSyntaxDocument doc = (RSyntaxDocument)sta.getDocument();
1240 handled = doc.insertBreakSpecialHandling(e);
1241 }
1242
1243 // If not...
1244 if (!handled) {
1245 handleInsertBreak(sta, noSelection);
1246 }
1247
1248 }
1249
1250 /**
1251 * @return The first location in the string past <code>pos</code> that
1252 * is NOT a whitespace char, or <code>-1</code> if only
1253 * whitespace chars follow <code>pos</code> (or it is the end
1254 * position in the string).
1255 */
1256 private static final int atEndOfLine(int pos, String s, int sLen) {
1257 for (int i=pos; i<sLen; i++) {
1258 if (!RSyntaxUtilities.isWhitespace(s.charAt(i)))
1259 return i;
1260 }
1261 return -1;
1262 }
1263
1264 private static final int getOpenBraceCount(RSyntaxDocument doc) {
1265 int openCount = 0;
1266 Element root = doc.getDefaultRootElement();
1267 int lineCount = root.getElementCount();
1268 for (int i=0; i<lineCount; i++) {
1269 Token t = doc.getTokenListForLine(i);
1270 while (t!=null && t.isPaintable()) {
1271 if (t.type==Token.SEPARATOR && t.textCount==1) {
1272 char ch = t.text[t.textOffset];
1273 if (ch=='{') {
1274 openCount++;
1275 }
1276 else if (ch=='}') {
1277 openCount--;
1278 }
1279 }
1280 t = t.getNextToken();
1281 }
1282 }
1283 return openCount;
1284 }
1285
1286 /**
1287 * Actually inserts the newline into the document, and auto-indents
1288 * if appropriate. This method can be called by token makers who
1289 * implement a custom action for inserting newlines.
1290 *
1291 * @param textArea
1292 * @param noSelection Whether there is no selection.
1293 */
1294 protected void handleInsertBreak(RSyntaxTextArea textArea,
1295 boolean noSelection) {
1296 // If we're auto-indenting...
1297 if (noSelection && textArea.isAutoIndentEnabled()) {
1298 insertNewlineWithAutoIndent(textArea);
1299 }
1300 else {
1301 textArea.replaceSelection("\n");
1302 if (noSelection) {
1303 possiblyCloseCurlyBrace(textArea, null);
1304 }
1305 }
1306 }
1307
1308 private void insertNewlineWithAutoIndent(RSyntaxTextArea sta) {
1309
1310 try {
1311
1312 int caretPos = sta.getCaretPosition();
1313 Document doc = sta.getDocument();
1314 Element map = doc.getDefaultRootElement();
1315 int lineNum = map.getElementIndex(caretPos);
1316 Element line = map.getElement(lineNum);
1317 int start = line.getStartOffset();
1318 int end = line.getEndOffset()-1; // Why always "-1"?
1319 int len = end-start;
1320 String s = doc.getText(start, len);
1321
1322 // endWS is the end of the leading whitespace of the
1323 // current line.
1324 String leadingWS = RSyntaxUtilities.getLeadingWhitespace(s);
1325 StringBuffer sb = new StringBuffer("\n");
1326 sb.append(leadingWS);
1327
1328 // If there is only whitespace between the caret and
1329 // the EOL, pressing Enter auto-indents the new line to
1330 // the same place as the previous line.
1331 int nonWhitespacePos = atEndOfLine(caretPos-start, s, len);
1332 if (nonWhitespacePos==-1) {
1333 if (leadingWS.length()==len &&
1334 sta.isClearWhitespaceLinesEnabled()) {
1335 // If the line was nothing but whitespace, select it
1336 // so its contents get removed.
1337 sta.setSelectionStart(start);
1338 sta.setSelectionEnd(end);
1339 }
1340 sta.replaceSelection(sb.toString());
1341 }
1342
1343 // If there is non-whitespace between the caret and the
1344 // EOL, pressing Enter takes that text to the next line
1345 // and auto-indents it to the same place as the last
1346 // line.
1347 else {
1348 sb.append(s.substring(nonWhitespacePos));
1349 sta.replaceRange(sb.toString(), caretPos, end);
1350 sta.setCaretPosition(caretPos + leadingWS.length()+1);
1351 }
1352
1353 // Must do it after everything else, as the "smart indent"
1354 // calculation depends on the previous line's state
1355 // AFTER the Enter press (stuff may have been moved down).
1356 if (sta.getShouldIndentNextLine(lineNum)) {
1357 sta.replaceSelection("\t");
1358 }
1359
1360 possiblyCloseCurlyBrace(sta, leadingWS);
1361
1362 } catch (BadLocationException ble) { // Never happens
1363 sta.replaceSelection("\n");
1364 ble.printStackTrace();
1365 }
1366
1367 }
1368
1369 private void possiblyCloseCurlyBrace(RSyntaxTextArea textArea,
1370 String leadingWS) {
1371
1372 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
1373
1374 if (textArea.getCloseCurlyBraces() &&
1375 doc.getCurlyBracesDenoteCodeBlocks()) {
1376
1377 int line = textArea.getCaretLineNumber();
1378 Token t = doc.getTokenListForLine(line-1);
1379 t = t.getLastNonCommentNonWhitespaceToken();
1380
1381 if (t!=null && t.isLeftCurly()) {
1382
1383 if (getOpenBraceCount(doc)>0) {
1384 StringBuffer sb = new StringBuffer();
1385 if (line==textArea.getLineCount()-1) {
1386 sb.append('\n');
1387 }
1388 if (leadingWS!=null) {
1389 sb.append(leadingWS);
1390 }
1391 sb.append("}\n");
1392 int dot = textArea.getCaretPosition();
1393 int end = textArea.getLineEndOffsetOfCurrentLine();
1394 // Insert at end of line, not at dot: they may have
1395 // pressed Enter in the middle of the line and brought
1396 // some text (though it must be whitespace and/or
1397 // comments) down onto the new line.
1398 textArea.insert(sb.toString(), end);
1399 textArea.setCaretPosition(dot); // Caret may have moved
1400 }
1401
1402 }
1403
1404 }
1405
1406 }
1407
1408 }
1409
1410
1411 /**
1412 * Action for inserting tabs. This is extended to "block indent" a
1413 * group of contiguous lines if they are selected.
1414 */
1415 public static class InsertTabAction extends RecordableTextAction {
1416
1417 private static final long serialVersionUID = 1L;
1418
1419 public InsertTabAction() {
1420 super(insertTabAction);
1421 }
1422
1423 public InsertTabAction(String name) {
1424 super(name);
1425 }
1426
1427 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1428
1429 if (!textArea.isEditable() || !textArea.isEnabled()) {
1430 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
1431 return;
1432 }
1433
1434 Document document = textArea.getDocument();
1435 Element map = document.getDefaultRootElement();
1436 Caret c = textArea.getCaret();
1437 int dot = c.getDot();
1438 int mark = c.getMark();
1439 int dotLine = map.getElementIndex(dot);
1440 int markLine = map.getElementIndex(mark);
1441
1442 // If there is a multi-line selection, indent all lines in
1443 // the selection.
1444 if (dotLine!=markLine) {
1445 int first = Math.min(dotLine, markLine);
1446 int last = Math.max(dotLine, markLine);
1447 Element elem; int start;
1448
1449 // Since we're using Document.insertString(), we must mimic the
1450 // soft tab behavior provided by RTextArea.replaceSelection().
1451 String replacement = "\t";
1452 if (textArea.getTabsEmulated()) {
1453 StringBuffer sb = new StringBuffer();
1454 int temp = textArea.getTabSize();
1455 for (int i=0; i<temp; i++) {
1456 sb.append(' ');
1457 }
1458 replacement = sb.toString();
1459 }
1460
1461 textArea.beginAtomicEdit();
1462 try {
1463 for (int i=first; i<last; i++) {
1464 elem = map.getElement(i);
1465 start = elem.getStartOffset();
1466 document.insertString(start, replacement, null);
1467 }
1468 // Don't do the last line if the caret is at its
1469 // beginning. We must call getDot() again and not just
1470 // use 'dot' as the caret's position may have changed
1471 // due to the insertion of the tabs above.
1472 elem = map.getElement(last);
1473 start = elem.getStartOffset();
1474 if (Math.max(c.getDot(), c.getMark())!=start) {
1475 document.insertString(start, replacement, null);
1476 }
1477 } catch (BadLocationException ble) { // Never happens.
1478 ble.printStackTrace();
1479 UIManager.getLookAndFeel().
1480 provideErrorFeedback(textArea);
1481 } finally {
1482 textArea.endAtomicEdit();
1483 }
1484 }
1485 else {
1486 textArea.replaceSelection("\t");
1487 }
1488
1489 }
1490
1491 public final String getMacroID() {
1492 return insertTabAction;
1493 }
1494
1495 }
1496
1497
1498 /**
1499 * Action to move the selection and/or caret. Constructor indicates
1500 * direction to use. This class overrides the behavior defined in
1501 * {@link RTextAreaEditorKit} to better skip "words" in source code.
1502 */
1503 public static class NextWordAction
1504 extends RTextAreaEditorKit.NextWordAction {
1505
1506 private Segment seg;
1507
1508 public NextWordAction(String nm, boolean select) {
1509 super(nm, select);
1510 seg = new Segment();
1511 }
1512
1513 /**
1514 * Overridden to do better with skipping "words" in code.
1515 */
1516 protected int getNextWord(RTextArea textArea, int offs)
1517 throws BadLocationException {
1518
1519 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
1520 if (offs==doc.getLength()) {
1521 return offs;
1522 }
1523
1524 Element root = doc.getDefaultRootElement();
1525 int line = root.getElementIndex(offs);
1526 int end = root.getElement(line).getEndOffset() - 1;
1527 if (offs==end) {// If we're already at the end of the line...
1528 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1529 if (rsta.isCodeFoldingEnabled()) { // Start of next visible line
1530 FoldManager fm = rsta.getFoldManager();
1531 int lineCount = root.getElementCount();
1532 while (++line<lineCount && fm.isLineHidden(line));
1533 if (line<lineCount) { // Found a lower visible line
1534 offs = root.getElement(line).getStartOffset();
1535 }
1536 // No lower visible line - we're already at last visible offset
1537 return offs;
1538 }
1539 else {
1540 return offs+1; // Start of next line.
1541 }
1542 }
1543 doc.getText(offs, end-offs, seg);
1544
1545 // Determine the "type" of char at offs - letter/digit,
1546 // whitespace or other
1547 char ch = seg.first();
1548
1549 // Skip the group of letters and/or digits
1550 if (Character.isLetterOrDigit(ch)) {
1551 do {
1552 ch = seg.next();
1553 } while (Character.isLetterOrDigit(ch));
1554 }
1555
1556 // Skip groups of "anything else" (operators, etc.).
1557 else if (!Character.isWhitespace(ch)) {
1558 do {
1559 ch = seg.next();
1560 } while (ch!=Segment.DONE &&
1561 !(Character.isLetterOrDigit(ch) ||
1562 Character.isWhitespace(ch)));
1563 }
1564
1565 // Skip any trailing whitespace
1566 while (Character.isWhitespace(ch)) {
1567 ch = seg.next();
1568 }
1569
1570 offs += seg.getIndex() - seg.getBeginIndex();
1571 return offs;
1572
1573 }
1574
1575 }
1576
1577
1578 /**
1579 * Action for when the user tries to insert a template (that is,
1580 * they've typed a template ID and pressed the trigger character
1581 * (a space) in an attempt to do the substitution).
1582 */
1583 public static class PossiblyInsertTemplateAction extends RecordableTextAction {
1584
1585 private static final long serialVersionUID = 1L;
1586
1587 public PossiblyInsertTemplateAction() {
1588 super(rstaPossiblyInsertTemplateAction);
1589 }
1590
1591 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1592
1593 if (!textArea.isEditable() || !textArea.isEnabled())
1594 return;
1595
1596 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1597
1598 if (RSyntaxTextArea.getTemplatesEnabled()) {
1599
1600 Document doc = textArea.getDocument();
1601 if (doc != null) {
1602
1603 try {
1604
1605 CodeTemplateManager manager = RSyntaxTextArea.
1606 getCodeTemplateManager();
1607 CodeTemplate template = manager==null ? null :
1608 manager.getTemplate(rsta);
1609
1610 // A non-null template means modify the text to insert!
1611 if (template!=null) {
1612 template.invoke(rsta);
1613 }
1614
1615 // No template - insert default text. This is
1616 // exactly what DefaultKeyTypedAction does.
1617 else {
1618 doDefaultInsert(rsta);
1619 }
1620
1621 } catch (BadLocationException ble) {
1622 UIManager.getLookAndFeel().
1623 provideErrorFeedback(textArea);
1624 }
1625
1626
1627 } // End of if (doc!=null).
1628
1629 } // End of if (textArea.getTemplatesEnabled()).
1630
1631 // If templates aren't enabled, just insert the text as usual.
1632 else {
1633 doDefaultInsert(rsta);
1634 }
1635
1636 }
1637
1638 private final void doDefaultInsert(RTextArea textArea) {
1639 // FIXME: We need a way to get the "trigger string" (i.e.,
1640 // the text that was just typed); however, the text area's
1641 // template manager might be null (if templates are disabled).
1642 // Also, the manager's trigger string doesn't yet match up with
1643 // that defined in RSyntaxTextAreaEditorKit.java (which is
1644 // hardcoded as a space)...
1645 //String str = manager.getInsertTriggerString();
1646 //int mod = manager.getInsertTrigger().getModifiers();
1647 //if (str!=null && str.length()>0 &&
1648 // ((mod&ActionEvent.ALT_MASK)==(mod&ActionEvent.CTRL_MASK))) {
1649 // char ch = str.charAt(0);
1650 // if (ch>=0x20 && ch!=0x7F)
1651 // textArea.replaceSelection(str);
1652 //}
1653 textArea.replaceSelection(" ");
1654 }
1655
1656 public final String getMacroID() {
1657 return rstaPossiblyInsertTemplateAction;
1658 }
1659
1660 }
1661
1662
1663 /**
1664 * Action to move the selection and/or caret. Constructor indicates
1665 * direction to use. This class overrides the behavior defined in
1666 * {@link RTextAreaEditorKit} to better skip "words" in source code.
1667 */
1668 public static class PreviousWordAction
1669 extends RTextAreaEditorKit.PreviousWordAction {
1670
1671 private Segment seg;
1672
1673 public PreviousWordAction(String nm, boolean select) {
1674 super(nm, select);
1675 seg = new Segment();
1676 }
1677
1678 /**
1679 * Overridden to do better with skipping "words" in code.
1680 */
1681 protected int getPreviousWord(RTextArea textArea, int offs)
1682 throws BadLocationException {
1683
1684 if (offs==0) {
1685 return offs;
1686 }
1687
1688 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
1689 Element root = doc.getDefaultRootElement();
1690 int line = root.getElementIndex(offs);
1691 int start = root.getElement(line).getStartOffset();
1692 if (offs==start) {// If we're already at the start of the line...
1693 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1694 if (rsta.isCodeFoldingEnabled()) { // End of next visible line
1695 FoldManager fm = rsta.getFoldManager();
1696 while (--line>=0 && fm.isLineHidden(line));
1697 if (line>=0) { // Found an earlier visible line
1698 offs = root.getElement(line).getEndOffset() - 1;
1699 }
1700 // No earlier visible line - we must be at offs==0...
1701 return offs;
1702 }
1703 else {
1704 return start-1; // End of previous line.
1705 }
1706 }
1707 doc.getText(start, offs-start, seg);
1708
1709 // Determine the "type" of char at offs - lower case, upper case,
1710 // whitespace or other
1711 char ch = seg.last();
1712
1713 // Skip any "leading" whitespace
1714 while (Character.isWhitespace(ch)) {
1715 ch = seg.previous();
1716 }
1717
1718 // Skip the group of letters and/or digits
1719 if (Character.isLetterOrDigit(ch)) {
1720 do {
1721 ch = seg.previous();
1722 } while (Character.isLetterOrDigit(ch));
1723 }
1724
1725 // Skip groups of "anything else" (operators, etc.).
1726 else if (!Character.isWhitespace(ch)) {
1727 do {
1728 ch = seg.previous();
1729 } while (ch!=Segment.DONE &&
1730 !(Character.isLetterOrDigit(ch) ||
1731 Character.isWhitespace(ch)));
1732 }
1733
1734 offs -= seg.getEndIndex() - seg.getIndex();
1735 if (ch!=Segment.DONE) {
1736 offs++;
1737 }
1738
1739 return offs;
1740
1741 }
1742
1743 }
1744
1745
1746 /**
1747 * Selects the word around the caret. This class is here to better
1748 * handle selecting "words" in programming languages.
1749 */
1750 public static class SelectWordAction
1751 extends RTextAreaEditorKit.SelectWordAction {
1752
1753 protected void createActions() {
1754 start = new BeginWordAction("pigdog", false);
1755 end = new EndWordAction("pigdog", true);
1756 }
1757
1758 }
1759
1760
1761 /**
1762 * Action that toggles whether the currently selected lines are
1763 * commented.
1764 */
1765 public static class ToggleCommentAction extends RecordableTextAction {
1766
1767 public ToggleCommentAction() {
1768 super(rstaToggleCommentAction);
1769 }
1770
1771 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1772
1773 if (!textArea.isEditable() || !textArea.isEnabled()) {
1774 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
1775 return;
1776 }
1777
1778 RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
1779 String[] startEnd = doc.getLineCommentStartAndEnd();
1780
1781 if (startEnd==null) {
1782 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
1783 return;
1784 }
1785
1786 Element map = doc.getDefaultRootElement();
1787 Caret c = textArea.getCaret();
1788 int dot = c.getDot();
1789 int mark = c.getMark();
1790 int line1 = map.getElementIndex(dot);
1791 int line2 = map.getElementIndex(mark);
1792 int start = Math.min(line1, line2);
1793 int end = Math.max(line1, line2);
1794
1795 // Don't toggle comment on last line if there is no
1796 // text selected on it.
1797 if (start!=end) {
1798 Element elem = map.getElement(end);
1799 if (Math.max(dot, mark)==elem.getStartOffset()) {
1800 end--;
1801 }
1802 }
1803
1804 textArea.beginAtomicEdit();
1805 try {
1806 boolean add = getDoAdd(doc,map, start,end, startEnd);
1807 for (line1=start; line1<=end; line1++) {
1808 Element elem = map.getElement(line1);
1809 handleToggleComment(elem, doc, startEnd, add);
1810 }
1811 } catch (BadLocationException ble) {
1812 ble.printStackTrace();
1813 UIManager.getLookAndFeel().provideErrorFeedback(textArea);
1814 } finally {
1815 textArea.endAtomicEdit();
1816 }
1817
1818 }
1819
1820 private boolean getDoAdd(Document doc, Element map, int startLine,
1821 int endLine, String[] startEnd)
1822 throws BadLocationException {
1823 boolean doAdd = false;
1824 for (int i=startLine; i<=endLine; i++) {
1825 Element elem = map.getElement(i);
1826 int start = elem.getStartOffset();
1827 String t = doc.getText(start, elem.getEndOffset()-start-1);
1828 if (!t.startsWith(startEnd[0]) ||
1829 (startEnd[1]!=null && !t.endsWith(startEnd[1]))) {
1830 doAdd = true;
1831 break;
1832 }
1833 }
1834 return doAdd;
1835 }
1836
1837 private void handleToggleComment(Element elem, Document doc,
1838 String[] startEnd, boolean add) throws BadLocationException {
1839 int start = elem.getStartOffset();
1840 int end = elem.getEndOffset() - 1;
1841 if (add) {
1842 doc.insertString(start, startEnd[0], null);
1843 if (startEnd[1]!=null) {
1844 doc.insertString(end+startEnd[0].length(), startEnd[1],
1845 null);
1846 }
1847 }
1848 else {
1849 doc.remove(start, startEnd[0].length());
1850 if (startEnd[1]!=null) {
1851 int temp = startEnd[1].length();
1852 doc.remove(end-startEnd[0].length()-temp, temp);
1853 }
1854 }
1855 }
1856
1857 public final String getMacroID() {
1858 return rstaToggleCommentAction;
1859 }
1860
1861 }
1862
1863
1864 /**
1865 * Toggles the fold at the current caret position or line.
1866 */
1867 public static class ToggleCurrentFoldAction extends FoldRelatedAction {
1868
1869 private static final long serialVersionUID = 1L;
1870
1871 public ToggleCurrentFoldAction() {
1872 super(rstaToggleCurrentFoldAction);
1873 setProperties(msg, "Action.ToggleCurrentFold");
1874 }
1875
1876 public ToggleCurrentFoldAction(String name, Icon icon, String desc,
1877 Integer mnemonic, KeyStroke accelerator) {
1878 super(name, icon, desc, mnemonic, accelerator);
1879 }
1880
1881 public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
1882 RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
1883 if (rsta.isCodeFoldingEnabled()) {
1884 Fold fold = getClosestFold(rsta);
1885 if (fold!=null) {
1886 fold.toggleCollapsedState();
1887 }
1888 possiblyRepaintGutter(textArea);
1889 }
1890 else {
1891 UIManager.getLookAndFeel().provideErrorFeedback(rsta);
1892 }
1893 }
1894
1895 public final String getMacroID() {
1896 return rstaToggleCurrentFoldAction;
1897 }
1898
1899 }
1900
1901
1902}
Note: See TracBrowser for help on using the repository browser.