/*
 * 08/29/2004
 *
 * RSyntaxTextAreaEditorKit.java - The editor kit used by RSyntaxTextArea.
 *
 * This library is distributed under a modified BSD license.  See the included
 * LICENSE file for details.
 */
package org.fife.ui.rsyntaxtextarea;

import java.awt.Component;
import java.awt.Font;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.text.CharacterIterator;
import java.util.ResourceBundle;
import java.util.Stack;

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 org.fife.ui.rsyntaxtextarea.folding.Fold;
import org.fife.ui.rsyntaxtextarea.folding.FoldCollapser;
import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate;
import org.fife.ui.rtextarea.IconRowHeader;
import org.fife.ui.rtextarea.RTextArea;
import org.fife.ui.rtextarea.RTextAreaEditorKit;
import org.fife.ui.rtextarea.RecordableTextAction;


/**
 * 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
 */
@SuppressWarnings("checkstyle:constantname")
public class RSyntaxTextAreaEditorKit extends RTextAreaEditorKit {

	private static final long serialVersionUID = 1L;

	public static final String rstaBacktickAction			= "RSTA.BacktickAction";
	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 rstaCopyAsStyledTextAction   = "RSTA.CopyAsStyledTextAction";
	public static final String rstaCutAsStyledTextAction   = "RSTA.CutAsStyledTextAction";
	public static final String rstaDecreaseIndentAction		= "RSTA.DecreaseIndentAction";
	public static final String rstaDoubleQuoteAction		= "RSTA.DoubleQuoteAction";
	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 rstaOpenParenAction			= "RSTA.OpenParenAction";
	public static final String rstaOpenSquareBracketAction	= "RSTA.OpenSquareBracketAction";
	public static final String rstaOpenCurlyAction			= "RSTA.OpenCurlyAction";
	public static final String rstaPossiblyInsertTemplateAction = "RSTA.TemplateAction";
	public static final String rstaSingleQuoteAction		= "RSTA.SingleQuoteAction";
	public static final String rstaToggleCommentAction 		= "RSTA.ToggleCommentAction";
	public static final String rstaToggleCurrentFoldAction	= "RSTA.ToggleCurrentFoldAction";

	private static final String MSG	= "org.fife.ui.rsyntaxtextarea.RSyntaxTextArea";
	private static final ResourceBundle msg = ResourceBundle.getBundle(MSG);


