blob: c90d7572c0fc4898e230e206895f36aca5b7a17d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 xored software, Inc. and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* xored software, Inc. - initial API and Implementation (Alex Panchenko)
*******************************************************************************/
package org.eclipse.dltk.ruby.formatter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.Map;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.dltk.formatter.AbstractScriptFormatter;
import org.eclipse.dltk.formatter.FormatterDocument;
import org.eclipse.dltk.formatter.FormatterIndentDetector;
import org.eclipse.dltk.formatter.FormatterWriter;
import org.eclipse.dltk.formatter.IFormatterContainerNode;
import org.eclipse.dltk.formatter.IFormatterContext;
import org.eclipse.dltk.ruby.formatter.internal.DumpContentException;
import org.eclipse.dltk.ruby.formatter.internal.Messages;
import org.eclipse.dltk.ruby.formatter.internal.RubyFormatterContext;
import org.eclipse.dltk.ruby.formatter.internal.RubyFormatterNodeBuilder;
import org.eclipse.dltk.ruby.formatter.internal.RubyFormatterNodeRewriter;
import org.eclipse.dltk.ruby.formatter.internal.RubyFormatterPlugin;
import org.eclipse.dltk.ruby.formatter.internal.RubyParser;
import org.eclipse.dltk.ui.formatter.FormatterException;
import org.eclipse.dltk.ui.formatter.FormatterSyntaxProblemException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.jruby.parser.RubyParserResult;
public class RubyFormatter extends AbstractScriptFormatter {
protected static final String[] INDENTING = {
RubyFormatterConstants.INDENT_CLASS,
RubyFormatterConstants.INDENT_MODULE,
RubyFormatterConstants.INDENT_METHOD,
RubyFormatterConstants.INDENT_BLOCKS,
RubyFormatterConstants.INDENT_IF,
RubyFormatterConstants.INDENT_CASE,
RubyFormatterConstants.INDENT_WHEN };
protected static final String[] BLANK_LINES = {
RubyFormatterConstants.LINES_FILE_AFTER_REQUIRE,
RubyFormatterConstants.LINES_FILE_BETWEEN_MODULE,
RubyFormatterConstants.LINES_FILE_BETWEEN_CLASS,
RubyFormatterConstants.LINES_FILE_BETWEEN_METHOD,
RubyFormatterConstants.LINES_BEFORE_FIRST,
RubyFormatterConstants.LINES_BEFORE_MODULE,
RubyFormatterConstants.LINES_BEFORE_CLASS,
RubyFormatterConstants.LINES_BEFORE_METHOD };
private final String lineDelimiter;
public RubyFormatter(String lineDelimiter, Map<String, ? extends Object> preferences) {
super(preferences);
this.lineDelimiter = lineDelimiter;
}
@Override
public int detectIndentationLevel(IDocument document, int offset) {
final String input = document.get();
final RubyParserResult result;
try {
result = RubyParser.parse(input);
final RubyFormatterNodeBuilder builder = new RubyFormatterNodeBuilder();
final FormatterDocument fDocument = createDocument(input);
IFormatterContainerNode root = builder.build(result, fDocument);
new RubyFormatterNodeRewriter(result).rewrite(root);
final IFormatterContext context = new RubyFormatterContext(0);
FormatterIndentDetector detector = new FormatterIndentDetector(
offset);
try {
root.accept(context, detector);
return detector.getLevel();
} catch (Exception e) {
// ignore
}
} catch (FormatterSyntaxProblemException e) {
// ignore
}
// TODO keep current indent
return 0;
}
@Override
public TextEdit format(String source, int offset, int length, int indent)
throws FormatterException {
final String input = source.substring(offset, offset + length);
final RubyParserResult result = RubyParser.parse(input);
final String output = format(input, result, indent);
if (output != null) {
if (!input.equals(output)) {
if (!isValidation()
|| equalsIgnoreBlanks(new StringReader(input),
new StringReader(output))) {
return new ReplaceEdit(offset, length, output);
} else {
RubyFormatterPlugin.log(new Status(IStatus.ERROR,
RubyFormatterPlugin.PLUGIN_ID, IStatus.OK,
Messages.RubyFormatter_contentCorrupted,
new DumpContentException(input)));
}
} else {
return new MultiTextEdit(); // NOP
}
}
return null;
}
protected boolean isValidation() {
return !getBoolean(RubyFormatterConstants.WRAP_COMMENTS);
}
/**
* @param input
* @param result
* @return
*/
private String format(String input, RubyParserResult result, int indent) {
final RubyFormatterNodeBuilder builder = new RubyFormatterNodeBuilder();
final FormatterDocument document = createDocument(input);
IFormatterContainerNode root = builder.build(result, document);
new RubyFormatterNodeRewriter(result).rewrite(root);
IFormatterContext context = new RubyFormatterContext(indent);
FormatterWriter writer = new FormatterWriter(document, lineDelimiter,
createIndentGenerator());
writer
.setWrapLength(getInt(RubyFormatterConstants.WRAP_COMMENTS_LENGTH));
writer.setLinesPreserve(getInt(RubyFormatterConstants.LINES_PRESERVE));
try {
root.accept(context, writer);
writer.flush(context);
return writer.getOutput();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private FormatterDocument createDocument(String input) {
FormatterDocument document = new FormatterDocument(input);
for (int i = 0; i < INDENTING.length; ++i) {
document.setBoolean(INDENTING[i], getBoolean(INDENTING[i]));
}
for (int i = 0; i < BLANK_LINES.length; ++i) {
document.setInt(BLANK_LINES[i], getInt(BLANK_LINES[i]));
}
document.setInt(RubyFormatterConstants.FORMATTER_TAB_SIZE,
getInt(RubyFormatterConstants.FORMATTER_TAB_SIZE));
document.setBoolean(RubyFormatterConstants.WRAP_COMMENTS,
getBoolean(RubyFormatterConstants.WRAP_COMMENTS));
return document;
}
public static boolean isBooleanOption(String optionName) {
for (int i = 0; i < INDENTING.length; ++i) {
if (INDENTING[i].equals(optionName)) {
return true;
}
}
return false;
}
public static boolean isIntegerOption(String optionName) {
for (int i = 0; i < BLANK_LINES.length; ++i) {
if (BLANK_LINES[i].equals(optionName)) {
return true;
}
}
return false;
}
private boolean equalsIgnoreBlanks(Reader inputReader, Reader outputReader) {
LineNumberReader input = new LineNumberReader(inputReader);
LineNumberReader output = new LineNumberReader(outputReader);
for (;;) {
final String inputLine = readLine(input);
final String outputLine = readLine(output);
if (inputLine == null) {
if (outputLine == null) {
return true;
} else {
return false;
}
} else if (outputLine == null) {
return false;
} else if (!inputLine.equals(outputLine)) {
return false;
}
}
}
private String readLine(LineNumberReader reader) {
String line;
do {
try {
line = reader.readLine();
} catch (IOException e) {
// should not happen
return null;
}
if (line == null) {
return line;
}
line = line.trim();
} while (line.length() == 0);
return line;
}
}