/*
 * 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;


/**
 * An extension of <code>RTextAreaEditorKit</code> that adds functionality for
 * programming-specific stuff.  There are currently subclasses to handle:
 *
 * <ul>
 *   <li>Toggling code folds.</li>
 *   <li>Aligning "closing" curly braces with their matches, if the current
 *       programming language uses curly braces to identify code blocks.</li>
 *   <li>Copying the current selection as RTF.</li>
 *   <li>Block indentation (increasing the indent of one or multiple lines)</li>
 *   <li>Block un-indentation (decreasing the indent of one or multiple lines)
 *       </li>
 *   <li>Inserting a "code template" when a configurable key (e.g. a space) is
 *       pressed</li>
 *   <li>Decreasing the point size of all fonts in the text area</li>
 *   <li>Increasing the point size of all fonts in the text area</li>
 *   <li>Moving the caret to the "matching bracket" of the one at the current
 *       caret position</li>
 *   <li>Toggling whether the currently selected lines are commented out.</li>
 *   <li>Better selection of "words" on mouse double-clicks for programming
 *       languages.</li>
 *   <li>Better keyboard navigation via Ctrl+arrow keys for programming
 *       languages.</li>
 * </ul>
 *
 * @author Robert Futrell
 * @version 0.5
 */
public class RSyntaxTextAreaEditorKit extends RTextAreaEditorKit {

	private static final long serialVersionUID = 1L;

	public static final String rstaCloseCurlyBraceAction	= "RSTA.CloseCurlyBraceAction";
	public static final String rstaCloseMarkupTagAction		= "RSTA.CloseMarkupTagAction";
	public static final String rstaCollapseAllFoldsAction	= "RSTA.CollapseAllFoldsAction";
	public static final String rstaCollapseAllCommentFoldsAction = "RSTA.CollapseAllCommentFoldsAction";
	public static final String rstaCollapseFoldAction		= "RSTA.CollapseFoldAction";
	public static final String rstaCopyAsRtfAction			= "RSTA.CopyAsRtfAction";
	public static final String rstaDecreaseIndentAction		= "RSTA.DecreaseIndentAction";
	public static final String rstaExpandAllFoldsAction		= "RSTA.ExpandAllFoldsAction";
	public static final String rstaExpandFoldAction			= "RSTA.ExpandFoldAction";
	public static final String rstaGoToMatchingBracketAction	= "RSTA.GoToMatchingBracketAction";
	public static final String rstaPossiblyInsertTemplateAction = "RSTA.TemplateAction";
	public static final String rstaToggleCommentAction 		= "RSTA.ToggleCommentAction";
	public static final String rstaToggleCurrentFoldAction	= "RSTA.ToggleCurrentFoldAction";

	private static final String MSG	= "com.fr.design.gui.syntax.ui.rsyntaxtextarea.RSyntaxTextArea";
	private static final ResourceBundle msg = ResourceBundle.getBundle(MSG);


	/**
	 * The actions that <code>RSyntaxTextAreaEditorKit</code> adds to those of
	 * <code>RTextAreaEditorKit</code>.
	 */
	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 <code>RSyntaxTextArea</code>s.
	 *
	 * @return The document.
	 */
	@Override
	public Document createDefaultDocument() {
		return new RSyntaxDocument(SyntaxConstants.SYNTAX_STYLE_NONE);
	}


	/**
	 * Overridden to return a row header that is aware of folding.
	 *
	 * @param textArea The text area.
	 * @return The icon row header.
	 */
	@Override
	public IconRowHeader createIconRowHeader(RTextArea textArea) {
		return new FoldingAwareIconRowHeader((RSyntaxTextArea)textArea);
	}


	/**
	 * Fetches the set of commands that can be used
	 * on a text component that is using a model and
	 * view produced by this kit.
	 *
	 * @return the command list
	 */ 
	@Override
	public Action[] getActions() {
		return TextAction.augmentList(super.getActions(),
							RSyntaxTextAreaEditorKit.defaultActions);
	}


	/**
	 * Returns localized text for an action.  There's definitely a better place
	 * for this functionality.
	 *
	 * @param key The key into the action resource bundle.
	 * @return The localized text.
	 */
	public static String getString(String key) {
		return msg.getString(key);
	}