	/**
	 * The actions that <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 CopyCutAsStyledTextAction(false),
		new CopyCutAsStyledTextAction(true),
		//new DecreaseFontSizeAction(),
		new DecreaseIndentAction(),
		new DeletePrevWordAction(),
		new DumbCompleteWordAction(),
		new EndAction(endAction, false),
		new EndAction(selectionEndAction, true),
		new EndWordAction(endWordAction, false),
		new EndWordAction(endWordAction, true),
		new ExpandAllFoldsAction(),
		new GoToMatchingBracketAction(),
		//new IncreaseFontSizeAction(),
		new InsertBreakAction(),
		new InsertPairedCharacterAction(rstaOpenParenAction, '(', ')'),
		new InsertPairedCharacterAction(rstaOpenSquareBracketAction, '[', ']'),
		new InsertPairedCharacterAction(rstaOpenCurlyAction, '{', '}'),
		new InsertQuoteAction(rstaDoubleQuoteAction, InsertQuoteAction.QuoteType.DOUBLE_QUOTE),
		new InsertQuoteAction(rstaSingleQuoteAction, InsertQuoteAction.QuoteType.SINGLE_QUOTE),
		new InsertQuoteAction(rstaBacktickAction, InsertQuoteAction.QuoteType.BACKTICK),
		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
			int languageIndex = 0; // TODO
			if (doc.isIdentifierChar(languageIndex, ch)) {
				if (offs!=end && !doc.isIdentifierChar(languageIndex, nextCh)) {
					return offs;
				}
				do {
					ch = seg.previous();
				} while (doc.isIdentifierChar(languageIndex, ch) && ch != CharacterIterator.DONE);
			}

			// 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);
				}
				RSyntaxUtilities.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) {

			if (!textArea.isEditable() || !textArea.isEnabled()) {
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				return;
			}

			RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
			RSyntaxDocument doc = (RSyntaxDocument)rsta.getDocument();

			int languageIndex = 0;
			int dot = textArea.getCaretPosition();
			if (dot>0) {
				Token t = RSyntaxUtilities.getTokenAtOffset(rsta, dot-1);
				languageIndex = t==null ? 0 : t.getLanguageIndex();
			}
			boolean alignCurlyBraces = rsta.isAutoIndentEnabled() &&
							doc.getCurlyBracesDenoteCodeBlocks(languageIndex);

			if (alignCurlyBraces) {
				textArea.beginAtomicEdit();
			}

			try {

				textArea.replaceSelection("}");

				// If the user wants to align curly braces...
				if (alignCurlyBraces) {

					Element root = doc.getDefaultRootElement();
					dot = rsta.getCaretPosition() - 1; // Start before '}'
					int line = root.getElementIndex(dot);
					Element elem = root.getElement(line);
					int start = elem.getStartOffset();

					// Get the current line's text up to the '}' entered.
					try {
						doc.getText(start, dot-start, seg);
					} catch (BadLocationException ble) { // Never happens
						ble.printStackTrace();
						return;
					}

					// Only attempt to align if there's only whitespace up to
					// the '}' entered.
					for (int i=0; i<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) { // 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<>();

			Element root = doc.getDefaultRootElement();
			int curLine = root.getElementIndex(dot);

			for (int i=0; i<=curLine; i++) {

				Token t = doc.getTokenListForLine(i);
				while (t!=null && t.isPaintable()) {

					if (t.getType()==Token.MARKUP_TAG_DELIMITER) {
						if (t.isSingleChar('<') || t.isSingleChar('[')) {
							t = t.getNextToken();
							while (t!=null && t.isPaintable()) {
								if (t.getType()==Token.MARKUP_TAG_NAME ||
										// Being lenient here and also checking
										// for attributes, in case they
										// (incorrectly) have whitespace between
										// the '<' char and the element name.
										t.getType()==Token.MARKUP_TAG_ATTRIBUTE) {
									stack.push(t.getLexeme());
									break;
								}
								t = t.getNextToken();
							}
						}
						else if (t.length()==2 && t.charAt(0)=='/' &&
								(t.charAt(1)=='>' ||
										t.charAt(1)==']')) {
							if (!stack.isEmpty()) { // Always true for valid XML
								stack.pop();
							}
						}
						else if (t.length()==2 &&
								(t.charAt(0)=='<' || t.charAt(0)=='[') &&
								t.charAt(1)=='/') {
							String tagName = null;
							if (!stack.isEmpty()) { // Always true for valid XML
								tagName = stack.pop();
							}
							if (t.getEndOffset()>=dot) {
								return tagName;
							}
						}
					}

					t = t==null ? null : t.getNextToken();

				}

			}

			return null; // Should never happen

		}

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

	}


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

		private static final long serialVersionUID = 1L;

		public CollapseAllCommentFoldsAction() {
			super(rstaCollapseAllCommentFoldsAction);
			setProperties(msg, "Action.CollapseCommentFolds");
		}

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

		@Override
		public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
			RSyntaxTextArea rsta = (RSyntaxTextArea)textArea;
			if (rsta.isCodeFoldingEnabled()) {
				FoldCollapser collapser = new FoldCollapser();
				collapser.collapseFolds(rsta.getFoldManager());
				RSyntaxUtilities.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());
				RSyntaxUtilities.possiblyRepaintGutter(textArea);
			}
			else {
				UIManager.getLookAndFeel().provideErrorFeedback(rsta);
			}
		}

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

	}


	/**
	 * Action for copying text as styled text.
	 */
	public static class CopyCutAsStyledTextAction extends RecordableTextAction {

		private Theme theme;
		private boolean cutAction;

		private static final long serialVersionUID = 2L;

		private static String getActionName(boolean cutAction) {
			return cutAction ? rstaCutAsStyledTextAction : rstaCopyAsStyledTextAction;
		}

		public CopyCutAsStyledTextAction(boolean cutAction) {
			super(getActionName(cutAction));
			this.cutAction = cutAction;
		}

		public CopyCutAsStyledTextAction(String themeName, Theme theme, boolean cutAction) {
			super(getActionName(cutAction) + "_" + themeName);
			this.theme = theme;
			this.cutAction = cutAction;

		}

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

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

			// TODO: Refactor popup menu logic so RTextArea doesn't get RSyntaxTextArea's
			// cut and copy actions
			if (!(textArea instanceof RSyntaxTextArea)) {
				handleActionPerformedPlainText(textArea);
				return;
			}

