/** * Aptana Studio * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions). * Please see the license.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.editor.ruby; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.regex.Pattern; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.jrubyparser.CompatVersion; import org.jrubyparser.Parser.NullWarnings; import org.jrubyparser.lexer.LexerSource; import org.jrubyparser.lexer.SyntaxException; import org.jrubyparser.parser.ParserConfiguration; import org.jrubyparser.parser.ParserSupport19; import org.jrubyparser.parser.Ruby19Parser; import org.jrubyparser.parser.RubyParser; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.StringUtil; import com.aptana.editor.common.text.RubyRegexpAutoIndentStrategy; /** * Special subclass of auto indenter that will auto-close methods/blocks/classes/types with 'end" when needed. * * @author cwilliams */ class RubyAutoIndentStrategy extends RubyRegexpAutoIndentStrategy { RubyAutoIndentStrategy(String contentType, SourceViewerConfiguration configuration, ISourceViewer sourceViewer, IPreferenceStore prefStore) { super(contentType, configuration, sourceViewer, prefStore); } private final Pattern openBlockPattern = Pattern.compile(".*[\\S].*do[\\w|\\s]*"); //$NON-NLS-1$ private static final String BLOCK_CLOSER = "end"; //$NON-NLS-1$ @Override protected boolean autoIndent(IDocument d, DocumentCommand c) { boolean superAutoIndent = super.autoIndent(d, c); int p = Math.max(0, c.offset == d.getLength() ? c.offset - 1 : c.offset); int line = 0; IRegion currentLineRegion = null; int startOfCurrentLine = 0; String lineString = null; try { line = d.getLineOfOffset(p); currentLineRegion = d.getLineInformation(line); startOfCurrentLine = currentLineRegion.getOffset(); lineString = d.get(startOfCurrentLine, c.offset - startOfCurrentLine); } catch (BadLocationException e) { IdeLog.logError(RubyEditorPlugin.getDefault(), "Unable to get text of line at offset: " + p, e); //$NON-NLS-1$ return false; } if (!superAutoIndent) { if (lineString.startsWith("=begin")) //$NON-NLS-1$ { // TODO If doesn't start at beginning of line, move to first column? String indent = getIndentString(); c.text += indent; c.caretOffset = c.offset + indent.length(); c.shiftsCaret = false; c.text += TextUtilities.getDefaultLineDelimiter(d) + "=end"; //$NON-NLS-1$ return true; } return false; } // Ruble says we're at an indentation point, this is where we should look for closing with "end" String trimmed = lineString.trim(); if (trimmed.equals("=begin")) //$NON-NLS-1$ { // TODO If doesn't start at beginning of line, move to first column String indent = getIndentString(); c.text += indent; c.caretOffset = c.offset + indent.length(); c.shiftsCaret = false; c.text += TextUtilities.getDefaultLineDelimiter(d) + "=end"; //$NON-NLS-1$ } // insert closing "end" on new line after an unclosed block if (closeBlock() && unclosedBlock(d, trimmed, c.offset)) { String previousLineIndent = getAutoIndentAfterNewLine(d, c); c.text += TextUtilities.getDefaultLineDelimiter(d) + previousLineIndent + BLOCK_CLOSER; } return true; } private boolean unclosedBlock(IDocument d, String trimmed, int offset) { // FIXME wow is this ugly! There has to be an easier way to tell if there's an unclosed block besides parsing // and catching a syntaxError! if (!atStartOfBlock(trimmed)) { return false; } // TODO Re-use parser pool? Right now we can't because syntax exceptions get silently swallowed; and we need to // pass down warnings/line number/etc in parseState. ParserConfiguration config = new ParserConfiguration(0, CompatVersion.BOTH); ParserSupport19 support = new ParserSupport19(); support.setConfiguration(config); support.setWarnings(new NullWarnings()); RubyParser parser = new Ruby19Parser(support); LexerSource lexerSource = null; Reader reader = null; try { reader = new BufferedReader(new StringReader(d.get())); lexerSource = LexerSource.getSource(StringUtil.EMPTY, reader, config); parser.parse(config, lexerSource); } catch (SyntaxException e) { if (e.getPid() != SyntaxException.PID.GRAMMAR_ERROR) { return false; } Reader reader2 = null; try { StringBuffer buffer = new StringBuffer(d.get()); buffer.insert(offset, TextUtilities.getDefaultLineDelimiter(d) + BLOCK_CLOSER); reader2 = new BufferedReader(new StringReader(buffer.toString())); lexerSource = LexerSource.getSource(StringUtil.EMPTY, reader2, config); parser.parse(config, lexerSource); } catch (SyntaxException syntaxException) { return false; } catch (IOException ioe) { return false; } finally { if (reader2 != null) { try { reader2.close(); } catch (IOException e1) // $codepro.audit.disable emptyCatchClause { // ignore } } } return true; } catch (Throwable t) { IdeLog.logError(RubyEditorPlugin.getDefault(), "Got unexpected exception parsing file", t); //$NON-NLS-1$ } finally { if (reader != null) { try { reader.close(); } catch (IOException e) // $codepro.audit.disable emptyCatchClause { // ignore } } } return false; } @SuppressWarnings("nls") private boolean atStartOfBlock(String line) { return line.startsWith("class ") || line.startsWith("if ") || line.startsWith("while ") || line.startsWith("module ") || line.startsWith("unless ") || line.startsWith("def ") || line.equals("begin") || line.startsWith("case ") || line.startsWith("for ") || openBlockPattern.matcher(line).matches(); } private boolean closeBlock() { // TODO Set up a pref value for user to turn this behavior off? return true; } }