/* * 01/25/2009 * * AbstractJFlexCTokenMaker.java - Base class for token makers that use curly * braces to denote code blocks, such as C, C++, Java, Perl, etc. * * This library is distributed under a modified BSD license. See the included * RSyntaxTextArea.License.txt file for details. */ package org.fife.ui.rsyntaxtextarea; import java.awt.event.ActionEvent; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.Action; import javax.swing.UIManager; import javax.swing.text.BadLocationException; import org.fife.ui.rtextarea.RTextArea; /** * Base class for JFlex-based token makers using C-style syntax. This class * knows how to: * * <ul> * <li>Auto-indent after opening braces and parens * <li>Automatically close multi-line and documentation comments * </ul> * * @author Robert Futrell * @version 1.0 */ public abstract class AbstractJFlexCTokenMaker extends AbstractJFlexTokenMaker { private final Action INSERT_BREAK_ACTION; private static final Pattern MLC_PATTERN = Pattern.compile("([ \\t]*)(/?[\\*]+)([ \\t]*)"); protected AbstractJFlexCTokenMaker() { INSERT_BREAK_ACTION = createInsertBreakAction(); } /** * Creates and returns the action to use when the user inserts a newline. * The default implementation intelligently closes multi-line comments. * Subclasses can override. * * @return The action. * @see #getInsertBreakAction() */ protected Action createInsertBreakAction() { return new CStyleInsertBreakAction(); } /** * Returns <code>true</code> always as C-style languages use curly braces * to denote code blocks. * * @return <code>true</code> always. */ @Override public boolean getCurlyBracesDenoteCodeBlocks(int languageIndex) { return true; } /** * Returns an action to handle "insert break" key presses (i.e. Enter). * An action is returned that handles newlines differently in multi-line * comments. * * @return The action. */ @Override public Action getInsertBreakAction() { return INSERT_BREAK_ACTION; } /** * {@inheritDoc} */ @Override public boolean getMarkOccurrencesOfTokenType(int type) { return type==Token.IDENTIFIER || type==Token.FUNCTION; } /** * {@inheritDoc} */ @Override public boolean getShouldIndentNextLineAfter(Token t) { if (t!=null && t.length()==1) { char ch = t.charAt(0); return ch=='{' || ch=='('; } return false; } /** * Returns whether a given token is an internal token type that represents * an MLC or documentation comment continuing on to the next line. This is * done by languages such as JavaScript that are a little more verbose * than necessary so that their code can be copy-and-pasted into other * <code>TokenMaker</code>s that use them as nested languages (such as * HTML, JSP, etc.). * * @param t The token to check. This cannot be <code>null</code>. * @return Whether the token is an internal token representing the end of * a line for an MLC/doc comment continuing on to the next line. */ private boolean isInternalEolTokenForMLCs(Token t) { int type = t.getType(); if (type<0) { type = getClosestStandardTokenTypeForInternalType(type); return type==TokenTypes.COMMENT_MULTILINE || type==TokenTypes.COMMENT_DOCUMENTATION; } return false; } /** * Action that knows how to special-case inserting a newline in a * multi-line comment for languages like C and Java. */ protected class CStyleInsertBreakAction extends RSyntaxTextAreaEditorKit.InsertBreakAction { @Override public void actionPerformedImpl(ActionEvent e, RTextArea textArea) { if (!textArea.isEditable() || !textArea.isEnabled()) { UIManager.getLookAndFeel().provideErrorFeedback(textArea); return; } RSyntaxTextArea rsta = (RSyntaxTextArea)getTextComponent(e); RSyntaxDocument doc = (RSyntaxDocument)rsta.getDocument(); int line = textArea.getCaretLineNumber(); int type = doc.getLastTokenTypeOnLine(line); if (type<0) { type = doc.getClosestStandardTokenTypeForInternalType(type); } // Only in MLC's should we try this if (type==Token.COMMENT_DOCUMENTATION || type==Token.COMMENT_MULTILINE) { insertBreakInMLC(e, rsta, line); } else { handleInsertBreak(rsta, true); } } /** * Returns whether the MLC token containing <code>offs</code> appears * to have a "nested" comment (i.e., contains "<code>/*</code>" * somewhere inside of it). This implies that it is likely a "new" MLC * and needs to be closed. While not foolproof, this is usually good * enough of a sign. * * @param textArea * @param line * @param offs * @return Whether a comment appears to be nested inside this one. */ private boolean appearsNested(RSyntaxTextArea textArea, int line, int offs) { final int firstLine = line; // Remember the line we start at. while (line<textArea.getLineCount()) { Token t = textArea.getTokenListForLine(line); int i = 0; // If examining the first line, start at offs. if (line++==firstLine) { t = RSyntaxUtilities.getTokenAtOffset(t, offs); if (t==null) { // offs was at end of the line continue; } i = t.documentToToken(offs); } else { i = t.getTextOffset(); } int textOffset = t.getTextOffset(); while (i<textOffset+t.length()-1) { if (t.charAt(i-textOffset)=='/' && t.charAt(i-textOffset+1)=='*') { return true; } i++; } // If tokens come after this one on this line, our MLC ended. if ((t=t.getNextToken())!=null && !isInternalEolTokenForMLCs(t)) { return false; } } return true; // No match - MLC goes to the end of the file } private void insertBreakInMLC(ActionEvent e, RSyntaxTextArea textArea, int line) { Matcher m = null; int start = -1; int end = -1; String text = null; try { start = textArea.getLineStartOffset(line); end = textArea.getLineEndOffset(line); text = textArea.getText(start, end-start); m = MLC_PATTERN.matcher(text); } catch (BadLocationException ble) { // Never happens UIManager.getLookAndFeel().provideErrorFeedback(textArea); ble.printStackTrace(); return; } if (m.lookingAt()) { String leadingWS = m.group(1); String mlcMarker = m.group(2); // If the caret is "inside" any leading whitespace or MLC // marker, move it to the end of the line. int dot = textArea.getCaretPosition(); if (dot>=start && dot<start+leadingWS.length()+mlcMarker.length()) { // If we're in the whitespace before the very start of the // MLC though, just insert a normal newline if (mlcMarker.charAt(0)=='/') { handleInsertBreak(textArea, true); return; } textArea.setCaretPosition(end-1); } else { // Ensure caret is at the "end" of any whitespace // immediately after the '*', but before any possible // non-whitespace chars. boolean moved = false; while (dot<end-1 && Character.isWhitespace(text.charAt(dot-start))) { moved = true; dot++; } if (moved) { textArea.setCaretPosition(dot); } } boolean firstMlcLine = mlcMarker.charAt(0)=='/'; boolean nested = appearsNested(textArea, line, start+leadingWS.length()+2); String header = leadingWS + (firstMlcLine ? " * " : "*") + m.group(3); textArea.replaceSelection("\n" + header); if (nested) { dot = textArea.getCaretPosition(); // Has changed textArea.insert("\n" + leadingWS + " */", dot); textArea.setCaretPosition(dot); } } else { handleInsertBreak(textArea, true); } } } }