			if (cutAction && (!textArea.isEditable() || !textArea.isEnabled())) {
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				return;
			}

			((RSyntaxTextArea)textArea).copyAsStyledText(theme);
			if (cutAction) {
				int selStart = textArea.getSelectionStart();
				int selEnd = textArea.getSelectionEnd();

				try {
					textArea.getDocument().remove(selStart, selEnd - selStart);
				} catch (BadLocationException ble) {
					ble.printStackTrace();
					UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				}
			}
			textArea.requestFocusInWindow();
		}

		private void handleActionPerformedPlainText(RTextArea textArea) {
			if (cutAction) {
				textArea.cut();
			}
			else {
				textArea.copy();
			}
			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 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.
							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.
				rsta.setFont(font.deriveFont(MINIMUM_SIZE));
				changed = true;
			}

			// If we updated at least one font, update the screen.  If
			// all the fonts were already the minimum size, beep.
			if (changed) {
				rsta.setSyntaxScheme(scheme);
				// NOTE:  This is a hack to get an encompassing
				// RTextScrollPane to repaint its line numbers to account
				// for a change in line height due to a font change.  I'm
				// not sure why we need to do this here but not when we
				// change the syntax highlighting color scheme via the
				// Options dialog... setSyntaxHighlightingColorScheme()
				// calls revalidate() which won't repaint the scroll pane
				// if scrollbars don't change, which is why we need this.
				Component parent = rsta.getParent();
				if (parent instanceof javax.swing.JViewport) {
					parent = parent.getParent();
					if (parent instanceof JScrollPane) {
						parent.repaint();
					}
				}
			}
			else {
				UIManager.getLookAndFeel().provideErrorFeedback(rsta);
			}

		}

	}


	/**
	 * Action for when un-indenting lines (either the current line if there is
	 * selection, or all selected lines if there is one).
	 */
	public static class DecreaseIndentAction extends RecordableTextAction {

		private static final long serialVersionUID = 1L;

		private Segment s;

		public DecreaseIndentAction() {
			this(rstaDecreaseIndentAction);
		}

		public DecreaseIndentAction(String name) {
			super(name);
			s = new Segment();
		}

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

			if (!textArea.isEditable() || !textArea.isEnabled()) {
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				return;
			}

			Document document = textArea.getDocument();
			Element map = document.getDefaultRootElement();
			Caret c = textArea.getCaret();
			int dot = c.getDot();
			int mark = c.getMark();
			int line1 = map.getElementIndex(dot);
			int tabSize = textArea.getTabSize();

			// If there is a selection, indent all lines in the selection.
			// Otherwise, indent the line the caret is on.
			if (dot!=mark) {
				// Note that we cheaply reuse variables here, so don't
				// take their names to mean what they are.
				int line2 = map.getElementIndex(mark);
				dot = Math.min(line1, line2);
				mark = Math.max(line1, line2);
				Element elem;
				textArea.beginAtomicEdit();
				try {
					for (line1=dot; 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);
				} finally {
					textArea.endAtomicEdit();
				}
			}
			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 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
			int languageIndex = 0; // TODO
			if (doc.isIdentifierChar(languageIndex, ch)) {
				do {
					ch = seg.previous();
				} while (doc.isIdentifierChar(languageIndex, ch));
			}

			// The "word" is a series of symbols.
			else {
				while (!Character.isWhitespace(ch) &&
						!doc.isIdentifierChar(languageIndex, 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;

		}

	}


	/**
	 * Overridden to use the programming language RSTA is displaying when
	 * computing words to complete.
	 */
	public static class DumbCompleteWordAction
			extends RTextAreaEditorKit.DumbCompleteWordAction {

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

			RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
			Element root = doc.getDefaultRootElement();
			int line = root.getElementIndex(offs);
			Element elem = root.getElement(line);

			// If caret is at the beginning of a word, we should return the
			// previous word
			int start = elem.getStartOffset();
			if (offs > start) {
				char ch = doc.charAt(offs);
				if (isIdentifierChar(ch)) {
					offs--;
				}
			}
			else { // offs == start => previous word is on previous line
				if (line == 0) {
					return -1;
				}
				elem = root.getElement(--line);
				offs = elem.getEndOffset() - 1;
			}

			int prevWordStart = getPreviousWordStartInLine(doc, elem, offs);
			while (prevWordStart == -1 && line > 0) {
				line--;
				elem = root.getElement(line);
				prevWordStart = getPreviousWordStartInLine(doc, elem,
						elem.getEndOffset());
			}

			return prevWordStart;

		}

		private int getPreviousWordStartInLine(RSyntaxDocument doc,
				Element elem, int offs) throws BadLocationException {

			int start = elem.getStartOffset();
			int cur = offs;

			// Skip any whitespace or non-word chars
			while (cur >= start) {
				char ch = doc.charAt(cur);
				if (isIdentifierChar(ch)) {
					break;
				}
				cur--;
			}
			if (cur < start) {
				// Empty line or nothing but whitespace/non-word chars
				return -1;
			}

			return getWordStartImpl(doc, elem, cur);

		}

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

			RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
			Element root = doc.getDefaultRootElement();
			int line = root.getElementIndex(offs);
			Element elem = root.getElement(line);
			int end = elem.getEndOffset() - 1;

			int wordEnd = offs;
			while (wordEnd <= end) {
				if (!isIdentifierChar(doc.charAt(wordEnd))) {
					break;
				}
				wordEnd++;
			}

			return wordEnd;

		}

		@Override
		protected int getWordStart(RTextArea textArea, int offs)
				throws BadLocationException {
			RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
			Element root = doc.getDefaultRootElement();
			int line = root.getElementIndex(offs);
			Element elem = root.getElement(line);
			return getWordStartImpl(doc, elem, offs);
		}

		private static int getWordStartImpl(RSyntaxDocument doc,
				Element elem, int offs) throws BadLocationException {

			int start = elem.getStartOffset();

			int wordStart = offs;
			while (wordStart >= start) {
				char ch = doc.charAt(wordStart);
				// Ignore newlines, so we work when caret is at end of line
				if (!isIdentifierChar(ch) && ch != '\n') {
					break;
				}
				wordStart--;
			}

			return wordStart==offs ? offs : wordStart + 1;

		}

		/**
		 * Overridden to not suggest word completions if the text right before
		 * the caret contains non-word characters, such as '/' or '%'.
		 *
		 * @param prefix The prefix characters before the caret.
		 * @return Whether the prefix could be part of a "word" in the context
		 *         of the text area's current content.
		 */
		@Override
		protected boolean isAcceptablePrefix(String prefix) {
			return prefix.length() > 0 &&
				isIdentifierChar(prefix.charAt(prefix.length()-1));
		}

		/**
		 * Returns whether the specified character should be considered part
		 * of an identifier.
		 *
		 * @param ch The character.
		 * @return Whether the character is part of an identifier.
		 */
		private static boolean isIdentifierChar(char ch) {
			//return doc.isIdentifierChar(languageIndex, ch);
			return Character.isLetterOrDigit(ch) || ch == '_' || ch == '$';
		}

	}


	/**
	 * 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
			int languageIndex = 0; // TODO
			if (doc.isIdentifierChar(languageIndex, ch)) {
				do {
					ch = seg.next();
				} while (doc.isIdentifierChar(languageIndex, ch) &&
						ch != CharacterIterator.DONE);
			}

			// 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));
				}
				RSyntaxUtilities.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.
	 */
	abstract static class FoldRelatedAction extends RecordableTextAction {

		FoldRelatedAction(String name) {
			super(name);
		}

		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;
		}

	}


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

		/**
		 * 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();
			}

		}

		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 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.
							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.
				rsta.setFont(font.deriveFont(MAXIMUM_SIZE));
				changed = true;
			}

			// If we updated at least one font, update the screen.  If
			// all 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 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 int getOpenBraceCount(RSyntaxDocument doc,
				int languageIndex) {
			int openCount = 0;
			for (Token t : doc) {
				if (t.getType()==Token.SEPARATOR && t.length()==1 &&
						t.getLanguageIndex()==languageIndex) {
					char ch = t.charAt(0);
					if (ch=='{') {
						openCount++;
					}
					else if (ch=='}') {
						openCount--;
					}
				}
			}
			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 The text area to examine.
		 * @param noSelection Whether there is no selection.
		 */
		protected void handleInsertBreak(RSyntaxTextArea textArea,
										boolean noSelection) {

			if (noSelection) {
				textArea.beginAtomicEdit();
				try {
					handleInsertBreakWithoutSelection(textArea);
				} catch (BadLocationException ble) { // Never happens
					textArea.replaceSelection("\n");
					ble.printStackTrace();
				} finally {
					textArea.endAtomicEdit();
				}
			}
			else {
				textArea.replaceSelection("\n");
			}
		}

		private void handleInsertBreakWithoutSelection(RSyntaxTextArea textArea)
				throws BadLocationException {

			int caretPos = textArea.getCaretPosition();
			Document doc = textArea.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);
			int caretOffsInLine = caretPos - start;

			StringBuilder sb = new StringBuilder("\n");
			String leadingWS = null;
			if (textArea.isAutoIndentEnabled()) {
				leadingWS = RSyntaxUtilities.getLeadingWhitespace(s, caretOffsInLine);
				sb.append(leadingWS);
			}

			// If the text remaining on the line would be all whitespace,
			// remove it if necessary
			if (textArea.isClearWhitespaceLinesEnabled() && isAllWhitespace(s, 0, caretOffsInLine)) {
				// Select all text on the line before the caret so it gets removed
				textArea.setCaretPosition(start);
			}

			// Find any non-whitespace text after the caret. If there is any, it gets put
			// onto the next line. Whitespace between the caret and that text gets removed.
			int nonWhitespacePos = atEndOfLine(caretPos-start, s, len);
			//textArea.moveCaretPosition(start + (nonWhitespacePos > -1 ? nonWhitespacePos : end));
			textArea.moveCaretPosition(nonWhitespacePos > -1 ? start + nonWhitespacePos : end);
			textArea.replaceSelection(sb.toString());

			// 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 (textArea.getShouldIndentNextLine(lineNum)) {
				textArea.replaceSelection("\t");
			}

			possiblyCloseCurlyBrace(textArea, leadingWS);
		}

		private static boolean isAllWhitespace(String str, int from, int to) {
			for (int i = from; i < to; i++) {
				if (!Character.isWhitespace(str.charAt(i))) {
					return false;
				}
			}
			return true;
		}

		private void possiblyCloseCurlyBrace(RSyntaxTextArea textArea,
											String leadingWS) {

			RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();

			if (textArea.getCloseCurlyBraces()) {

				int line = textArea.getCaretLineNumber();
				Token t = doc.getTokenListForLine(line-1);
				t = t.getLastNonCommentNonWhitespaceToken();

				if (t!=null && t.isLeftCurly()) {

					int languageIndex = t.getLanguageIndex();
					if (doc.getCurlyBracesDenoteCodeBlocks(languageIndex) &&
							getOpenBraceCount(doc, languageIndex)>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
					}

				}

			}

		}

	}


	/**
	 * If there is no selection, a character is inserted. If there is a selection,
	 * it is wrapped by the character and its pair. Useful for e.g. quotes, parens,
	 * etc.
	 */
	public static class InsertPairedCharacterAction extends DefaultKeyTypedAction {

		private static final long serialVersionUID = 1L;

		private final char ch;
		private final char pairedCh;

		public InsertPairedCharacterAction(String actionName, char ch, char pairedCh) {
			super(actionName);
			this.ch = ch;
			this.pairedCh = pairedCh;
		}

		@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();

			if (noSelection || !sta.getInsertPairedCharacters()) {
				// Default action can be unique across OS's
				super.actionPerformedImpl(e, textArea);
			}
			else {
				wrapSelection(textArea);
			}
		}

		private void wrapSelection(RTextArea textArea) {

			int selStart = textArea.getSelectionStart();
			int selEnd = textArea.getSelectionEnd();

			textArea.beginAtomicEdit();
			try {
				textArea.insert(String.valueOf(ch), selStart);
				textArea.insert(String.valueOf(pairedCh), selEnd + 1);
				// Remove the auto-increase from insertion
				textArea.setSelectionEnd(selEnd + 1);
			} finally {
				textArea.endAtomicEdit();
			}
		}

	}


	/**
	 * Inserts a quote character. If the current language supports string literals with this
	 * quote character, the following additional logic occurs:
	 * <ul>
	 *     <li>If the caret is not in a string literal or comment, both the opening and closing
	 *         quotes are entered</li>
	 *     <li>If the caret is at the end (the closing quote) of a valid quoted literal, the
	 *         existing closing quote character is overwritten, rather than a new quote
	 *         character being entered</li>
	 * </ul>
	 * This feature is meant to simplify the common case of typing single-line strings.
	 */
	public static class InsertQuoteAction extends InsertPairedCharacterAction {

		/**
		 * The type of quote to insert.
		 */
		public enum QuoteType {
			DOUBLE_QUOTE('"', TokenTypes.LITERAL_STRING_DOUBLE_QUOTE, TokenTypes.ERROR_STRING_DOUBLE),
			SINGLE_QUOTE('\'', TokenTypes.LITERAL_CHAR, TokenTypes.ERROR_CHAR),
			BACKTICK('`', TokenTypes.LITERAL_BACKQUOTE, -1);

			private final char ch;
			private final int validTokenType;
			private final int invalidTokenType;

			QuoteType(char ch, int validTokenType, int invalidTokenType) {
				this.ch = ch;
				this.validTokenType = validTokenType;
				this.invalidTokenType = invalidTokenType;
			}
		}

		private final QuoteType quoteType;
		private final String stringifiedQuoteTypeCh;

		public InsertQuoteAction(String actionName, QuoteType quoteType) {
			super(actionName, quoteType.ch, quoteType.ch);
			this.quoteType = quoteType;
			stringifiedQuoteTypeCh = String.valueOf(quoteType.ch);
		}

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

			if (!textArea.isEditable() || !textArea.isEnabled()) {
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				return;
			}

			RSyntaxTextArea rsta = (RSyntaxTextArea) textArea;

			if (!rsta.getInsertPairedCharacters() ||
				textArea.getSelectionStart() != textArea.getSelectionEnd() ||
				textArea.getTextMode() == RTextArea.OVERWRITE_MODE) {
				super.actionPerformedImpl(e, textArea);
				return;
			}

			int offs = rsta.getCaretPosition();
			Token t = RSyntaxUtilities.getTokenAtOffsetOrLastTokenIfEndOfLine(rsta, offs);
			int tokenType = t != null ? t.getType() : TokenTypes.NULL;
			boolean isComment = t != null && t.isComment();

			if (tokenType == quoteType.validTokenType) {
				if (offs == t.getEndOffset() - 1) {
					textArea.moveCaretPosition(offs + 1); // Force a replacement to ensure undo is contiguous
					textArea.replaceSelection(stringifiedQuoteTypeCh);
					textArea.setCaretPosition(offs + 1);
				}
				else {
					super.actionPerformedImpl(e, textArea);
				}
			}
			else if (isComment || tokenType == quoteType.invalidTokenType) {
				// We could be smarter here for invalid quoted literals - if we knew whether the language
				// used '\' as an escape character, and the caret is NOT between a '\' and the closing
				// quote, we could then assume it's an invalid string due to e.g. a bad escape char, and
				// overwrite the closing quote. But for now we're just doing nothing in this case
				super.actionPerformedImpl(e, textArea); // Just insert the character
			}
			else {
				insertEmptyQuoteLiteral(rsta);
			}
		}

		private void insertEmptyQuoteLiteral(RSyntaxTextArea textArea) {

			textArea.beginAtomicEdit();

			try {

				textArea.replaceSelection(stringifiedQuoteTypeCh);

				// Check whether the starting quote started a string literal. If it did,
				// enter the closing quote. This is done to sniff out language tht don't
				// support string literals.
				int caretPos = textArea.getCaretPosition();
				Token t = RSyntaxUtilities.getTokenAtOffsetOrLastTokenIfEndOfLine(textArea, caretPos);
				int tokenType = t != null ? t.getType() : TokenTypes.NULL;
				if (tokenType == quoteType.validTokenType || tokenType == quoteType.invalidTokenType) {
					textArea.replaceSelection(stringifiedQuoteTypeCh);
					textArea.setCaretPosition(textArea.getCaretPosition() - 1);
				}
			} finally {
				textArea.endAtomicEdit();
			}
		}
	}


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

			// If there is a selection, indent all lines in it
			if (dot != mark) {

				int dotLine = map.getElementIndex(dot);
				int markLine = map.getElementIndex(mark);
				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
			int languageIndex = 0;
			if (doc.isIdentifierChar(languageIndex, ch)) {
				do {
					ch = seg.next();
				} while (doc.isIdentifierChar(languageIndex, ch) &&
						ch != CharacterIterator.DONE);
			}

			// Skip groups of "anything else" (operators, etc.).
			else if (!Character.isWhitespace(ch)) {
				do {
					ch = seg.next();
				} while (ch!=Segment.DONE &&
						!(doc.isIdentifierChar(languageIndex, 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 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
			int languageIndex = 0;
			if (doc.isIdentifierChar(languageIndex, ch)) {
				do {
					ch = seg.previous();
				} while (doc.isIdentifierChar(languageIndex, ch) &&
						ch != CharacterIterator.DONE);
			}

			// Skip groups of "anything else" (operators, etc.).
			else if (!Character.isWhitespace(ch)) {
				do {
					ch = seg.previous();
				} while (ch!=Segment.DONE &&
						!(doc.isIdentifierChar(languageIndex, ch) ||
								Character.isWhitespace(ch)));
			}

			offs -= seg.getEndIndex() - seg.getIndex();
			if (ch!=Segment.DONE) {
				offs++;
			}

			return offs;

		}

	}


	/**
	 * Selects the word around the caret.  This class is here to better
	 * handle selecting "words" in programming languages.
	 */
	public static class SelectWordAction
					extends RTextAreaEditorKit.SelectWordAction {

		@Override
		protected void createActions() {
			start = new BeginWordAction("pigdog", false);
			end = new EndWordAction("pigdog", true);
		}

	}


	/**
	 * Action that toggles whether the currently selected lines are
	 * commented.
	 */
	public static class ToggleCommentAction extends RecordableTextAction {

		public ToggleCommentAction() {
			super(rstaToggleCommentAction);
		}

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

			if (!textArea.isEditable() || !textArea.isEnabled()) {
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				return;
			}

			RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
			Element map = doc.getDefaultRootElement();
			Caret c = textArea.getCaret();
			int dot = c.getDot();
			int mark = c.getMark();
			int line1 = map.getElementIndex(dot);
			int line2 = map.getElementIndex(mark);
			int start = Math.min(line1, line2);
			int end   = Math.max(line1, line2);

			Token t = doc.getTokenListForLine(start);
			int languageIndex = t!=null ? t.getLanguageIndex() : 0;
			String[] startEnd = doc.getLineCommentStartAndEnd(languageIndex);

			if (startEnd==null) {
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
				return;
			}

			// Don't toggle comment on last line if there is no
			// text selected on it.
			if (start!=end) {
				Element elem = map.getElement(end);
				if (Math.max(dot, mark)==elem.getStartOffset()) {
					end--;
				}
			}

			textArea.beginAtomicEdit();
			try {
				boolean add = getDoAdd(doc,map, start,end, startEnd);
				for (line1=start; line1<=end; line1++) {
					Element elem = map.getElement(line1);
					handleToggleComment(elem, doc, startEnd, add);
				}
			} catch (BadLocationException ble) {
				ble.printStackTrace();
				UIManager.getLookAndFeel().provideErrorFeedback(textArea);
			} finally {
				textArea.endAtomicEdit();
			}

		}

		private boolean getDoAdd(Document doc, Element map, int startLine,
							int endLine, String[] startEnd)
								throws BadLocationException {
			boolean doAdd = false;
			for (int i=startLine; i<=endLine; i++) {
				Element elem = map.getElement(i);
				int start = elem.getStartOffset();
				String t = doc.getText(start, elem.getEndOffset()-start-1);
				if (!t.startsWith(startEnd[0]) ||
						(startEnd[1]!=null && !t.endsWith(startEnd[1]))) {
					doAdd = true;
					break;
				}
			}
			return doAdd;
		}

		private void handleToggleComment(Element elem, Document doc,
			String[] startEnd, boolean add) throws BadLocationException {
			int start = elem.getStartOffset();
			int end = elem.getEndOffset() - 1;
			if (add) {
				if (startEnd[1]!=null) {
					doc.insertString(end, startEnd[1], null);
				}
				doc.insertString(start, startEnd[0], null);
			}
			else {
				if (startEnd[1]!=null) {
					int temp = startEnd[1].length();
					doc.remove(end-temp, temp);
				}
				doc.remove(start, startEnd[0].length());
			}
		}

		@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();
				}
				RSyntaxUtilities.possiblyRepaintGutter(textArea);
			}
			else {
				UIManager.getLookAndFeel().provideErrorFeedback(rsta);
			}
		}

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

	}


}