	/**
	 * Positions the caret at the beginning of the word.  This class is here
	 * to better handle finding the "beginning of the word" for programming
	 * languages.
	 */
	protected static class BeginWordAction
							extends RTextAreaEditorKit.BeginWordAction {

		private Segment seg;

		protected BeginWordAction(String name, boolean select) {
			super(name, select);
			seg = new Segment();
		}

		@Override
		protected int getWordStart(RTextArea textArea, int offs)
										throws BadLocationException {

			if (offs==0) {
				return offs;
			}

			RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
			int line = textArea.getLineOfOffset(offs);
			int start = textArea.getLineStartOffset(line);
			if (offs==start) {
				return start;
			}
			int end = textArea.getLineEndOffset(line);
			if (line!=textArea.getLineCount()-1) {
				end--;
			}
			doc.getText(start, end-start, seg);

			// Determine the "type" of char at offs - lower case, upper case,
			// whitespace or other.  We take special care here as we're starting
			// in the middle of the Segment to check whether we're already at
			// the "beginning" of a word.
			int firstIndex = seg.getBeginIndex() + (offs-start) - 1;
			seg.setIndex(firstIndex);
			char ch = seg.current();
			char nextCh = offs==end ? 0 : seg.array[seg.getIndex() + 1];

			// The "word" is a group of letters and/or digits
			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. "<code>&lt;/</code>"
		 *        or "<code>[</code>" in the case of BBCode).
		 * @return The name of the tag to close, or <code>null</code> if it
		 *         could not be determined.
		 */
		private String discoverTagName(RSyntaxDocument doc, int dot) {

			Stack<String> stack = new Stack<String>();

			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(msg, "Action.CollapseCommentFolds");
		}

		public CollapseAllCommentFoldsAction(String name, Icon icon,
				String desc, Integer mnemonic, KeyStroke accelerator) {
			super(name, icon, desc, mnemonic, accelerator);
		}

		@Override
		public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
			RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
			if (rsta.isCodeFoldingEnabled()) {
				FoldCollapser collapser = new FoldCollapser();
				collapser.collapseFolds(rsta.getFoldManager());
				possiblyRepaintGutter(textArea);
			}
			else {
				UIManager.getLookAndFeel().provideErrorFeedback(rsta);
			}
		}

		@Override
		public final String getMacroID() {
			return rstaCollapseAllCommentFoldsAction;
		}

	}


	/**
	 * Collapses all folds.
	 */
	public static class CollapseAllFoldsAction extends FoldRelatedAction {

		private static final long serialVersionUID = 1L;

		public CollapseAllFoldsAction() {
			this(false);
		}

		public CollapseAllFoldsAction(boolean localizedName) {
			super(rstaCollapseAllFoldsAction);
			if (localizedName) {
				setProperties(msg, "Action.CollapseAllFolds");
			}
		}

		public CollapseAllFoldsAction(String name, Icon icon,
				String desc, Integer mnemonic, KeyStroke accelerator) {
			super(name, icon, desc, mnemonic, accelerator);
		}

		@Override
		public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
			RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
			if (rsta.isCodeFoldingEnabled()) {
				FoldCollapser collapser = new FoldCollapser() {
					@Override
					public boolean getShouldCollapse(Fold fold) {
						return true;
					}
				};
				collapser.collapseFolds(rsta.getFoldManager());
				possiblyRepaintGutter(textArea);
			}
			else {
				UIManager.getLookAndFeel().provideErrorFeedback(rsta);
			}
		}

		@Override
		public final String getMacroID() {
			return rstaCollapseAllFoldsAction;
		}

	}


	/**
	 * Action for copying text as RTF.
	 */
	public static class CopyAsRtfAction extends RecordableTextAction {

		private static final long serialVersionUID = 1L;

		public CopyAsRtfAction() {
			super(rstaCopyAsRtfAction);
		}

		public CopyAsRtfAction(String name, Icon icon, String desc,
					Integer mnemonic, KeyStroke accelerator) {
			super(name, icon, desc, mnemonic, accelerator);
		}

		@Override
		public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
			((RSyntaxTextArea)textArea).copyAsRtf();
			textArea.requestFocusInWindow();
		}

		@Override
		public final String getMacroID() {
			return getName();
		}

	}


	/**
	 * Action for decreasing the font size of all fonts in the text area.
	 */
	public static class DecreaseFontSizeAction
					extends RTextAreaEditorKit.DecreaseFontSizeAction {

		private static final long serialVersionUID = 1L;

		public DecreaseFontSizeAction() {
			super();
		}

		public DecreaseFontSizeAction(String name, Icon icon, String desc,
							Integer mnemonic, KeyStroke accelerator) {
			super(name, icon, desc, mnemonic, accelerator);
		}

		@Override
		public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {

			RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
			SyntaxScheme scheme = rsta.getSyntaxScheme();

			// All we need to do is update all of the fonts in syntax
			// schemes, then call setSyntaxHighlightingColorScheme with the
			// same scheme already being used.  This relies on the fact that
			// that method does not check whether the new scheme is different
			// from the old scheme before updating.

			boolean changed = false;
			int count = scheme.getStyleCount();
			for (int i=0; i<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(msg, "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 <code>pos</code> that
		 *         is NOT a whitespace char, or <code>-1</code> if only
		 *         whitespace chars follow <code>pos</code> (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(msg, "Action.ToggleCurrentFold");
		}

		public ToggleCurrentFoldAction(String name, Icon icon, String desc,
					Integer mnemonic, KeyStroke accelerator) {
			super(name, icon, desc, mnemonic, accelerator);
		}

		@Override
		public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
			RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
			if (rsta.isCodeFoldingEnabled()) {
				Fold fold = getClosestFold(rsta);
				if (fold!=null) {
					fold.toggleCollapsedState();
				}
				possiblyRepaintGutter(textArea);
			}
			else {
				UIManager.getLookAndFeel().provideErrorFeedback(rsta);
			}
		}

		@Override
		public final String getMacroID() {
			return rstaToggleCurrentFoldAction;
		}

	}


}