package org.rubypeople.rdt.internal.ui.text.ruby;
import java.util.regex.Pattern;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
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.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.jruby.lexer.yacc.SyntaxException;
import org.rubypeople.rdt.core.IRubyProject;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.formatter.DefaultCodeFormatterConstants;
import org.rubypeople.rdt.internal.core.parser.RubyParser;
import org.rubypeople.rdt.internal.corext.util.CodeFormatterUtil;
import org.rubypeople.rdt.internal.ui.RubyPlugin;
import org.rubypeople.rdt.internal.ui.text.RubyHeuristicScanner;
import org.rubypeople.rdt.internal.ui.text.RubyIndenter;
import org.rubypeople.rdt.ui.PreferenceConstants;
public class RubyAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy implements IPropertyChangeListener {
/** Preference key for automatically 'end'ing statements */
private final static String END_STATEMENTS= PreferenceConstants.EDITOR_END_STATEMENTS;
private final Pattern openBlockPattern = Pattern.compile(".*[\\S].*do[\\w|\\s]*");
private static final String BLOCK_CLOSER = "end";
private String fPartitioning;
private final IRubyProject fProject;
private boolean endStatements;
private IPreferenceStore fPreferenceStore;
/**
* Creates a new Ruby auto indent strategy for the given document partitioning.
*
* @param partitioning the document partitioning
* @param project the project to get formatting preferences from, or null to use default preferences
*/
public RubyAutoIndentStrategy(String partitioning, IRubyProject project) {
fPartitioning= partitioning;
fProject= project;
fPreferenceStore = RubyPlugin.getDefault().getPreferenceStore();
endStatements= fPreferenceStore.getBoolean(END_STATEMENTS);
fPreferenceStore.addPropertyChangeListener(this);
}
/*
* @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
*/
public void customizeDocumentCommand(IDocument d, DocumentCommand c) {
if (c.doit == false)
return;
if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text))
smartIndentAfterNewLine(d, c);
}
private boolean isLineDelimiter(IDocument document, String text) {
String[] delimiters= document.getLegalLineDelimiters();
if (delimiters != null)
return TextUtilities.equals(delimiters, text) > -1;
return false;
}
private void smartIndentAfterNewLine(IDocument d, DocumentCommand c) {
RubyHeuristicScanner scanner= new RubyHeuristicScanner(d);
RubyIndenter indenter= new RubyIndenter(d, scanner, fProject);
StringBuffer indent= indenter.computeIndentation(c.offset);
if (indent == null)
indent= new StringBuffer();
int docLength= d.getLength();
if (c.offset == -1 || docLength == 0)
return;
try {
int p= (c.offset == docLength ? c.offset - 1 : c.offset);
int line= d.getLineOfOffset(p);
StringBuffer buf= new StringBuffer(c.text + indent);
IRegion currentLineRegion= d.getLineInformation(line);
int lineEnd= currentLineRegion.getOffset() + currentLineRegion.getLength();
int contentStart= findEndOfWhiteSpace(d, c.offset, lineEnd);
c.length= Math.max(contentStart - c.offset, 0);
int startOfCurrentLine= currentLineRegion.getOffset();
String trimmed = getTrimmedLine(d, startOfCurrentLine, c.offset);
if (mightHaveToShiftCurrentLine(trimmed)) {// check to see if we need to fix the indentation of this line
IRegion previousLineRegion = d.getLineInformation(line - 1);
String previousLine = getTrimmedLine(d, previousLineRegion.getOffset(), previousLineRegion.getOffset() + previousLineRegion.getLength());
String previousIndent= indenter.computeIndentation(previousLineRegion.getOffset()).toString();
// FIXME This all assumes spaces!
String unindented = "";
if (middleOfBlockRightAfterBeginning(trimmed, previousLine) ) {
unindented = previousIndent;
if (whenAfterCase(trimmed, previousLine)) {
// add extra indent, dpending upon code formatting option
if (RubyCore.getPlugin().getPluginPreferences().getBoolean(DefaultCodeFormatterConstants.FORMATTER_INDENT_CASE_BODY)) {
unindented += CodeFormatterUtil.createIndentString(1, fProject);
}
}
} else {
int length = previousIndent.length() - CodeFormatterUtil.createIndentString(1, fProject).length();
int nextCalculated = nextMeaningfulIndentLength(d, indenter, line);
int unit = CodeFormatterUtil.createIndentString(1, fProject).length();
if (nextCalculated != -1 && (length > (nextCalculated + unit))) {
unindented = previousIndent.substring(0, length - unit);
} else if (length <= 0) {
unindented = "";
} else {
unindented = previousIndent.substring(0, length);
}
} // FIXME Deindenting 'end' of case that has indented 'when's comes out incorrectly
if (unindented.length() < indent.length()) { // if calculated indent length is less than indent we currently have queued up...
d.replace(startOfCurrentLine, c.offset - startOfCurrentLine, unindented + trimmed); // fix indent of this line
int shift = indent.length() - unindented.length();
c.offset = c.offset - shift; // change where we're adding the newline
buf.delete(buf.length() - shift, buf.length()); // remove additional indent from being inserted
}
}
// If we're hitting return at the end of the line of a new block, add indent
if (atIndentPoint(trimmed)) {
buf.append(CodeFormatterUtil.createIndentString(1, fProject));
c.caretOffset= c.offset + buf.length();
c.shiftsCaret= false;
}
if (trimmed.equals("=begin")) { // TODO If doesn't start at beginnig of line, move to first column
buf.append(CodeFormatterUtil.createIndentString(1, fProject));
c.caretOffset= c.offset + buf.length();
c.shiftsCaret= false;
buf.append(TextUtilities.getDefaultLineDelimiter(d));
buf.append("=end");
}
// insert closing "end" on new line after an unclosed block
if (closeBlock() && unclosedBlock(d, trimmed, c.offset)) {
// copy old content of line behind insertion point to new line
// if (c.offset == 0) {
if (lineEnd - contentStart > 0) {
c.length= lineEnd - c.offset;
buf.append(d.get(contentStart, lineEnd - contentStart).toCharArray());
}
// }
buf.append(TextUtilities.getDefaultLineDelimiter(d));
buf.append(indent);
buf.append(BLOCK_CLOSER);
}
c.text= buf.toString();
} catch (BadLocationException e) {
RubyPlugin.log(e);
}
}
private int nextMeaningfulIndentLength(IDocument d, RubyIndenter indenter, int line) throws BadLocationException {
for (int i = line + 1; i < d.getNumberOfLines(); i++) {
IRegion nextLineRegion = d.getLineInformation(i);
String trimmed = getTrimmedLine(d, nextLineRegion.getOffset(), nextLineRegion.getOffset() + nextLineRegion.getLength());
if (trimmed == null || trimmed.length() == 0) continue;
String nextIndent= indenter.computeIndentation(nextLineRegion.getOffset()).toString();
return nextIndent.length();
}
return -1;
}
private boolean middleOfBlockRightAfterBeginning(String trimmed, String previousLine) {
return middleOfIfRightAfterBeginning(trimmed, previousLine) || middleOfBeginRightAfterBeginning(trimmed, previousLine) || elseRightAfterElsif(trimmed, previousLine)
|| ensureRightAfterRescue(trimmed, previousLine) || whenAfterCase(trimmed, previousLine) || elsifRightAfterElsif(trimmed, previousLine);
}
private boolean middleOfBeginRightAfterBeginning(String trimmed, String previousLine) {
return previousLine.equals("begin") && (trimmed.startsWith("rescue") || trimmed.equals("ensure") || trimmed.equals("rescue"));
}
private boolean middleOfIfRightAfterBeginning(String trimmed, String previousLine) {
return previousLine.startsWith("if ") && (trimmed.startsWith("elsif") || trimmed.equals("else"));
}
private boolean ensureRightAfterRescue(String trimmed, String previousLine) {
return (previousLine.startsWith("rescue ") || previousLine.equals("rescue")) && trimmed.equals("ensure");
}
private boolean elseRightAfterElsif(String trimmed, String previousLine) {
return previousLine.startsWith("elsif ") && trimmed.equals("else");
}
private boolean elsifRightAfterElsif(String trimmed, String previousLine) {
return previousLine.startsWith("elsif ") && trimmed.startsWith("elsif ");
}
private boolean whenAfterCase(String trimmed, String previousLine) {
return previousLine.startsWith("case ") && trimmed.startsWith("when ");
}
private boolean atIndentPoint(String trimmed) {
if (trimmed == null || trimmed.length() == 0) return false;
return atStartOfBlock(trimmed) || isMiddleOfBlockKeyword(trimmed);
}
private boolean isMiddleOfBlockKeyword(String trimmed) {
if (trimmed == null || trimmed.length() == 0) return false;
return trimmed.equals("rescue") || trimmed.equals("else") || trimmed.equals("ensure")
|| trimmed.startsWith("elsif ") || trimmed.startsWith("rescue ") || trimmed.startsWith("when ");
}
private boolean mightHaveToShiftCurrentLine(String trimmed) {
if (trimmed == null || trimmed.length() == 0) return false;
return isMiddleOfBlockKeyword(trimmed) || trimmed.equals(BLOCK_CLOSER);
}
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;
}
RubyParser parser = new RubyParser();
try {
parser.parse(d.get());
} catch (SyntaxException e) {
String msg = e.getMessage();
if (msg.contains("expecting") && (msg.contains("kEND") || msg.contains("kTHEN")))
return true;
try {
StringBuffer buffer = new StringBuffer(d.get());
buffer.insert(offset, TextUtilities.getDefaultLineDelimiter(d) + BLOCK_CLOSER);
parser.parse(buffer.toString());
} catch (SyntaxException syntaxException) {
return false;
}
return true;
}
return false;
}
private String getTrimmedLine(IDocument d, int start, int offset) throws BadLocationException {
String line = d.get(start, offset - start);
return line.trim();
}
private boolean atStartOfBlock(String line) {
return line.startsWith("class ") || line.startsWith("if ") || 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() {
return endStatements;
}
public void propertyChange(PropertyChangeEvent event) {
String property = event.getProperty();
if (END_STATEMENTS.equals(property)) {
endStatements = fPreferenceStore.getBoolean(property);
return;
}
}
}