diff --git a/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxTextAreaEditorKit.java b/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxTextAreaEditorKit.java index c431e329c..6aa0cd8e4 100644 --- a/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxTextAreaEditorKit.java +++ b/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxTextAreaEditorKit.java @@ -2,29 +2,39 @@ * 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 com.fr.design.gui.syntax.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 com.fr.design.gui.syntax.ui.rsyntaxtextarea.folding.Fold; import com.fr.design.gui.syntax.ui.rsyntaxtextarea.folding.FoldCollapser; import com.fr.design.gui.syntax.ui.rsyntaxtextarea.folding.FoldManager; import com.fr.design.gui.syntax.ui.rsyntaxtextarea.templates.CodeTemplate; import com.fr.design.gui.syntax.ui.rtextarea.Gutter; import com.fr.design.gui.syntax.ui.rtextarea.IconRowHeader; -import com.fr.design.gui.syntax.ui.rtextarea.RecordableTextAction; import com.fr.design.gui.syntax.ui.rtextarea.RTextArea; import com.fr.design.gui.syntax.ui.rtextarea.RTextAreaEditorKit; +import com.fr.design.gui.syntax.ui.rtextarea.RecordableTextAction; +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JScrollPane; +import javax.swing.KeyStroke; +import javax.swing.UIManager; +import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.text.Document; +import javax.swing.text.Element; +import javax.swing.text.Segment; +import javax.swing.text.TextAction; +import java.awt.Component; +import java.awt.Font; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.util.Objects; +import java.util.Stack; /** @@ -32,24 +42,24 @@ import com.fr.design.gui.syntax.ui.rtextarea.RTextAreaEditorKit; * programming-specific stuff. There are currently subclasses to handle: * * * * @author Robert Futrell @@ -57,1884 +67,1872 @@ import com.fr.design.gui.syntax.ui.rtextarea.RTextAreaEditorKit; */ 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"; - - /** - * The actions that 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 by RSyntaxTextAreas. - * - * @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 com.fr.design.i18n.Toolkit.i18nText(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 - if (Character.isLetterOrDigit(ch)) { - if (offs!=end && !Character.isLetterOrDigit(nextCh)) { - return offs; - } - do { - ch = seg.previous(); - } while (Character.isLetterOrDigit(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(); - boolean alignCurlyBraces = rsta.isAutoIndentEnabled() && - doc.getCurlyBracesDenoteCodeBlocks(); - - if (alignCurlyBraces) { - textArea.beginAtomicEdit(); - } - - try { - - textArea.replaceSelection("}"); - - // If the user wants to align curly braces... - if (alignCurlyBraces) { - - Element root = doc.getDefaultRootElement(); - int 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, or null if it - * could not be determined. - */ - private String discoverTagName(RSyntaxDocument doc, int dot) { - - Stack stack = 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.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("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("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; - 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; ipos that - * is NOT a whitespace char, or -1 if only - * whitespace chars follow pos (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; i0 && - // ((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 - if (Character.isLetterOrDigit(ch)) { - do { - ch = seg.previous(); - } while (Character.isLetterOrDigit(ch)); - } - - // Skip groups of "anything else" (operators, etc.). - else if (!Character.isWhitespace(ch)) { - do { - ch = seg.previous(); - } while (ch!=Segment.DONE && - !(Character.isLetterOrDigit(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(); - String[] startEnd = doc.getLineCommentStartAndEnd(); - - if (startEnd==null) { - UIManager.getLookAndFeel().provideErrorFeedback(textArea); - return; - } - - 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); - - // 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("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; - } - - } + 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"; + + /** + * The actions that 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 by RSyntaxTextAreas. + * + * @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 com.fr.design.i18n.Toolkit.i18nText(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 + if (Character.isLetterOrDigit(ch)) { + if (offs != end && !Character.isLetterOrDigit(nextCh)) { + return offs; + } + do { + ch = seg.previous(); + } while (Character.isLetterOrDigit(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(); + boolean alignCurlyBraces = rsta.isAutoIndentEnabled() && + doc.getCurlyBracesDenoteCodeBlocks(); + + if (alignCurlyBraces) { + textArea.beginAtomicEdit(); + } + + try { + + textArea.replaceSelection("}"); + + // If the user wants to align curly braces... + if (alignCurlyBraces) { + + Element root = doc.getDefaultRootElement(); + int 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 < seg.count; i++) { + char ch = seg.array[seg.offset + i]; + if (!Character.isWhitespace(ch)) { + return; + } + } + + // Locate the matching '{' bracket, and replace the leading + // whitespace for the '}' to match that of the '{' char's line. + bracketInfo = RSyntaxUtilities.getMatchingBracketPosition( + rsta, bracketInfo); + if (bracketInfo.y > -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, or null if it + * could not be determined. + */ + private String discoverTagName(RSyntaxDocument doc, int dot) { + + Stack stack = 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 = Objects.requireNonNull(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("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("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 < count; i++) { + Style ss = scheme.getStyle(i); + if (ss != null) { + Font font = ss.font; + if (font != null) { + float oldSize = font.getSize2D(); + float newSize = oldSize - decreaseAmount; + if (newSize >= 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; + try { + for (line1 = dot; line1 < mark; line1++) { + elem = map.getElement(line1); + handleDecreaseIndent(elem, document, tabSize); + } + // Don't do the last line if the caret is at its + // beginning. We must call getDot() again and not just + // use 'dot' as the caret's position may have changed + // due to the insertion of the tabs above. + elem = map.getElement(mark); + int start = elem.getStartOffset(); + if (Math.max(c.getDot(), c.getMark()) != start) { + handleDecreaseIndent(elem, document, tabSize); + } + } catch (BadLocationException ble) { + ble.printStackTrace(); + UIManager.getLookAndFeel(). + provideErrorFeedback(textArea); + } + } else { + Element elem = map.getElement(line1); + try { + handleDecreaseIndent(elem, document, tabSize); + } catch (BadLocationException ble) { + ble.printStackTrace(); + UIManager.getLookAndFeel(). + provideErrorFeedback(textArea); + } + } + + } + + @Override + public final String getMacroID() { + return rstaDecreaseIndentAction; + } + + /** + * Actually does the "de-indentation." This method finds where the + * given element's leading whitespace ends, then, if there is indeed + * leading whitespace, removes either the last char in it (if it is a + * tab), or removes up to the number of spaces equal to a tab in the + * specified document (i.e., if the tab size was 5 and there were 3 + * spaces at the end of the leading whitespace, the three will be + * removed; if there were 8 spaces, only the first 5 would be + * removed). + * + * @param elem The element to "de-indent." + * @param doc The document containing the specified element. + * @param tabSize The size of a tab, in spaces. + */ + private final void handleDecreaseIndent(Element elem, Document doc, + int tabSize) + throws BadLocationException { + int start = elem.getStartOffset(); + int end = elem.getEndOffset() - 1; // Why always true?? + doc.getText(start, end - start, s); + int i = s.offset; + end = i + s.count; + if (end > i) { + // 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 < end && s.array[i] == ' ' && toRemove < tabSize) { + i++; + toRemove++; + } + doc.remove(start, toRemove); + } + } + } + + } + + + /** + * Deletes the previous word, but differentiates symbols from "words" to + * match the behavior of code editors. + */ + public static class DeletePrevWordAction + extends RTextAreaEditorKit.DeletePrevWordAction { + + private Segment seg = new Segment(); + + @Override + protected int getPreviousWordStart(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 - 1; // Just delete the newline + } + 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(); + + // Always strip off whitespace first + if (Character.isWhitespace(ch)) { + do { + ch = seg.previous(); + } while (Character.isWhitespace(ch)); + } + + // The "word" is a group of letters and/or digits + if (Character.isLetterOrDigit(ch)) { + do { + ch = seg.previous(); + } while (Character.isLetterOrDigit(ch)); + } + + // The "word" is a series of symbols. + else { + while (!Character.isWhitespace(ch) && + !Character.isLetterOrDigit(ch) + && ch != Segment.DONE) { + ch = seg.previous(); + } + } + + if (ch == Segment.DONE) { + return start; // Removed last "token" of the line + } + offs -= firstIndex - seg.getIndex(); + return offs; + + } + + } + + + /** + * Moves the caret to the end of the document, taking into account code + * folding. + */ + public static class EndAction extends RTextAreaEditorKit.EndAction { + + public EndAction(String name, boolean select) { + super(name, select); + } + + @Override + protected int getVisibleEnd(RTextArea textArea) { + RSyntaxTextArea rsta = (RSyntaxTextArea) textArea; + return rsta.getLastVisibleOffset(); + } + + } + + + /** + * Positions the caret at the end of the word. This class is here to + * better handle finding the "end of the word" in programming languages. + */ + protected static class EndWordAction + extends RTextAreaEditorKit.EndWordAction { + + private Segment seg; + + protected EndWordAction(String name, boolean select) { + super(name, select); + seg = new Segment(); + } + + @Override + protected int getWordEnd(RTextArea textArea, int offs) + throws BadLocationException { + + RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument(); + if (offs == doc.getLength()) { + return offs; + } + + int line = textArea.getLineOfOffset(offs); + int end = textArea.getLineEndOffset(line); + if (line != textArea.getLineCount() - 1) { + end--; // Hide newline + } + if (offs == end) { + return end; + } + doc.getText(offs, end - offs, seg); + + // Determine the "type" of char at offs - letter/digit, + // whitespace or other + char ch = seg.first(); + + // The "word" is a group of letters and/or digits + if (Character.isLetterOrDigit(ch)) { + do { + ch = seg.next(); + } while (Character.isLetterOrDigit(ch)); + } + + // The "word" is whitespace. + else if (Character.isWhitespace(ch)) { + + do { + ch = seg.next(); + } while (Character.isWhitespace(ch)); + } + + // Otherwise, the "word" is a single character of some other type + // (operator, etc.). + + offs += seg.getIndex() - seg.getBeginIndex(); + return offs; + + } + + } + + + /** + * Expands all folds. + */ + public static class ExpandAllFoldsAction extends FoldRelatedAction { + + private static final long serialVersionUID = 1L; + + public ExpandAllFoldsAction() { + this(false); + } + + public ExpandAllFoldsAction(boolean localizedName) { + super(rstaExpandAllFoldsAction); + if (localizedName) { + setProperties("Action.ExpandAllFolds"); + } + } + + public ExpandAllFoldsAction(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()) { + FoldManager fm = rsta.getFoldManager(); + for (int i = 0; i < fm.getFoldCount(); i++) { + expand(fm.getFold(i)); + } + possiblyRepaintGutter(rsta); + } else { + UIManager.getLookAndFeel().provideErrorFeedback(rsta); + } + } + + private void expand(Fold fold) { + fold.setCollapsed(false); + for (int i = 0; i < fold.getChildCount(); i++) { + expand(fold.getChild(i)); + } + } + + @Override + public final String getMacroID() { + return rstaExpandAllFoldsAction; + } + + } + + + /** + * Base class for folding-related actions. + */ + static abstract class FoldRelatedAction extends RecordableTextAction { + + public FoldRelatedAction(String name) { + super(name); + } + + public FoldRelatedAction(String name, Icon icon, + String desc, Integer mnemonic, KeyStroke accelerator) { + super(name, icon, desc, mnemonic, accelerator); + } + + protected Fold getClosestFold(RSyntaxTextArea textArea) { + int offs = textArea.getCaretPosition(); + int line = textArea.getCaretLineNumber(); + FoldManager fm = textArea.getFoldManager(); + Fold fold = fm.getFoldForLine(line); + if (fold == null) { + fold = fm.getDeepestOpenFoldContaining(offs); + } + return fold; + } + + /** + * Repaints the gutter in a text area's scroll pane, if necessary. + * + * @param textArea The text area. + */ + protected void possiblyRepaintGutter(RTextArea textArea) { + Gutter gutter = RSyntaxUtilities.getGutter(textArea); + if (gutter != null) { + gutter.repaint(); + } + } + + } + + + /** + * Action for moving the caret to the "matching bracket" of the bracket + * at the caret position (either before or after). + */ + public static class GoToMatchingBracketAction + extends RecordableTextAction { + + private static final long serialVersionUID = 1L; + + private Point bracketInfo; + + public GoToMatchingBracketAction() { + super(rstaGoToMatchingBracketAction); + } + + public GoToMatchingBracketAction(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; + bracketInfo = RSyntaxUtilities.getMatchingBracketPosition(rsta, + bracketInfo); + if (bracketInfo.y > -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 < count; i++) { + Style ss = scheme.getStyle(i); + if (ss != null) { + Font font = ss.font; + if (font != null) { + float oldSize = font.getSize2D(); + float newSize = oldSize + increaseAmount; + if (newSize <= MAXIMUM_SIZE) { + // Grow by increaseAmount. + ss.font = font.deriveFont(newSize); + changed = true; + } else if (oldSize < MAXIMUM_SIZE) { + // Can't grow by full increaseAmount, but + // can grow a little bit. + ss.font = font.deriveFont(MAXIMUM_SIZE); + changed = true; + } + } + } + } + + // Do the text area's font also. + Font font = rsta.getFont(); + float oldSize = font.getSize2D(); + float newSize = oldSize + increaseAmount; + if (newSize <= MAXIMUM_SIZE) { + // Grow by increaseAmount. + rsta.setFont(font.deriveFont(newSize)); + changed = true; + } else if (oldSize < MAXIMUM_SIZE) { + // Can't grow by full increaseAmount, but + // can grow a little bit. + rsta.setFont(font.deriveFont(MAXIMUM_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 the user presses the Enter key. This is here so we can + * be smart and "auto-indent" for programming languages. + */ + public static class InsertBreakAction + extends RTextAreaEditorKit.InsertBreakAction { + + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { + + if (!textArea.isEditable() || !textArea.isEnabled()) { + UIManager.getLookAndFeel().provideErrorFeedback(textArea); + return; + } + + RSyntaxTextArea sta = (RSyntaxTextArea) textArea; + boolean noSelection = sta.getSelectionStart() == sta.getSelectionEnd(); + + // First, see if this language wants to handle inserting newlines + // itself. + boolean handled = false; + if (noSelection) { + RSyntaxDocument doc = (RSyntaxDocument) sta.getDocument(); + handled = doc.insertBreakSpecialHandling(e); + } + + // If not... + if (!handled) { + handleInsertBreak(sta, noSelection); + } + + } + + /** + * @return The first location in the string past pos that + * is NOT a whitespace char, or -1 if only + * whitespace chars follow pos (or it is the end + * position in the string). + */ + private static final int atEndOfLine(int pos, String s, int sLen) { + for (int i = pos; i < sLen; i++) { + if (!RSyntaxUtilities.isWhitespace(s.charAt(i))) { + return i; + } + } + return -1; + } + + private static final int getOpenBraceCount(RSyntaxDocument doc) { + int openCount = 0; + Element root = doc.getDefaultRootElement(); + int lineCount = root.getElementCount(); + for (int i = 0; i < lineCount; i++) { + Token t = doc.getTokenListForLine(i); + while (t != null && t.isPaintable()) { + if (t.getType() == Token.SEPARATOR && t.length() == 1) { + char ch = t.charAt(0); + if (ch == '{') { + openCount++; + } else if (ch == '}') { + openCount--; + } + } + t = t.getNextToken(); + } + } + return openCount; + } + + /** + * Actually inserts the newline into the document, and auto-indents + * if appropriate. This method can be called by token makers who + * implement a custom action for inserting newlines. + * + * @param textArea + * @param noSelection Whether there is no selection. + */ + protected void handleInsertBreak(RSyntaxTextArea textArea, + boolean noSelection) { + // If we're auto-indenting... + if (noSelection && textArea.isAutoIndentEnabled()) { + insertNewlineWithAutoIndent(textArea); + } else { + textArea.replaceSelection("\n"); + if (noSelection) { + possiblyCloseCurlyBrace(textArea, null); + } + } + } + + private void insertNewlineWithAutoIndent(RSyntaxTextArea sta) { + + try { + + int caretPos = sta.getCaretPosition(); + Document doc = sta.getDocument(); + Element map = doc.getDefaultRootElement(); + int lineNum = map.getElementIndex(caretPos); + Element line = map.getElement(lineNum); + int start = line.getStartOffset(); + int end = line.getEndOffset() - 1; // Why always "-1"? + int len = end - start; + String s = doc.getText(start, len); + + // endWS is the end of the leading whitespace of the + // current line. + String leadingWS = RSyntaxUtilities.getLeadingWhitespace(s); + StringBuilder sb = new StringBuilder("\n"); + sb.append(leadingWS); + + // If there is only whitespace between the caret and + // the EOL, pressing Enter auto-indents the new line to + // the same place as the previous line. + int nonWhitespacePos = atEndOfLine(caretPos - start, s, len); + if (nonWhitespacePos == -1) { + if (leadingWS.length() == len && + sta.isClearWhitespaceLinesEnabled()) { + // If the line was nothing but whitespace, select it + // so its contents get removed. + sta.setSelectionStart(start); + sta.setSelectionEnd(end); + } + sta.replaceSelection(sb.toString()); + } + + // If there is non-whitespace between the caret and the + // EOL, pressing Enter takes that text to the next line + // and auto-indents it to the same place as the last + // line. + else { + sb.append(s.substring(nonWhitespacePos)); + sta.replaceRange(sb.toString(), caretPos, end); + sta.setCaretPosition(caretPos + leadingWS.length() + 1); + } + + // Must do it after everything else, as the "smart indent" + // calculation depends on the previous line's state + // AFTER the Enter press (stuff may have been moved down). + if (sta.getShouldIndentNextLine(lineNum)) { + sta.replaceSelection("\t"); + } + + possiblyCloseCurlyBrace(sta, leadingWS); + + } catch (BadLocationException ble) { // Never happens + sta.replaceSelection("\n"); + ble.printStackTrace(); + } + + } + + private void possiblyCloseCurlyBrace(RSyntaxTextArea textArea, + String leadingWS) { + + RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument(); + + if (textArea.getCloseCurlyBraces() && + doc.getCurlyBracesDenoteCodeBlocks()) { + + int line = textArea.getCaretLineNumber(); + Token t = doc.getTokenListForLine(line - 1); + t = t.getLastNonCommentNonWhitespaceToken(); + + if (t != null && t.isLeftCurly()) { + + if (getOpenBraceCount(doc) > 0) { + 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 < temp; i++) { + sb.append(' '); + } + replacement = sb.toString(); + } + + textArea.beginAtomicEdit(); + try { + for (int i = first; i < last; i++) { + elem = map.getElement(i); + start = elem.getStartOffset(); + document.insertString(start, replacement, null); + } + // Don't do the last line if the caret is at its + // beginning. We must call getDot() again and not just + // use 'dot' as the caret's position may have changed + // due to the insertion of the tabs above. + elem = map.getElement(last); + start = elem.getStartOffset(); + if (Math.max(c.getDot(), c.getMark()) != start) { + document.insertString(start, replacement, null); + } + } catch (BadLocationException ble) { // Never happens. + ble.printStackTrace(); + UIManager.getLookAndFeel(). + provideErrorFeedback(textArea); + } finally { + textArea.endAtomicEdit(); + } + } else { + textArea.replaceSelection("\t"); + } + + } + + @Override + public final String getMacroID() { + return insertTabAction; + } + + } + + + /** + * 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 NextWordAction + extends RTextAreaEditorKit.NextWordAction { + + private Segment seg; + + public NextWordAction(String nm, boolean select) { + super(nm, select); + seg = new Segment(); + } + + /** + * Overridden to do better with skipping "words" in code. + */ + @Override + protected int getNextWord(RTextArea textArea, int offs) + throws BadLocationException { + + RSyntaxDocument doc = (RSyntaxDocument) textArea.getDocument(); + if (offs == doc.getLength()) { + return offs; + } + + Element root = doc.getDefaultRootElement(); + int line = root.getElementIndex(offs); + int end = root.getElement(line).getEndOffset() - 1; + if (offs == end) {// If we're already at the end of the line... + RSyntaxTextArea rsta = (RSyntaxTextArea) textArea; + if (rsta.isCodeFoldingEnabled()) { // Start of next visible line + FoldManager fm = rsta.getFoldManager(); + int lineCount = root.getElementCount(); + while (++line < lineCount && fm.isLineHidden(line)) { + ; + } + if (line < lineCount) { // Found a lower visible line + offs = root.getElement(line).getStartOffset(); + } + // No lower visible line - we're already at last visible offset + return offs; + } else { + return offs + 1; // Start of next line. + } + } + doc.getText(offs, end - offs, seg); + + // Determine the "type" of char at offs - letter/digit, + // whitespace or other + char ch = seg.first(); + + // Skip the group of letters and/or digits + if (Character.isLetterOrDigit(ch)) { + do { + ch = seg.next(); + } while (Character.isLetterOrDigit(ch)); + } + + // Skip groups of "anything else" (operators, etc.). + else if (!Character.isWhitespace(ch)) { + do { + ch = seg.next(); + } while (ch != Segment.DONE && + !(Character.isLetterOrDigit(ch) || + Character.isWhitespace(ch))); + } + + // Skip any trailing whitespace + while (Character.isWhitespace(ch)) { + ch = seg.next(); + } + + offs += seg.getIndex() - seg.getBeginIndex(); + return offs; + + } + + } + + + /** + * Action for when the user tries to insert a template (that is, + * they've typed a template ID and pressed the trigger character + * (a space) in an attempt to do the substitution). + */ + public static class PossiblyInsertTemplateAction extends RecordableTextAction { + + private static final long serialVersionUID = 1L; + + public PossiblyInsertTemplateAction() { + super(rstaPossiblyInsertTemplateAction); + } + + @Override + public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { + + if (!textArea.isEditable() || !textArea.isEnabled()) { + return; + } + + RSyntaxTextArea rsta = (RSyntaxTextArea) textArea; + + if (RSyntaxTextArea.getTemplatesEnabled()) { + + Document doc = textArea.getDocument(); + if (doc != null) { + + try { + + CodeTemplateManager manager = RSyntaxTextArea. + getCodeTemplateManager(); + CodeTemplate template = manager == null ? null : + manager.getTemplate(rsta); + + // A non-null template means modify the text to insert! + if (template != null) { + template.invoke(rsta); + } + + // No template - insert default text. This is + // exactly what DefaultKeyTypedAction does. + else { + doDefaultInsert(rsta); + } + + } catch (BadLocationException ble) { + UIManager.getLookAndFeel(). + provideErrorFeedback(textArea); + } + + + } // End of if (doc!=null). + + } // End of if (textArea.getTemplatesEnabled()). + + // If templates aren't enabled, just insert the text as usual. + else { + doDefaultInsert(rsta); + } + + } + + private final void doDefaultInsert(RTextArea textArea) { + // FIXME: We need a way to get the "trigger string" (i.e., + // the text that was just typed); however, the text area's + // template manager might be null (if templates are disabled). + // Also, the manager's trigger string doesn't yet match up with + // that defined in RSyntaxTextAreaEditorKit.java (which is + // hardcoded as a space)... + //String str = manager.getInsertTriggerString(); + //int mod = manager.getInsertTrigger().getModifiers(); + //if (str!=null && str.length()>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 + if (Character.isLetterOrDigit(ch)) { + do { + ch = seg.previous(); + } while (Character.isLetterOrDigit(ch)); + } + + // Skip groups of "anything else" (operators, etc.). + else if (!Character.isWhitespace(ch)) { + do { + ch = seg.previous(); + } while (ch != Segment.DONE && + !(Character.isLetterOrDigit(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(); + String[] startEnd = doc.getLineCommentStartAndEnd(); + + if (startEnd == null) { + UIManager.getLookAndFeel().provideErrorFeedback(textArea); + return; + } + + 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); + + // 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("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; + } + + } } \ No newline at end of file diff --git a/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxUtilities.java b/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxUtilities.java index 87be45892..ddb3ddfa3 100644 --- a/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxUtilities.java +++ b/designer-base/src/main/java/com/fr/design/gui/syntax/ui/rsyntaxtextarea/RSyntaxUtilities.java @@ -3,12 +3,30 @@ * * RSyntaxUtilities.java - Utility methods used by RSyntaxTextArea and its * views. - * + * * This library is distributed under a modified BSD license. See the included * RSyntaxTextArea.License.txt file for details. */ package com.fr.design.gui.syntax.ui.rsyntaxtextarea; +import com.fr.design.gui.syntax.ui.rsyntaxtextarea.TokenUtils.TokenSubList; +import com.fr.design.gui.syntax.ui.rsyntaxtextarea.folding.FoldManager; +import com.fr.design.gui.syntax.ui.rtextarea.Gutter; +import com.fr.design.gui.syntax.ui.rtextarea.RTextArea; +import com.fr.design.gui.syntax.ui.rtextarea.RTextScrollPane; + +import javax.swing.JLabel; +import javax.swing.JViewport; +import javax.swing.SwingConstants; +import javax.swing.UIManager; +import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.text.Document; +import javax.swing.text.Element; +import javax.swing.text.Position; +import javax.swing.text.Segment; +import javax.swing.text.TabExpander; +import javax.swing.text.View; import java.awt.Color; import java.awt.Container; import java.awt.Point; @@ -16,23 +34,9 @@ import java.awt.Rectangle; import java.awt.Shape; import java.awt.Toolkit; import java.util.Map; +import java.util.Objects; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import javax.swing.*; -import javax.swing.text.BadLocationException; -import javax.swing.text.Caret; -import javax.swing.text.Document; -import javax.swing.text.Element; -import javax.swing.text.Position; -import javax.swing.text.Segment; -import javax.swing.text.TabExpander; -import javax.swing.text.View; - -import com.fr.design.gui.syntax.ui.rsyntaxtextarea.TokenUtils.TokenSubList; -import com.fr.design.gui.syntax.ui.rsyntaxtextarea.folding.FoldManager; -import com.fr.design.gui.syntax.ui.rtextarea.Gutter; -import com.fr.design.gui.syntax.ui.rtextarea.RTextArea; -import com.fr.design.gui.syntax.ui.rtextarea.RTextScrollPane; /** @@ -44,1267 +48,1276 @@ import com.fr.design.gui.syntax.ui.rtextarea.RTextScrollPane; */ public class RSyntaxUtilities implements SwingConstants { - /** - * Integer constant representing a Windows-variant OS. - */ - public static final int OS_WINDOWS = 1; - - /** - * Integer constant representing Mac OS X. - */ - public static final int OS_MAC_OSX = 2; - - /** - * Integer constant representing Linux. - */ - public static final int OS_LINUX = 4; - - /** - * Integer constant representing an "unknown" OS. 99.99% of the - * time, this means some UNIX variant (AIX, SunOS, etc.). - */ - public static final int OS_OTHER = 8; - - /** - * Used for the color of hyperlinks when a LookAndFeel uses light text - * against a dark background. - */ - private static final Color LIGHT_HYPERLINK_FG = new Color(0xd8ffff); - - private static final int OS = getOSImpl(); - - //private static final int DIGIT_MASK = 1; - private static final int LETTER_MASK = 2; - //private static final int WHITESPACE_MASK = 4; - //private static final int UPPER_CASE_MASK = 8; - private static final int HEX_CHARACTER_MASK = 16; - private static final int LETTER_OR_DIGIT_MASK = 32; - private static final int BRACKET_MASK = 64; - private static final int JAVA_OPERATOR_MASK = 128; - - /** - * A lookup table used to quickly decide if a 16-bit Java char is a - * US-ASCII letter (A-Z or a-z), a digit, a whitespace char (either space - * (0x0020) or tab (0x0009)), etc. This method should be faster - * than Character.isLetter, Character.isDigit, - * and Character.isWhitespace because we know we are dealing - * with ASCII chars and so don't have to worry about code planes, etc. - */ - private static final int[] dataTable = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, // 0-15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 - 4, 128, 0, 0, 0, 128, 128, 0, 64, 64, 128, 128, 0, 128, 0, 128, // 32-47 - 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 128, 0, 128, 128, 128, 128, // 48-63 - 0, 58, 58, 58, 58, 58, 58, 42, 42, 42, 42, 42, 42, 42, 42, 42, // 64-79 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 64, 0, 64, 128, 0, // 80-95 - 0, 50, 50, 50, 50, 50, 50, 34, 34, 34, 34, 34, 34, 34, 34, 34, // 96-111 - 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 64, 128, 64, 128, 0, // 112-127 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144- - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160- - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176- - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192- - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208- - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224- - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255. - }; - - /** - * Used in bracket matching methods. - */ - private static Segment charSegment = new Segment(); - - /** - * Used in token list manipulation methods. - */ - private static final TokenImpl tempToken = new TokenImpl(); - - /** - * Used internally. - */ - private static final char[] JS_KEYWORD_RETURN = { 'r', 'e', 't', 'u', 'r', 'n' }; - - /** - * Used internally. - */ - private static final String BRACKETS = "{([})]"; - - - /** - * Returns a string with characters that are special to HTML (such as - * <, > and &) replaced - * by their HTML escape sequences. - * - * @param s The input string. - * @param newlineReplacement What to replace newline characters with. - * If this is null, they are simply removed. - * @param inPreBlock Whether this HTML will be in within pre - * tags. If this is true, spaces will be kept as-is; - * otherwise, they will be converted to " ". - * @return The escaped version of s. - */ - public static final String escapeForHtml(String s, - String newlineReplacement, boolean inPreBlock) { - - if (s==null) { - return null; - } - if (newlineReplacement==null) { - newlineReplacement = ""; - } - final String tabString = " "; - boolean lastWasSpace = false; - - StringBuilder sb = new StringBuilder(); - - for (int i=0; i': - sb.append(">"); - lastWasSpace = false; - break; - default: - sb.append(ch); - lastWasSpace = false; - break; - } - } - - return sb.toString(); - - } - - - /** - * Returns the rendering hints for text that will most accurately reflect - * those of the native windowing system. - * - * @return The rendering hints, or null if they cannot be - * determined. - */ - public static Map getDesktopAntiAliasHints() { - return (Map)Toolkit.getDefaultToolkit(). - getDesktopProperty("awt.font.desktophints"); - } - - - /** - * Returns the color to use for the line underneath a folded region line. - * - * @param textArea The text area. - * @return The color to use. - */ - public static Color getFoldedLineBottomColor(RSyntaxTextArea textArea) { - Color color = Color.gray; - Gutter gutter = RSyntaxUtilities.getGutter(textArea); - if (gutter!=null) { - color = gutter.getFoldIndicatorForeground(); - } - return color; - } - - - /** - * Returns the gutter component of the scroll pane containing a text - * area, if any. - * - * @param textArea The text area. - * @return The gutter, or null if the text area is not in - * an {@link RTextScrollPane}. - * @see RTextScrollPane#getGutter() - */ - public static Gutter getGutter(RTextArea textArea) { - Gutter gutter = null; - Container parent = textArea.getParent(); - if (parent instanceof JViewport) { - parent = parent.getParent(); - if (parent instanceof RTextScrollPane) { - RTextScrollPane sp = (RTextScrollPane)parent; - gutter = sp.getGutter(); // Should always be non-null - } - } - return gutter; - } - - - /** - * Returns the color to use for hyperlink-style components. This method - * will return Color.blue unless it appears that the current - * LookAndFeel uses light text on a dark background, in which case a - * brighter alternative is returned. - * - * @return The color to use for hyperlinks. - * @see #isLightForeground(Color) - */ - public static final Color getHyperlinkForeground() { - - // This property is defined by all standard LaFs, even Nimbus (!), - // but you never know what crazy LaFs there are... - Color fg = UIManager.getColor("Label.foreground"); - if (fg==null) { - fg = new JLabel().getForeground(); - } - - return isLightForeground(fg) ? LIGHT_HYPERLINK_FG : Color.blue; - - } - - - /** - * Returns the leading whitespace of a string. - * - * @param text The String to check. - * @return The leading whitespace. - * @see #getLeadingWhitespace(Document, int) - */ - public static String getLeadingWhitespace(String text) { - int count = 0; - int len = text.length(); - while (countoffs is not a valid offset - * in the document. - * @see #getLeadingWhitespace(String) - */ - public static String getLeadingWhitespace(Document doc, int offs) - throws BadLocationException { - Element root = doc.getDefaultRootElement(); - int line = root.getElementIndex(offs); - Element elem = root.getElement(line); - int startOffs = elem.getStartOffset(); - int endOffs = elem.getEndOffset() - 1; - String text = doc.getText(startOffs, endOffs-startOffs); - return getLeadingWhitespace(text); - } - - - private static final Element getLineElem(Document d, int offs) { - Element map = d.getDefaultRootElement(); - int index = map.getElementIndex(offs); - Element elem = map.getElement(index); - if ((offs>=elem.getStartOffset()) && (offsp0, as this is - * the character where the x-pixel value is 0. - * - * @param textArea The text area containing the text. - * @param s A segment in which to load the line. This is passed in so we - * don't have to reallocate a new Segment for each - * call. - * @param p0 The starting position in the physical line in the document. - * @param p1 The position for which to get the bounding box in the view. - * @param e How to expand tabs. - * @param rect The rectangle whose x- and width-values are changed to - * represent the bounding box of p1. This is reused - * to keep from needlessly reallocating Rectangles. - * @param x0 The x-coordinate (pixel) marking the left-hand border of the - * text. This is useful if the text area has a border, for example. - * @return The bounding box in the view of the character p1. - * @throws BadLocationException If p0 or p1 is - * not a valid location in the specified text area's document. - * @throws IllegalArgumentException If p0 and p1 - * are not on the same line. - */ - public static Rectangle getLineWidthUpTo(RSyntaxTextArea textArea, - Segment s, int p0, int p1, - TabExpander e, Rectangle rect, - int x0) - throws BadLocationException { - - RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument(); - - // Ensure p0 and p1 are valid document positions. - if (p0<0) - throw new BadLocationException("Invalid document position", p0); - else if (p1>doc.getLength()) - throw new BadLocationException("Invalid document position", p1); - - // Ensure p0 and p1 are in the same line, and get the start/end - // offsets for that line. - Element map = doc.getDefaultRootElement(); - int lineNum = map.getElementIndex(p0); - // We do ">1" because p1 might be the first position on the next line - // or the last position on the previous one. - // if (lineNum!=map.getElementIndex(p1)) - if (Math.abs(lineNum-map.getElementIndex(p1))>1) - throw new IllegalArgumentException("p0 and p1 are not on the " + - "same line (" + p0 + ", " + p1 + ")."); - - // Get the token list. - Token t = doc.getTokenListForLine(lineNum); - - // Modify the token list 't' to begin at p0 (but still have correct - // token types, etc.), and get the x-location (in pixels) of the - // beginning of this new token list. - TokenSubList subList = TokenUtils.getSubTokenList(t, p0, e, textArea, - 0, tempToken); - t = subList.tokenList; - - rect = t.listOffsetToView(textArea, e, p1, x0, rect); - return rect; - - } - - - /** - * Returns the location of the bracket paired with the one at the current - * caret position. - * - * @param textArea The text area. - * @param input A point to use as the return value. If this is - * null, a new object is created and returned. - * @return A point representing the matched bracket info. The "x" field - * is the offset of the bracket at the caret position (either just - * before or just after the caret), and the "y" field is the offset - * of the matched bracket. Both "x" and "y" will be - * -1 if there isn't a matching bracket (or the caret - * isn't on a bracket). - */ - public static Point getMatchingBracketPosition(RSyntaxTextArea textArea, - Point input) { - - if (input==null) { - input = new Point(); - } - input.setLocation(-1, -1); - - try { - - // Actually position just BEFORE caret. - int caretPosition = textArea.getCaretPosition() - 1; - RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument(); - char bracket = 0; - - // If the caret was at offset 0, we can't check "to its left." - if (caretPosition>=0) { - bracket = doc.charAt(caretPosition); - } - - // Try to match a bracket "to the right" of the caret if one - // was not found on the left. - int index = BRACKETS.indexOf(bracket); - if (index==-1 && caretPosition