package org.rubypeople.rdt.internal.formatter; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.eclipse.core.runtime.Platform; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.formatter.CodeFormatter; import org.rubypeople.rdt.core.formatter.DefaultCodeFormatterConstants; import org.rubypeople.rdt.core.formatter.Indents; public class OldCodeFormatter extends CodeFormatter { private final static String BLOCK_BEGIN_RE = "(class|module|def|if|unless|case|while|until|for|begin|do)"; private String BLOCK_MID_RE = "(else|elsif|rescue|ensure)"; private final static String BLOCK_END_RE = "(end)"; private final static String DELIMITER_RE = "[?$/(){}#\\`.:\\]\\[]"; private final static String[] LITERAL_BEGIN_LITERALS = { "\"", "'", "=begin", "%[Qqrxw]?.", "/", "<<[\\-]?[']?[a-zA-Z_]+[']?", ":[a-zA-Z]+[a-zA-z_\\d]*" }; // FIXME Need to handle array concats that looks like heredocs properly i.e. // "@recipients<< '<'<<recipients<<'>'" private final static String[] LITERAL_END_RES = { "[^\\\\](\\\\\\\\)*\"", "[^\\\\](\\\\\\\\)*'", "=end", "", "", "", "" }; private final int BLOCK_BEGIN_PAREN = 2; private final int BLOCK_MID_PAREN = 5; private final int BLOCK_END_PAREN = 8; private final int LITERAL_BEGIN_PAREN = 10; private static Pattern MODIFIER_RE; private static Pattern OPERATOR_RE; private static Pattern NON_BLOCK_DO_RE; private static String LITERAL_BEGIN_RE; // automatically concatenated from // LITERAL_BEGIN_LITERALS private static Pattern[] LITERAL_END_RES_COMPILED; static { LITERAL_END_RES_COMPILED = new Pattern[LITERAL_END_RES.length]; for (int i = 0; i < LITERAL_END_RES.length; i++) { try { LITERAL_END_RES_COMPILED[i] = Pattern.compile(LITERAL_END_RES[i]); } catch (PatternSyntaxException e) { System.out.println(e); } } StringBuffer sb = new StringBuffer(); sb.append("("); for (int i = 0; i < LITERAL_BEGIN_LITERALS.length; i++) { sb.append(LITERAL_BEGIN_LITERALS[i]); if (i < LITERAL_BEGIN_LITERALS.length - 1) { sb.append("|"); } } sb.append(")"); LITERAL_BEGIN_RE = sb.toString(); try { MODIFIER_RE = Pattern.compile("if|unless|while|until|rescue"); OPERATOR_RE = Pattern.compile("[\\-,.+*/%&|\\^~=<>:]"); NON_BLOCK_DO_RE = Pattern.compile("(^|[\\s])(while|until|for|rescue)[\\s]"); } catch (PatternSyntaxException e) { System.out.println(e); } } private DefaultCodeFormatterOptions preferences; private Map options; public OldCodeFormatter() { this(new DefaultCodeFormatterOptions(DefaultCodeFormatterConstants.getRubyConventionsSettings()), null); } public OldCodeFormatter(DefaultCodeFormatterOptions preferences) { this(preferences, null); } public OldCodeFormatter(DefaultCodeFormatterOptions defaultCodeFormatterOptions, Map options) { if (options != null) { this.options = options; this.preferences = new DefaultCodeFormatterOptions(options); } else { this.options = RubyCore.getOptions(); this.preferences = new DefaultCodeFormatterOptions(DefaultCodeFormatterConstants .getRubyConventionsSettings()); } if (defaultCodeFormatterOptions != null) { this.preferences.set(defaultCodeFormatterOptions.getMap()); } } public OldCodeFormatter(Map options) { this(null, options); } /** * @deprecated As of 0.8.0 we're moving to use CodeFormatters that spit out TextEdits. * @param unformatted * @return */ public synchronized String formatString(String unformatted) { AbstractBlockMarker firstAbstractBlockMarker = this.createBlockMarkerList(unformatted); if (isDebug()) { firstAbstractBlockMarker.print(); } int initialIndentLevel = Indents.measureIndentUnits(unformatted, preferences.tab_size, preferences.indentation_size); try { return this.formatString(unformatted, firstAbstractBlockMarker, initialIndentLevel); } catch (PatternSyntaxException ex) { return unformatted; } } private boolean isDebug() { String codeFormatterOption = Platform.getDebugOption(RubyCore.PLUGIN_ID + "/codeformatter"); boolean isDebug = codeFormatterOption == null ? false : codeFormatterOption.equalsIgnoreCase("true"); return isDebug; } protected String formatString(String unformatted, AbstractBlockMarker abstractBlockMarker, int initialIndentLevel) throws PatternSyntaxException { Pattern pat = Pattern.compile("\n"); String[] lines = pat.split(unformatted); IndentationState state = null; StringBuffer formatted = new StringBuffer(); Pattern whitespacePattern = Pattern.compile("^[\t ]*"); for (int i = 0; i < lines.length; i++) { Matcher whitespaceMatcher = whitespacePattern.matcher(lines[i]); whitespaceMatcher.find(); int leadingWhitespace = whitespaceMatcher.end(0); if (state == null) { state = new IndentationState(unformatted, leadingWhitespace, initialIndentLevel); } state.incPos(leadingWhitespace); String strippedLine = new String(lines[i].substring(leadingWhitespace)); AbstractBlockMarker newBlockMarker = this.findNextBlockMarker(abstractBlockMarker, state.getPos(), state); if (newBlockMarker != null) { newBlockMarker.indentBeforePrint(state); newBlockMarker.appendIndentedLine(formatted, state, lines[i], strippedLine, options); newBlockMarker.indentAfterPrint(state); abstractBlockMarker = newBlockMarker; } else { abstractBlockMarker.appendIndentedLine(formatted, state, lines[i], strippedLine, options); } if (i != lines.length - 1) { formatted.append("\n"); } state.incPos(strippedLine.length() + 1); } if (unformatted.lastIndexOf("\n") == unformatted.length() - 1) { formatted.append("\n"); } return formatted.toString(); } private AbstractBlockMarker findNextBlockMarker(AbstractBlockMarker abstractBlockMarker, int pos, IndentationState state) { AbstractBlockMarker startBlockMarker = abstractBlockMarker; while (abstractBlockMarker.getNext() != null && abstractBlockMarker.getNext().getPos() <= pos) { if (abstractBlockMarker != startBlockMarker) { abstractBlockMarker.indentBeforePrint(state); abstractBlockMarker.indentAfterPrint(state); } abstractBlockMarker = abstractBlockMarker.getNext(); } return startBlockMarker == abstractBlockMarker ? null : abstractBlockMarker; } protected AbstractBlockMarker createBlockMarkerList(String unformatted) { Pattern pat = null; try { pat = Pattern.compile("(^|[\\s]|;)" + BLOCK_BEGIN_RE + "($|[\\s]|" + DELIMITER_RE + ")|(^|[\\s])" + getBlockMiddleRegex() + "($|[\\s]|" + DELIMITER_RE + ")|(^|[\\s]|;)" + BLOCK_END_RE + "($|[\\s]|;)|" + LITERAL_BEGIN_RE + "|" + DELIMITER_RE); } catch (PatternSyntaxException e) { System.out.println(e); } int pos = 0; AbstractBlockMarker lastBlockMarker = new NeutralMarker("start", 0); AbstractBlockMarker firstBlockMarker = lastBlockMarker; Matcher re = pat.matcher(unformatted); while (pos != -1 && re.find(pos)) { AbstractBlockMarker newBlockMarker = null; if (re.group(BLOCK_BEGIN_PAREN) != null) { pos = re.end(BLOCK_BEGIN_PAREN); String blockBeginStr = re.group(BLOCK_BEGIN_PAREN); if (MODIFIER_RE.matcher(blockBeginStr).matches() && !this.isRubyExprBegin(unformatted, re.start(BLOCK_BEGIN_PAREN), "modifier")) { continue; } if (blockBeginStr.equals("do") && this.isNonBlockDo(unformatted, re.start(BLOCK_BEGIN_PAREN))) { continue; } newBlockMarker = new BeginBlockMarker(re.group(BLOCK_BEGIN_PAREN), re.start(BLOCK_BEGIN_PAREN)); } else if (re.group(BLOCK_MID_PAREN) != null) { pos = re.end(BLOCK_MID_PAREN); String blockMiddleStr = re.group(BLOCK_MID_PAREN); if (MODIFIER_RE.matcher(blockMiddleStr).matches() && !this.isRubyExprBegin(unformatted, re.start(BLOCK_MID_PAREN), "modifier")) { continue; } newBlockMarker = new MidBlockMarker(re.group(BLOCK_MID_PAREN), re.start(BLOCK_MID_PAREN)); } else if (re.group(BLOCK_END_PAREN) != null) { pos = re.end(BLOCK_END_PAREN); newBlockMarker = new EndBlockMarker(re.group(BLOCK_END_PAREN), re.start(BLOCK_END_PAREN)); } else if (re.group(LITERAL_BEGIN_PAREN) != null) { pos = re.end(LITERAL_BEGIN_PAREN); String matchedLiteralBegin = re.group(LITERAL_BEGIN_PAREN); if (matchedLiteralBegin.startsWith("%")) { int delimitChar = matchedLiteralBegin.charAt(matchedLiteralBegin.length() - 1); boolean expand = matchedLiteralBegin.charAt(1) != 'q'; if (delimitChar == '[') { pos = this.forwardString(unformatted, pos, '[', ']', expand); } else if (delimitChar == '(') { pos = this.forwardString(unformatted, pos, '(', ')', expand); } else if (delimitChar == '{') { pos = this.forwardString(unformatted, pos, '{', '}', expand); } else if (delimitChar == '<') { pos = this.forwardString(unformatted, pos, '<', '>', expand); } else { pos = unformatted.indexOf(delimitChar, pos); } } else if (matchedLiteralBegin.startsWith("/")) { // we do not consider reg exp over multiple lines. Therefore // a reg exp over // mutliple lines might get formatted. On the other hand // code between // two division slashes on several lines is being formatted. // We could avoid that // behaviour only if we could make a difference between // slashes for division and // slashes for regular expressions. int posClosingSlash = this.forwardString(unformatted, pos, ' ', "/", true); if (posClosingSlash == pos) { continue; } int posNextLine = unformatted.indexOf("\n", pos); if (posNextLine != -1 && posClosingSlash > posNextLine) { continue; } pos = posClosingSlash; } else if (matchedLiteralBegin.startsWith("'")) { if (pos > 1 && unformatted.charAt(pos - 2) == '$') { continue; } pos = this.forwardString(unformatted, pos, ' ', "'", true); } else if (matchedLiteralBegin.startsWith("<<")) { int startId = 2; int endId = matchedLiteralBegin.length(); boolean isMinus = (matchedLiteralBegin.charAt(startId) == '-'); if (isMinus) { startId += 1; } if (startId < matchedLiteralBegin.length() - 1 && matchedLiteralBegin.charAt(startId) == '\'') { startId += 1; endId -= 1; } String reStr = (isMinus ? "" : "\n") + matchedLiteralBegin.substring(startId, endId); try { Pattern idSearch = Pattern.compile(reStr); Matcher matcher = idSearch.matcher(unformatted); if (matcher.find(pos)) { pos = matcher.end(0); } else { pos = -1; } } catch (PatternSyntaxException e1) { continue; } } else { for (int i = 0; i < LITERAL_BEGIN_LITERALS.length; i++) { if (LITERAL_BEGIN_LITERALS[i].equals(matchedLiteralBegin)) { Pattern matchEnd = LITERAL_END_RES_COMPILED[i]; pos = -1; Matcher tmpMatch = matchEnd.matcher(unformatted); if (tmpMatch.find(re.end(LITERAL_BEGIN_PAREN) - 1)) { pos = tmpMatch.end(0); } break; } } } newBlockMarker = new NoFormattingMarker(matchedLiteralBegin, re.start(LITERAL_BEGIN_PAREN)); if (pos != -1) { lastBlockMarker.setNext(newBlockMarker); lastBlockMarker = newBlockMarker; newBlockMarker = new NeutralMarker("", pos); } } else { String delimiter = re.group(0); if (delimiter.equals("#")) { pos = unformatted.indexOf("\n", re.end(0)); continue; } else if (delimiter.equals("{")) { newBlockMarker = new BeginBlockMarker("{", re.start(0)); } else if (delimiter.equals("}")) { newBlockMarker = new EndBlockMarker("}", re.start(0)); } else if (delimiter.equals("(")) { newBlockMarker = new FixLengthMarker("(", re.start(0)); } else if (delimiter.equals(")")) { newBlockMarker = new NeutralMarker(")", re.start(0)); } pos = re.end(0); } if (newBlockMarker == null) { continue; } if (lastBlockMarker != null) { // if (!lastBlockMarker.getKeyword().equals("begin") && newBlockMarker.getKeyword().equals("rescue")) { // // we have a rescue modifier? // continue; // } lastBlockMarker.setNext(newBlockMarker); } lastBlockMarker = newBlockMarker; } return firstBlockMarker; } private String getBlockMiddleRegex() { if (this.preferences.indent_case_body) { return BLOCK_MID_RE; } StringBuffer buffer = new StringBuffer(BLOCK_MID_RE); buffer.insert(1, "when|"); return buffer.toString(); } /* * (defun ruby-forward-string (term &optional end no-error expand) (let ((n 1) (c (string-to-char term)) (re (if * expand (concat "[^\\]\\(\\\\\\\\\\)*\\([" term "]\\|\\(#{\\)\\)") (concat "[^\\]\\(\\\\\\\\\\)*[" term "]")))) * (while (and (re-search-forward re end no-error) (if (match-beginning 3) (ruby-forward-string "}{" end no-error * nil) (> (setq n (if (eq (char-before (point)) c) (1- n) (1+ n))) 0))) (forward-char -1)) (cond ((zerop n)) * (no-error nil) (error "unterminated string")))) */ protected int forwardString(String unformatted, int pos, char opening, char closing, boolean expand) { return this.forwardString(unformatted, pos, opening, "\\" + opening + "\\" + closing, expand); } protected int forwardString(String unformatted, int pos, char opening, String term, boolean expand) { int n = 1; try { Pattern pat = Pattern.compile(expand ? "[" + term + "]|(#\\{)" : "[" + term + "]"); Matcher re = pat.matcher(unformatted); while (re.find(pos) && n > 0) { if (re.group(1) != null) { pos = this.forwardString(unformatted, re.end(1), '{', "\\{\\}", expand); } else { pos = re.end(0); if (pos > 2 && unformatted.charAt(pos - 2) == '\\' && unformatted.charAt(pos - 3) != '\\') { continue; } if (re.group(0).charAt(0) == opening) { n += 1; } else { n -= 1; } } } } catch (PatternSyntaxException e) { e.printStackTrace(); } return pos; } /* * (defun ruby-expr-beg (&optional option) (save-excursion (store-match-data nil) (skip-chars-backward " \t") (cond * ((bolp) t) ((looking-at "\\?") (or (bolp) (forward-char -1)) (not (looking-at "\\sw"))) (t (forward-char -1) (or * (looking-at ruby-operator-re) (looking-at "[\\[({,;]") (and (not (eq option 'modifier)) (looking-at "[!?]")) (and * (looking-at ruby-symbol-re) (skip-chars-backward ruby-symbol-chars) (cond ((or (looking-at ruby-block-beg-re) * (looking-at ruby-block-op-re) (looking-at ruby-block-mid-re)) (goto-char (match-end 0)) (looking-at "\\>")) (t * (and (not (eq option 'expr-arg)) (looking-at "[a-zA-Z][a-zA-z0-9_]* +/[^ \t]")))))))))) */ protected int skipCharsBackward(String unformatted, int pos) { // skipCharsBackward returns the position of the first char which is not // tab or space left from pos and is in the // same line as pos do { if (pos == 0) { return 0; } if (unformatted.charAt(pos - 1) == '\n') { return pos; } pos -= 1; } while (unformatted.charAt(pos) == '\t' || unformatted.charAt(pos) == ' '); return pos; } protected int backToIndentation(String unformatted, int pos) { do { if (pos == 0) { return 0; } if (unformatted.charAt(pos - 1) == '\n') { break; } pos -= 1; } while (true); while (unformatted.charAt(pos) == '\t' || unformatted.charAt(pos) == ' ') { pos += 1; if (pos == unformatted.length()) { break; } } return pos; } protected int posOfLineStart(String unformatted, int pos) { do { if (pos == 0) { return 0; } if (unformatted.charAt(pos - 1) == '\n') { break; } pos -= 1; } while (true); return pos; } protected boolean matchREBackward(String str, Pattern re) { int pos = str.length() - 1; while (pos >= 0) { if (str.charAt(pos) == ';') { return false; } if (re.matcher(str).find(pos)) { return true; } pos -= 1; } return false; } protected boolean isRubyExprBegin(String unformatted, int pos, String option) { int firstNonSpaceCharInLine = this.skipCharsBackward(unformatted, pos); if (firstNonSpaceCharInLine == 0 || unformatted.charAt(firstNonSpaceCharInLine - 1) == '\n') { return true; } char c = unformatted.charAt(firstNonSpaceCharInLine); if (c == ';') { return true; } String c_str = "" + c; if (OPERATOR_RE.matcher(c_str).matches()) { return true; } return false; } protected boolean isNonBlockDo(String unformatted, int pos) { int lineStart = this.posOfLineStart(unformatted, pos); return this.matchREBackward(new String(unformatted.substring(lineStart, pos)), NON_BLOCK_DO_RE); } public TextEdit format(int kind, String source, int offset, int length, int indentationLevel, String lineSeparator) { String newText = formatString(new String(source.substring(offset, length)), indentationLevel); return new ReplaceEdit(offset, length, newText); } private String formatString(String unformatted, int indentationLevel) { AbstractBlockMarker firstAbstractBlockMarker = this.createBlockMarkerList(unformatted); if (isDebug()) { firstAbstractBlockMarker.print(); } try { return this.formatString(unformatted, firstAbstractBlockMarker, indentationLevel); } catch (PatternSyntaxException ex) { return unformatted; } } }