/******************************************************************************* * Copyright (c) 2008 xored software, Inc. * * 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) * Appcelerator, Inc. - Improved implementation (Shalom Gibly) *******************************************************************************/ package com.aptana.editor.ruby.formatter; import java.io.StringReader; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.formatter.IFormattingContext; import org.eclipse.osgi.util.NLS; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.jrubyparser.CompatVersion; import org.jrubyparser.ast.CommentNode; import org.jrubyparser.parser.ParserResult; import com.aptana.core.logging.IdeLog; import com.aptana.editor.common.util.EditorUtil; import com.aptana.editor.ruby.RubyEditorPlugin; import com.aptana.editor.ruby.formatter.internal.RubyFormatterContext; import com.aptana.editor.ruby.formatter.internal.RubyFormatterNodeBuilder; import com.aptana.editor.ruby.formatter.internal.RubyFormatterNodeRewriter; import com.aptana.formatter.AbstractScriptFormatter; import com.aptana.formatter.FormatterDocument; import com.aptana.formatter.FormatterIndentDetector; import com.aptana.formatter.FormatterUtils; import com.aptana.formatter.FormatterWriter; import com.aptana.formatter.IDebugScopes; import com.aptana.formatter.IFormatterContext; import com.aptana.formatter.IFormatterDocument; import com.aptana.formatter.nodes.IFormatterContainerNode; import com.aptana.formatter.ui.FormatterException; import com.aptana.formatter.ui.FormatterMessages; import com.aptana.ruby.core.NullParserResult; import com.aptana.ruby.core.RubySourceParser; import com.aptana.ui.util.StatusLineMessageTimerManager; 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 }; public RubyFormatter(String lineSeparator, Map<String, String> preferences, String mainContentType) { super(preferences, mainContentType, lineSeparator); } public int detectIndentationLevel(IDocument document, int offset, boolean isSelection, IFormattingContext formattingContext) { try { ITypedRegion partition = document.getPartition(offset); if (partition != null && partition.getOffset() == offset) { return super.detectIndentationLevel(document, offset); } final String source = document.get(); final ParserResult result; RubySourceParser sourceParser = getSourceParser(); result = sourceParser.parse(source); if (!(result instanceof NullParserResult)) { final RubyFormatterNodeBuilder builder = new RubyFormatterNodeBuilder(); final FormatterDocument fDocument = createDocument(source); 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 (Throwable t) { IdeLog.logError(RubyFormatterPlugin.getDefault(), t, IDebugScopes.DEBUG); } return 0; } /* * (non-Javadoc) * @see com.aptana.formatter.ui.IScriptFormatter#getIndentSize() */ public int getIndentSize() { return getInt(RubyFormatterConstants.FORMATTER_INDENTATION_SIZE, 1); } /* * (non-Javadoc) * @see com.aptana.formatter.ui.IScriptFormatter#getIndentType() */ public String getIndentType() { return getString(RubyFormatterConstants.FORMATTER_TAB_CHAR); } /* * (non-Javadoc) * @see com.aptana.formatter.ui.IScriptFormatter#getTabSize() */ public int getTabSize() { return getInt(RubyFormatterConstants.FORMATTER_TAB_SIZE, getEditorSpecificTabWidth()); } /* * (non-Javadoc) * @see com.aptana.formatter.IScriptFormatter#getEditorSpecificTabWidth() */ public int getEditorSpecificTabWidth() { return EditorUtil.getSpaceIndentSize(RubyEditorPlugin.getDefault().getBundle().getSymbolicName()); } /* * (non-Javadoc) * @see com.aptana.formatter.IScriptFormatter#isEditorInsertSpacesForTabs() */ public boolean isEditorInsertSpacesForTabs() { return FormatterUtils.isInsertSpacesForTabs(RubyEditorPlugin.getDefault().getPreferenceStore()); } /* * (non-Javadoc) * @see com.aptana.formatter.IScriptFormatter#format(java.lang.String, int, int, int, boolean, * org.eclipse.jface.text.formatter.IFormattingContext, java.lang.String) */ public TextEdit format(String source, int offset, int length, int indent, boolean isSelection, IFormattingContext context, String indentSufix) throws FormatterException { String input = source.substring(offset, offset + length); if (isSlave()) { // We are formatting an ERB content if (input.startsWith("<%=")) { //$NON-NLS-1$ input = input.substring(3); offset += 3; length -= 3; } else if (input.startsWith("<%")) { //$NON-NLS-1$ input = input.substring(2); offset += 2; length -= 2; } if (input.endsWith("%>")) { //$NON-NLS-1$ input = input.substring(0, input.length() - 2); length -= 2; } // We also skip any new-line characters for the ERB case. Otherwise, the formatting will jump the code up. int toTrim = 0; for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); if (c == '\n' || c == '\r') { toTrim++; } else { break; } } if (toTrim > 0) { input = input.substring(toTrim); offset += toTrim; length -= toTrim; } } RubySourceParser sourceParser = getSourceParser(); ParserResult result = sourceParser.parse(input); try { if (!(result instanceof NullParserResult)) { String output = format(input, result, indent, isSelection); if (output != null) { output = trimLeft(output); if (offset > 0) { output = ' ' + output; } if (!input.equals(output)) { if (!isValidation() || equalLinesIgnoreBlanks(new StringReader(input), new StringReader(output))) { return new ReplaceEdit(offset, length, output); } else { logError(input, output); } } else { return new MultiTextEdit(); // NOP } } } else { StatusLineMessageTimerManager.setErrorMessage(NLS .bind(FormatterMessages.Formatter_formatterParsingErrorStatus, Messages.RubyFormatter_rubyParserError), ERROR_DISPLAY_TIMEOUT, true); } } catch (Throwable t) { StatusLineMessageTimerManager.setErrorMessage(FormatterMessages.Formatter_formatterErrorStatus, ERROR_DISPLAY_TIMEOUT, true); IdeLog.logError(RubyFormatterPlugin.getDefault(), t, IDebugScopes.DEBUG); } return null; } /** * @param output * @return */ private static String trimLeft(String output) { int offset = 0; for (; offset < output.length(); offset++) { if (!Character.isWhitespace(output.charAt(offset))) { break; } } if (offset != 0) { return output.substring(offset); } return output; } /** * @return RubySourceParser */ protected RubySourceParser getSourceParser() { return new RubySourceParser(CompatVersion.BOTH); } protected boolean isValidation() { return !getBoolean(RubyFormatterConstants.WRAP_COMMENTS); } /** * @param input * @param result * @return * @throws Exception */ private String format(String input, ParserResult result, int indent, boolean isSelection) throws Exception { int spacesCount = -1; if (isSelection) { spacesCount = countLeftWhitespaceChars(input); } 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, lineSeparator, createIndentGenerator()); writer.setWrapLength(getInt(RubyFormatterConstants.WRAP_COMMENTS_LENGTH)); writer.setLinesPreserve(getInt(RubyFormatterConstants.LINES_PRESERVE)); root.accept(context, writer); writer.flush(context); String output = writer.getOutput(); List<IRegion> offOnRegions = builder.getOffOnRegions(); if (offOnRegions != null && !offOnRegions.isEmpty()) { // We re-parse the output to extract its On-Off regions, so we will be able to compute the offsets and // adjust it. List<IRegion> outputOnOffRegions = getOutputOnOffRegions(output, getString(RubyFormatterConstants.FORMATTER_OFF), getString(RubyFormatterConstants.FORMATTER_ON), document); output = FormatterUtils.applyOffOnRegions(input, output, offOnRegions, outputOnOffRegions); } if (isSelection) { output = leftTrim(output, spacesCount); } return output; } private FormatterDocument createDocument(String input) { FormatterDocument document = new FormatterDocument(input); for (String key : INDENTING) { document.setBoolean(key, getBoolean(key)); } for (String key : BLANK_LINES) { document.setInt(key, getInt(key)); } document.setInt(RubyFormatterConstants.FORMATTER_TAB_SIZE, getInt(RubyFormatterConstants.FORMATTER_TAB_SIZE)); document.setBoolean(RubyFormatterConstants.WRAP_COMMENTS, getBoolean(RubyFormatterConstants.WRAP_COMMENTS)); // Formatter OFF/ON document.setBoolean(RubyFormatterConstants.FORMATTER_OFF_ON_ENABLED, getBoolean(RubyFormatterConstants.FORMATTER_OFF_ON_ENABLED)); document.setString(RubyFormatterConstants.FORMATTER_ON, getString(RubyFormatterConstants.FORMATTER_ON)); document.setString(RubyFormatterConstants.FORMATTER_OFF, getString(RubyFormatterConstants.FORMATTER_OFF)); return document; } // Collect and return the regions that will be excluded from formatting (between the 'OFF' and 'ON' tags). private List<IRegion> getOutputOnOffRegions(String output, String formatterOffPattern, String formatterOnPattern, IFormatterDocument document) { RubySourceParser sourceParser = getSourceParser(); ParserResult result = sourceParser.parse(output); if (result != null && !(result instanceof NullParserResult)) { return collectOffOnRegions(result.getCommentNodes(), document); } return null; } /** * Resolves the formatter's Off-On regions.<br> * The method will try to collect the 'Off' and 'On' tags and set the regions that will be ignored when formatting.<br> * * @param commentNodes * @param document * @return A list of regions (may be null) */ public static List<IRegion> collectOffOnRegions(List<CommentNode> commentNodes, IFormatterDocument document) { if (!document.getBoolean(RubyFormatterConstants.FORMATTER_OFF_ON_ENABLED) || commentNodes == null) { return null; } LinkedHashMap<Integer, String> commentsMap = new LinkedHashMap<Integer, String>(commentNodes.size()); for (CommentNode comment : commentNodes) { int start = comment.getPosition().getStartOffset(); commentsMap.put(start, comment.getContent()); } // Generate the OFF/ON regions if (!commentsMap.isEmpty()) { Pattern onPattern = Pattern.compile(Pattern.quote(document.getString(RubyFormatterConstants.FORMATTER_ON))); Pattern offPattern = Pattern .compile(Pattern.quote(document.getString(RubyFormatterConstants.FORMATTER_OFF))); return FormatterUtils.resolveOnOffRegions(commentsMap, onPattern, offPattern, document.getLength() - 1); } return null; } }