org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit Maven / Gradle / Ivy
The newest version!
/* * 08/29/2004 * * RSyntaxTextAreaEditorKit.java - The editor kit used by RSyntaxTextArea. * * This library is distributed under a modified BSD license. See the included * RSyntaxTextArea.License.txt file for details. */ package org.fife.ui.rsyntaxtextarea; import java.awt.*; import java.awt.event.*; import java.util.ResourceBundle; import java.util.Stack; import javax.swing.*; import javax.swing.text.*; import org.fife.ui.rsyntaxtextarea.folding.Fold; import org.fife.ui.rsyntaxtextarea.folding.FoldCollapser; import org.fife.ui.rsyntaxtextarea.folding.FoldManager; import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.IconRowHeader; import org.fife.ui.rtextarea.RecordableTextAction; import org.fife.ui.rtextarea.RTextArea; import org.fife.ui.rtextarea.RTextAreaEditorKit; /** * An extension of
that * is NOT a whitespace char, orRTextAreaEditorKit
that adds functionality for * programming-specific stuff. There are currently subclasses to handle: * **
* * @author Robert Futrell * @version 0.5 */ public class RSyntaxTextAreaEditorKit extends RTextAreaEditorKit { private static final long serialVersionUID = 1L; public static final String rstaCloseCurlyBraceAction = "RSTA.CloseCurlyBraceAction"; public static final String rstaCloseMarkupTagAction = "RSTA.CloseMarkupTagAction"; public static final String rstaCollapseAllFoldsAction = "RSTA.CollapseAllFoldsAction"; public static final String rstaCollapseAllCommentFoldsAction = "RSTA.CollapseAllCommentFoldsAction"; public static final String rstaCollapseFoldAction = "RSTA.CollapseFoldAction"; public static final String rstaCopyAsRtfAction = "RSTA.CopyAsRtfAction"; public static final String rstaDecreaseIndentAction = "RSTA.DecreaseIndentAction"; public static final String rstaExpandAllFoldsAction = "RSTA.ExpandAllFoldsAction"; public static final String rstaExpandFoldAction = "RSTA.ExpandFoldAction"; public static final String rstaGoToMatchingBracketAction = "RSTA.GoToMatchingBracketAction"; public static final String rstaPossiblyInsertTemplateAction = "RSTA.TemplateAction"; public static final String rstaToggleCommentAction = "RSTA.ToggleCommentAction"; public static final String rstaToggleCurrentFoldAction = "RSTA.ToggleCurrentFoldAction"; private static final String MSG = "org.fife.ui.rsyntaxtextarea.RSyntaxTextArea"; private static final ResourceBundle msg = ResourceBundle.getBundle(MSG); /** * The actions that- Toggling code folds.
*- Aligning "closing" curly braces with their matches, if the current * programming language uses curly braces to identify code blocks.
*- Copying the current selection as RTF.
*- Block indentation (increasing the indent of one or multiple lines)
*- Block un-indentation (decreasing the indent of one or multiple lines) *
*- Inserting a "code template" when a configurable key (e.g. a space) is * pressed
*- Decreasing the point size of all fonts in the text area
*- Increasing the point size of all fonts in the text area
*- Moving the caret to the "matching bracket" of the one at the current * caret position
*- Toggling whether the currently selected lines are commented out.
*- Better selection of "words" on mouse double-clicks for programming * languages.
*- Better keyboard navigation via Ctrl+arrow keys for programming * languages.
*RSyntaxTextAreaEditorKit
adds to those of *RTextAreaEditorKit
. */ private static final Action[] defaultActions = { new CloseCurlyBraceAction(), new CloseMarkupTagAction(), new BeginWordAction(beginWordAction, false), new BeginWordAction(selectionBeginWordAction, true), new ChangeFoldStateAction(rstaCollapseFoldAction, true), new ChangeFoldStateAction(rstaExpandFoldAction, false), new CollapseAllFoldsAction(), new CopyAsRtfAction(), //new DecreaseFontSizeAction(), new DecreaseIndentAction(), new DeletePrevWordAction(), new EndAction(endAction, false), new EndAction(selectionEndAction, true), new EndWordAction(endWordAction, false), new EndWordAction(endWordAction, true), new ExpandAllFoldsAction(), new GoToMatchingBracketAction(), new InsertBreakAction(), //new IncreaseFontSizeAction(), new InsertTabAction(), new NextWordAction(nextWordAction, false), new NextWordAction(selectionNextWordAction, true), new PossiblyInsertTemplateAction(), new PreviousWordAction(previousWordAction, false), new PreviousWordAction(selectionPreviousWordAction, true), new SelectWordAction(), new ToggleCommentAction(), }; /** * Constructor. */ public RSyntaxTextAreaEditorKit() { } /** * Returns the default document used byRSyntaxTextArea
s. * * @return The document. */ @Override public Document createDefaultDocument() { return new RSyntaxDocument(SyntaxConstants.SYNTAX_STYLE_NONE); } /** * Overridden to return a row header that is aware of folding. * * @param textArea The text area. * @return The icon row header. */ @Override public IconRowHeader createIconRowHeader(RTextArea textArea) { return new FoldingAwareIconRowHeader((RSyntaxTextArea)textArea); } /** * Fetches the set of commands that can be used * on a text component that is using a model and * view produced by this kit. * * @return the command list */ @Override public Action[] getActions() { return TextAction.augmentList(super.getActions(), RSyntaxTextAreaEditorKit.defaultActions); } /** * Returns localized text for an action. There's definitely a better place * for this functionality. * * @param key The key into the action resource bundle. * @return The localized text. */ public static String getString(String key) { return msg.getString(key); } /** * Positions the caret at the beginning of the word. This class is here * to better handle finding the "beginning of the word" for programming * languages. */ protected static class BeginWordAction extends RTextAreaEditorKit.BeginWordAction { private Segment seg; protected BeginWordAction(String name, boolean select) { super(name, select); seg = new Segment(); } @Override protected int getWordStart(RTextArea textArea, int offs) throws BadLocationException { if (offs==0) { return offs; } RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument(); int line = textArea.getLineOfOffset(offs); int start = textArea.getLineStartOffset(line); if (offs==start) { return start; } int end = textArea.getLineEndOffset(line); if (line!=textArea.getLineCount()-1) { end--; } doc.getText(start, end-start, seg); // Determine the "type" of char at offs - lower case, upper case, // whitespace or other. We take special care here as we're starting // in the middle of the Segment to check whether we're already at // the "beginning" of a word. int firstIndex = seg.getBeginIndex() + (offs-start) - 1; seg.setIndex(firstIndex); char ch = seg.current(); char nextCh = offs==end ? 0 : seg.array[seg.getIndex() + 1]; // The "word" is a group of letters and/or digits int languageIndex = 0; // TODO if (doc.isIdentifierChar(languageIndex, ch)) { if (offs!=end && !doc.isIdentifierChar(languageIndex, nextCh)) { return offs; } do { ch = seg.previous(); } while (doc.isIdentifierChar(languageIndex, ch)); } // The "word" is whitespace else if (Character.isWhitespace(ch)) { if (offs!=end && !Character.isWhitespace(nextCh)) { return offs; } do { ch = seg.previous(); } while (Character.isWhitespace(ch)); } // Otherwise, the "word" a single "something else" char (operator, // etc.). offs -= firstIndex - seg.getIndex() + 1;//seg.getEndIndex() - seg.getIndex(); if (ch!=Segment.DONE && nextCh!='\n') { offs++; } return offs; } } /** * Expands or collapses the nearest fold. */ public static class ChangeFoldStateAction extends FoldRelatedAction { private boolean collapse; public ChangeFoldStateAction(String name, boolean collapse) { super(name); this.collapse = collapse; } public ChangeFoldStateAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; if (rsta.isCodeFoldingEnabled()) { Fold fold = getClosestFold(rsta); if (fold!=null) { fold.setCollapsed(collapse); } possiblyRepaintGutter(textArea); } else { UIManager.getLookAndFeel().provideErrorFeedback(rsta); } } @Override public final String getMacroID() { return getName(); } } /** * Action that (optionally) aligns a closing curly brace with the line * containing its matching opening curly brace. */ public static class CloseCurlyBraceAction extends RecordableTextAction { private static final long serialVersionUID = 1L; private Point bracketInfo; private Segment seg; public CloseCurlyBraceAction() { super(rstaCloseCurlyBraceAction); seg = new Segment(); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; RSyntaxDocument doc = (RSyntaxDocument)rsta.getDocument(); int languageIndex = 0; int dot = textArea.getCaretPosition(); if (dot>0) { Token t = RSyntaxUtilities.getTokenAtOffset(rsta, dot-1); languageIndex = t==null ? 0 : t.getLanguageIndex(); } boolean alignCurlyBraces = rsta.isAutoIndentEnabled() && doc.getCurlyBracesDenoteCodeBlocks(languageIndex); if (alignCurlyBraces) { textArea.beginAtomicEdit(); } try { textArea.replaceSelection("}"); // If the user wants to align curly braces... if (alignCurlyBraces) { Element root = doc.getDefaultRootElement(); dot = rsta.getCaretPosition() - 1; // Start before '}' int line = root.getElementIndex(dot); Element elem = root.getElement(line); int start = elem.getStartOffset(); // Get the current line's text up to the '}' entered. try { doc.getText(start, dot-start, seg); } catch (BadLocationException ble) { // Never happens ble.printStackTrace(); return; } // Only attempt to align if there's only whitespace up to // the '}' entered. for (int i=0; i-1) { try { String ws = RSyntaxUtilities.getLeadingWhitespace( doc, bracketInfo.y); rsta.replaceRange(ws, start, dot); } catch (BadLocationException ble) { ble.printStackTrace(); return; } } } } finally { if (alignCurlyBraces) { textArea.endAtomicEdit(); } } } @Override public final String getMacroID() { return rstaCloseCurlyBraceAction; } } /** * (Optionally) completes a closing markup tag. */ public static class CloseMarkupTagAction extends RecordableTextAction { private static final long serialVersionUID = 1L; public CloseMarkupTagAction() { super(rstaCloseMarkupTagAction); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { if (!textArea.isEditable() || !textArea.isEnabled()) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); return; } RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; RSyntaxDocument doc = (RSyntaxDocument)rsta.getDocument(); Caret c = rsta.getCaret(); boolean selection = c.getDot()!=c.getMark(); rsta.replaceSelection("/"); // Don't automatically complete a tag if there was a selection int dot = c.getDot(); if (doc.getLanguageIsMarkup() && doc.getCompleteMarkupCloseTags() && !selection && rsta.getCloseMarkupTags() && dot>1) { try { // Check actual char before token type, since it's quicker char ch = doc.charAt(dot-2); if (ch=='<' || ch=='[') { Token t = doc.getTokenListForLine( rsta.getCaretLineNumber()); t = RSyntaxUtilities.getTokenAtOffset(t, dot-1); if (t!=null && t.getType()==Token.MARKUP_TAG_DELIMITER) { //System.out.println("Huzzah - closing tag!"); String tagName = discoverTagName(doc, dot); if (tagName!=null) { rsta.replaceSelection(tagName + (char)(ch+2)); } } } } catch (BadLocationException ble) { // Never happens UIManager.getLookAndFeel().provideErrorFeedback(rsta); ble.printStackTrace(); } } } /** * Discovers the name of the tag being closed. Assumes standard * SGML-style markup tags. * * @param doc The document to parse. * @param dot The location of the caret. This should be right after * the start of a closing tag token (e.g. " </
" * or "[
" in the case of BBCode). * @return The name of the tag to close, ornull
if it * could not be determined. */ private String discoverTagName(RSyntaxDocument doc, int dot) { Stackstack = new Stack (); Element root = doc.getDefaultRootElement(); int curLine = root.getElementIndex(dot); for (int i=0; i<=curLine; i++) { Token t = doc.getTokenListForLine(i); while (t!=null && t.isPaintable()) { if (t.getType()==Token.MARKUP_TAG_DELIMITER) { if (t.isSingleChar('<') || t.isSingleChar('[')) { t = t.getNextToken(); while (t!=null && t.isPaintable()) { if (t.getType()==Token.MARKUP_TAG_NAME || // Being lenient here and also checking // for attributes, in case they // (incorrectly) have whitespace between // the '<' char and the element name. t.getType()==Token.MARKUP_TAG_ATTRIBUTE) { stack.push(t.getLexeme()); break; } t = t.getNextToken(); } } else if (t.length()==2 && t.charAt(0)=='/' && (t.charAt(1)=='>' || t.charAt(1)==']')) { if (!stack.isEmpty()) { // Always true for valid XML stack.pop(); } } else if (t.length()==2 && (t.charAt(0)=='<' || t.charAt(0)=='[') && t.charAt(1)=='/') { String tagName = null; if (!stack.isEmpty()) { // Always true for valid XML tagName = stack.pop(); } if (t.getEndOffset()>=dot) { return tagName; } } } t = t==null ? null : t.getNextToken(); } } return null; // Should never happen } @Override public String getMacroID() { return getName(); } } /** * Collapses all comment folds. */ public static class CollapseAllCommentFoldsAction extends FoldRelatedAction{ private static final long serialVersionUID = 1L; public CollapseAllCommentFoldsAction() { super(rstaCollapseAllCommentFoldsAction); setProperties(msg, "Action.CollapseCommentFolds"); } public CollapseAllCommentFoldsAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; if (rsta.isCodeFoldingEnabled()) { FoldCollapser collapser = new FoldCollapser(); collapser.collapseFolds(rsta.getFoldManager()); possiblyRepaintGutter(textArea); } else { UIManager.getLookAndFeel().provideErrorFeedback(rsta); } } @Override public final String getMacroID() { return rstaCollapseAllCommentFoldsAction; } } /** * Collapses all folds. */ public static class CollapseAllFoldsAction extends FoldRelatedAction { private static final long serialVersionUID = 1L; public CollapseAllFoldsAction() { this(false); } public CollapseAllFoldsAction(boolean localizedName) { super(rstaCollapseAllFoldsAction); if (localizedName) { setProperties(msg, "Action.CollapseAllFolds"); } } public CollapseAllFoldsAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; if (rsta.isCodeFoldingEnabled()) { FoldCollapser collapser = new FoldCollapser() { @Override public boolean getShouldCollapse(Fold fold) { return true; } }; collapser.collapseFolds(rsta.getFoldManager()); possiblyRepaintGutter(textArea); } else { UIManager.getLookAndFeel().provideErrorFeedback(rsta); } } @Override public final String getMacroID() { return rstaCollapseAllFoldsAction; } } /** * Action for copying text as RTF. */ public static class CopyAsRtfAction extends RecordableTextAction { private static final long serialVersionUID = 1L; public CopyAsRtfAction() { super(rstaCopyAsRtfAction); } public CopyAsRtfAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { ((RSyntaxTextArea)textArea).copyAsRtf(); textArea.requestFocusInWindow(); } @Override public final String getMacroID() { return getName(); } } /** * Action for decreasing the font size of all fonts in the text area. */ public static class DecreaseFontSizeAction extends RTextAreaEditorKit.DecreaseFontSizeAction { private static final long serialVersionUID = 1L; public DecreaseFontSizeAction() { super(); } public DecreaseFontSizeAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; SyntaxScheme scheme = rsta.getSyntaxScheme(); // All we need to do is update all of the fonts in syntax // schemes, then call setSyntaxHighlightingColorScheme with the // same scheme already being used. This relies on the fact that // that method does not check whether the new scheme is different // from the old scheme before updating. boolean changed = false; int count = scheme.getStyleCount(); for (int i=0; i =MINIMUM_SIZE) { // Shrink by decreaseAmount. ss.font = font.deriveFont(newSize); changed = true; } else if (oldSize>MINIMUM_SIZE) { // Can't shrink by full decreaseAmount, but // can shrink a little bit. ss.font = font.deriveFont(MINIMUM_SIZE); changed = true; } } } } // Do the text area's font also. Font font = rsta.getFont(); float oldSize = font.getSize2D(); float newSize = oldSize - decreaseAmount; if (newSize>=MINIMUM_SIZE) { // Shrink by decreaseAmount. rsta.setFont(font.deriveFont(newSize)); changed = true; } else if (oldSize>MINIMUM_SIZE) { // Can't shrink by full decreaseAmount, but // can shrink a little bit. rsta.setFont(font.deriveFont(MINIMUM_SIZE)); changed = true; } // If we updated at least one font, update the screen. If // all of the fonts were already the minimum size, beep. if (changed) { rsta.setSyntaxScheme(scheme); // NOTE: This is a hack to get an encompassing // RTextScrollPane to repaint its line numbers to account // for a change in line height due to a font change. I'm // not sure why we need to do this here but not when we // change the syntax highlighting color scheme via the // Options dialog... setSyntaxHighlightingColorScheme() // calls revalidate() which won't repaint the scroll pane // if scrollbars don't change, which is why we need this. Component parent = rsta.getParent(); if (parent instanceof javax.swing.JViewport) { parent = parent.getParent(); if (parent instanceof JScrollPane) { parent.repaint(); } } } else UIManager.getLookAndFeel().provideErrorFeedback(rsta); } } /** * Action for when un-indenting lines (either the current line if there is * selection, or all selected lines if there is one). */ public static class DecreaseIndentAction extends RecordableTextAction { private static final long serialVersionUID = 1L; private Segment s; public DecreaseIndentAction() { this(rstaDecreaseIndentAction); } public DecreaseIndentAction(String name) { super(name); s = new Segment(); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { if (!textArea.isEditable() || !textArea.isEnabled()) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); return; } Document document = textArea.getDocument(); Element map = document.getDefaultRootElement(); Caret c = textArea.getCaret(); int dot = c.getDot(); int mark = c.getMark(); int line1 = map.getElementIndex(dot); int tabSize = textArea.getTabSize(); // If there is a selection, indent all lines in the selection. // Otherwise, indent the line the caret is on. if (dot!=mark) { // Note that we cheaply reuse variables here, so don't // take their names to mean what they are. int line2 = map.getElementIndex(mark); dot = Math.min(line1, line2); mark = Math.max(line1, line2); Element elem; textArea.beginAtomicEdit(); try { for (line1=dot; line1i) { // If the first character is a tab, remove it. if (s.array[i]=='\t') { doc.remove(start, 1); } // Otherwise, see if the first character is a space. If it // is, remove all contiguous whitespaces at the beginning of // this line, up to the tab size. else if (s.array[i]==' ') { i++; int toRemove = 1; while (i -1) { // Go to the position AFTER the bracket so the previous // bracket (which we were just on) is highlighted. rsta.setCaretPosition(bracketInfo.y+1); } else { UIManager.getLookAndFeel().provideErrorFeedback(rsta); } } @Override public final String getMacroID() { return rstaGoToMatchingBracketAction; } } /** * Action for increasing the font size of all fonts in the text area. */ public static class IncreaseFontSizeAction extends RTextAreaEditorKit.IncreaseFontSizeAction { private static final long serialVersionUID = 1L; public IncreaseFontSizeAction() { super(); } public IncreaseFontSizeAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; SyntaxScheme scheme = rsta.getSyntaxScheme(); // All we need to do is update all of the fonts in syntax // schemes, then call setSyntaxHighlightingColorScheme with the // same scheme already being used. This relies on the fact that // that method does not check whether the new scheme is different // from the old scheme before updating. boolean changed = false; int count = scheme.getStyleCount(); for (int i=0; i pos -1
if only * whitespace chars followpos
(or it is the end * position in the string). */ private static final int atEndOfLine(int pos, String s, int sLen) { for (int i=pos; i0) { StringBuilder sb = new StringBuilder(); if (line==textArea.getLineCount()-1) { sb.append('\n'); } if (leadingWS!=null) { sb.append(leadingWS); } sb.append("}\n"); int dot = textArea.getCaretPosition(); int end = textArea.getLineEndOffsetOfCurrentLine(); // Insert at end of line, not at dot: they may have // pressed Enter in the middle of the line and brought // some text (though it must be whitespace and/or // comments) down onto the new line. textArea.insert(sb.toString(), end); textArea.setCaretPosition(dot); // Caret may have moved } } } } } /** * Action for inserting tabs. This is extended to "block indent" a * group of contiguous lines if they are selected. */ public static class InsertTabAction extends RecordableTextAction { private static final long serialVersionUID = 1L; public InsertTabAction() { super(insertTabAction); } public InsertTabAction(String name) { super(name); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { if (!textArea.isEditable() || !textArea.isEnabled()) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); return; } Document document = textArea.getDocument(); Element map = document.getDefaultRootElement(); Caret c = textArea.getCaret(); int dot = c.getDot(); int mark = c.getMark(); int dotLine = map.getElementIndex(dot); int markLine = map.getElementIndex(mark); // If there is a multi-line selection, indent all lines in // the selection. if (dotLine!=markLine) { int first = Math.min(dotLine, markLine); int last = Math.max(dotLine, markLine); Element elem; int start; // Since we're using Document.insertString(), we must mimic the // soft tab behavior provided by RTextArea.replaceSelection(). String replacement = "\t"; if (textArea.getTabsEmulated()) { StringBuilder sb = new StringBuilder(); int temp = textArea.getTabSize(); for (int i=0; i 0 && // ((mod&ActionEvent.ALT_MASK)==(mod&ActionEvent.CTRL_MASK))) { // char ch = str.charAt(0); // if (ch>=0x20 && ch!=0x7F) // textArea.replaceSelection(str); //} textArea.replaceSelection(" "); } @Override public final String getMacroID() { return rstaPossiblyInsertTemplateAction; } } /** * Action to move the selection and/or caret. Constructor indicates * direction to use. This class overrides the behavior defined in * {@link RTextAreaEditorKit} to better skip "words" in source code. */ public static class PreviousWordAction extends RTextAreaEditorKit.PreviousWordAction { private Segment seg; public PreviousWordAction(String nm, boolean select) { super(nm, select); seg = new Segment(); } /** * Overridden to do better with skipping "words" in code. */ @Override protected int getPreviousWord(RTextArea textArea, int offs) throws BadLocationException { if (offs==0) { return offs; } RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument(); Element root = doc.getDefaultRootElement(); int line = root.getElementIndex(offs); int start = root.getElement(line).getStartOffset(); if (offs==start) {// If we're already at the start of the line... RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; if (rsta.isCodeFoldingEnabled()) { // End of next visible line FoldManager fm = rsta.getFoldManager(); while (--line>=0 && fm.isLineHidden(line)); if (line>=0) { // Found an earlier visible line offs = root.getElement(line).getEndOffset() - 1; } // No earlier visible line - we must be at offs==0... return offs; } else { return start-1; // End of previous line. } } doc.getText(start, offs-start, seg); // Determine the "type" of char at offs - lower case, upper case, // whitespace or other char ch = seg.last(); // Skip any "leading" whitespace while (Character.isWhitespace(ch)) { ch = seg.previous(); } // Skip the group of letters and/or digits int languageIndex = 0; if (doc.isIdentifierChar(languageIndex, ch)) { do { ch = seg.previous(); } while (doc.isIdentifierChar(languageIndex, ch)); } // Skip groups of "anything else" (operators, etc.). else if (!Character.isWhitespace(ch)) { do { ch = seg.previous(); } while (ch!=Segment.DONE && !(doc.isIdentifierChar(languageIndex, ch) || Character.isWhitespace(ch))); } offs -= seg.getEndIndex() - seg.getIndex(); if (ch!=Segment.DONE) { offs++; } return offs; } } /** * Selects the word around the caret. This class is here to better * handle selecting "words" in programming languages. */ public static class SelectWordAction extends RTextAreaEditorKit.SelectWordAction { @Override protected void createActions() { start = new BeginWordAction("pigdog", false); end = new EndWordAction("pigdog", true); } } /** * Action that toggles whether the currently selected lines are * commented. */ public static class ToggleCommentAction extends RecordableTextAction { public ToggleCommentAction() { super(rstaToggleCommentAction); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { if (!textArea.isEditable() || !textArea.isEnabled()) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); return; } RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument(); Element map = doc.getDefaultRootElement(); Caret c = textArea.getCaret(); int dot = c.getDot(); int mark = c.getMark(); int line1 = map.getElementIndex(dot); int line2 = map.getElementIndex(mark); int start = Math.min(line1, line2); int end = Math.max(line1, line2); Token t = doc.getTokenListForLine(start); int languageIndex = t!=null ? t.getLanguageIndex() : 0; String[] startEnd = doc.getLineCommentStartAndEnd(languageIndex); if (startEnd==null) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); return; } // Don't toggle comment on last line if there is no // text selected on it. if (start!=end) { Element elem = map.getElement(end); if (Math.max(dot, mark)==elem.getStartOffset()) { end--; } } textArea.beginAtomicEdit(); try { boolean add = getDoAdd(doc,map, start,end, startEnd); for (line1=start; line1<=end; line1++) { Element elem = map.getElement(line1); handleToggleComment(elem, doc, startEnd, add); } } catch (BadLocationException ble) { ble.printStackTrace(); UIManager.getLookAndFeel().provideErrorFeedback(textArea); } finally { textArea.endAtomicEdit(); } } private boolean getDoAdd(Document doc, Element map, int startLine, int endLine, String[] startEnd) throws BadLocationException { boolean doAdd = false; for (int i=startLine; i<=endLine; i++) { Element elem = map.getElement(i); int start = elem.getStartOffset(); String t = doc.getText(start, elem.getEndOffset()-start-1); if (!t.startsWith(startEnd[0]) || (startEnd[1]!=null && !t.endsWith(startEnd[1]))) { doAdd = true; break; } } return doAdd; } private void handleToggleComment(Element elem, Document doc, String[] startEnd, boolean add) throws BadLocationException { int start = elem.getStartOffset(); int end = elem.getEndOffset() - 1; if (add) { doc.insertString(start, startEnd[0], null); if (startEnd[1]!=null) { doc.insertString(end+startEnd[0].length(), startEnd[1], null); } } else { doc.remove(start, startEnd[0].length()); if (startEnd[1]!=null) { int temp = startEnd[1].length(); doc.remove(end-startEnd[0].length()-temp, temp); } } } @Override public final String getMacroID() { return rstaToggleCommentAction; } } /** * Toggles the fold at the current caret position or line. */ public static class ToggleCurrentFoldAction extends FoldRelatedAction { private static final long serialVersionUID = 1L; public ToggleCurrentFoldAction() { super(rstaToggleCurrentFoldAction); setProperties(msg, "Action.ToggleCurrentFold"); } public ToggleCurrentFoldAction(String name, Icon icon, String desc, Integer mnemonic, KeyStroke accelerator) { super(name, icon, desc, mnemonic, accelerator); } @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; if (rsta.isCodeFoldingEnabled()) { Fold fold = getClosestFold(rsta); if (fold!=null) { fold.toggleCollapsedState(); } possiblyRepaintGutter(textArea); } else { UIManager.getLookAndFeel().provideErrorFeedback(rsta); } } @Override public final String getMacroID() { return rstaToggleCurrentFoldAction; } } }