/* * Copyright (c) 2012, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.ui.internal.text.functions; import com.google.dart.tools.core.DartCore; import com.google.dart.tools.core.formatter.DefaultCodeFormatterConstants; import com.google.dart.tools.ui.DartToolsPlugin; import com.google.dart.tools.ui.DartX; import com.google.dart.tools.ui.internal.util.CodeFormatterUtil; import com.google.dart.tools.ui.text.editor.tmp.JavaScriptCore; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; /** * Uses the {@link com.google.dart.tools.ui.DartHeuristicScanner} to get the indentation level for a * certain position in a document. * <p> * An instance holds some internal position in the document and is therefore not thread-safe. */ public class DartIndenter { /** * The Dart Core preferences. */ private final class CorePrefs { final boolean prefUseTabs; final int prefTabSize; final int prefIndentationSize; final boolean prefArrayDimensionsDeepIndent; final int prefArrayIndent; final boolean prefArrayDeepIndent; final boolean prefTernaryDeepAlign; final int prefTernaryIndent; final int prefCaseIndent; final int prefCaseBlockIndent; final int prefSimpleIndent; final int prefBracketIndent; final boolean prefMethodDeclDeepIndent; final int prefMethodDeclIndent; final boolean prefMethodCallDeepIndent; final int prefMethodCallIndent; final boolean prefParenthesisDeepIndent; final int prefParenthesisIndent; final int prefBlockIndent; final int prefMethodBodyIndent; final int prefTypeIndent; final boolean prefIndentBracesForBlocks; final boolean prefIndentBracesForArrays; final boolean prefIndentBracesForMethods; final boolean prefIndentBracesForTypes; final int prefContinuationIndent; final boolean prefHasGenerics; final String prefTabChar; final int prefDefunIndent; private final IProject fProject; CorePrefs(IProject project) { fProject = project; if (isStandalone()) { prefUseTabs = false; prefTabSize = CodeFormatterUtil.getTabWidth(); prefIndentationSize = CodeFormatterUtil.getTabWidth(); prefArrayDimensionsDeepIndent = true; prefContinuationIndent = 2; prefBlockIndent = 1; prefArrayIndent = prefContinuationIndent; prefArrayDeepIndent = true; prefTernaryDeepAlign = false; prefTernaryIndent = prefContinuationIndent; prefCaseIndent = 1; prefCaseBlockIndent = prefBlockIndent; prefIndentBracesForBlocks = false; prefSimpleIndent = (prefIndentBracesForBlocks && prefBlockIndent == 0) ? 1 : prefBlockIndent; prefBracketIndent = prefBlockIndent; prefMethodDeclDeepIndent = true; prefMethodDeclIndent = 1; prefMethodCallDeepIndent = false; prefMethodCallIndent = 2; prefParenthesisDeepIndent = false; prefParenthesisIndent = prefContinuationIndent; prefMethodBodyIndent = 1; prefTypeIndent = 1; prefIndentBracesForArrays = false; prefIndentBracesForMethods = false; prefIndentBracesForTypes = false; prefHasGenerics = true; prefTabChar = JavaScriptCore.SPACE; prefDefunIndent = prefContinuationIndent; } else { prefUseTabs = prefUseTabs(); prefTabSize = prefTabSize(); prefIndentationSize = prefIndentationSize(); prefArrayDimensionsDeepIndent = prefArrayDimensionsDeepIndent(); prefContinuationIndent = prefContinuationIndent(); prefBlockIndent = prefBlockIndent(); prefArrayIndent = prefArrayIndent(); prefArrayDeepIndent = prefArrayDeepIndent(); prefTernaryDeepAlign = prefTernaryDeepAlign(); prefTernaryIndent = prefTernaryIndent(); prefCaseIndent = prefCaseIndent(); prefCaseBlockIndent = prefCaseBlockIndent(); prefIndentBracesForBlocks = prefIndentBracesForBlocks(); prefSimpleIndent = prefSimpleIndent(); prefBracketIndent = prefBracketIndent(); prefMethodDeclDeepIndent = prefMethodDeclDeepIndent(); prefMethodDeclIndent = prefMethodDeclIndent(); prefMethodCallDeepIndent = prefMethodCallDeepIndent(); prefMethodCallIndent = prefMethodCallIndent(); prefParenthesisDeepIndent = prefParenthesisDeepIndent(); prefParenthesisIndent = prefParenthesisIndent(); prefMethodBodyIndent = prefMethodBodyIndent(); prefTypeIndent = prefTypeIndent(); prefIndentBracesForArrays = prefIndentBracesForArrays(); prefIndentBracesForMethods = prefIndentBracesForMethods(); prefIndentBracesForTypes = prefIndentBracesForTypes(); prefHasGenerics = hasGenerics(); prefTabChar = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR); prefDefunIndent = prefDefunIndent(); } } /** * Returns the possibly project-specific core preference defined under <code>key</code>. * * @param key the key of the preference * @return the value of the preference */ private String getCoreFormatterOption(String key) { // if (fProject != null) { // return fProject.getOption(key, true); // } return DartCore.getOption(key); } private boolean hasGenerics() { return true; } /** * Returns <code>true</code> if the class is used outside the workbench, <code>false</code> in * normal mode * * @return <code>true</code> if the plug-ins are not available */ private boolean isStandalone() { DartX.todo(); return true; // return JavaScriptCore.getPlugin() == null; } private boolean prefArrayDeepIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_EXPRESSIONS_IN_ARRAY_INITIALIZER); try { return DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_ON_COLUMN; } catch (IllegalArgumentException e) { // ignore and return default } return true; } private boolean prefArrayDimensionsDeepIndent() { return true; // sensible default, no formatter setting } private int prefArrayIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_EXPRESSIONS_IN_ARRAY_INITIALIZER); try { if (DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_BY_ONE) { return 1; } } catch (IllegalArgumentException e) { // ignore and return default } return prefContinuationIndent(); // default } private int prefBlockIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_STATEMENTS_COMPARE_TO_BLOCK); if (DefaultCodeFormatterConstants.FALSE.equals(option)) { return 0; } return 1; // sensible default } private int prefBracketIndent() { return prefBlockIndent(); } private int prefCaseBlockIndent() { if (DefaultCodeFormatterConstants.TRUE.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_SWITCHSTATEMENTS_COMPARE_TO_CASES))) { return prefBlockIndent(); } else { return 1; } } private int prefCaseIndent() { if (DefaultCodeFormatterConstants.TRUE.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_SWITCHSTATEMENTS_COMPARE_TO_SWITCH))) { return prefBlockIndent(); } else { return 1; } } private int prefContinuationIndent() { try { return Integer.parseInt(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_CONTINUATION_INDENTATION)); } catch (NumberFormatException e) { // ignore and return default } return 2; // sensible default } private int prefDefunIndent() { return prefContinuationIndent(); } private int prefIndentationSize() { return CodeFormatterUtil.getIndentWidth(fProject); } private boolean prefIndentBracesForArrays() { return DefaultCodeFormatterConstants.NEXT_LINE_SHIFTED.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_ARRAY_INITIALIZER)); } private boolean prefIndentBracesForBlocks() { return DefaultCodeFormatterConstants.NEXT_LINE_SHIFTED.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_BLOCK)); } private boolean prefIndentBracesForMethods() { return DefaultCodeFormatterConstants.NEXT_LINE_SHIFTED.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_METHOD_DECLARATION)); } private boolean prefIndentBracesForTypes() { return DefaultCodeFormatterConstants.NEXT_LINE_SHIFTED.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_TYPE_DECLARATION)); } private int prefMethodBodyIndent() { if (DefaultCodeFormatterConstants.FALSE.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_STATEMENTS_COMPARE_TO_BODY))) { return 0; } return 1; // sensible default } private boolean prefMethodCallDeepIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_ARGUMENTS_IN_METHOD_INVOCATION); try { return DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_ON_COLUMN; } catch (IllegalArgumentException e) { // ignore and return default } return false; // sensible default } private int prefMethodCallIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_ARGUMENTS_IN_METHOD_INVOCATION); try { if (DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_BY_ONE) { return 1; } else { return prefContinuationIndent(); } } catch (IllegalArgumentException e) { // ignore and return default } return 1; // sensible default } private boolean prefMethodDeclDeepIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_PARAMETERS_IN_METHOD_DECLARATION); try { return DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_ON_COLUMN; } catch (IllegalArgumentException e) { // ignore and return default } return true; } private int prefMethodDeclIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_PARAMETERS_IN_METHOD_DECLARATION); try { if (DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_BY_ONE) { return 1; } else { return prefContinuationIndent(); } } catch (IllegalArgumentException e) { // ignore and return default } return 1; } private boolean prefParenthesisDeepIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_CONTINUATION_INDENTATION); try { return DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_ON_COLUMN; } catch (IllegalArgumentException e) { // ignore and return default } return false; // sensible default } private int prefParenthesisIndent() { return prefContinuationIndent(); } private int prefSimpleIndent() { if (prefIndentBracesForBlocks() && prefBlockIndent() == 0) { return 1; } else { return prefBlockIndent(); } } private int prefTabSize() { return CodeFormatterUtil.getTabWidth(); } private boolean prefTernaryDeepAlign() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_CONDITIONAL_EXPRESSION); try { return DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_ON_COLUMN; } catch (IllegalArgumentException e) { // ignore and return default } return false; } private int prefTernaryIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_CONDITIONAL_EXPRESSION); try { if (DefaultCodeFormatterConstants.getIndentStyle(option) == DefaultCodeFormatterConstants.INDENT_BY_ONE) { return 1; } else { return prefContinuationIndent(); } } catch (IllegalArgumentException e) { // ignore and return default } return prefContinuationIndent(); } private int prefTypeIndent() { String option = getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_BODY_DECLARATIONS_COMPARE_TO_TYPE_HEADER); if (DefaultCodeFormatterConstants.FALSE.equals(option)) { return 0; } return 1; // sensible default } private boolean prefUseTabs() { return !JavaScriptCore.SPACE.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR)); } } /** The document being scanned. */ private final IDocument fDocument; /** The indentation accumulated by <code>findReferencePosition</code>. */ private int fIndent; /** * The absolute (character-counted) indentation offset for special cases (method defs, array * initializers) */ private int fAlign; /** The stateful scan position for the indentation methods. */ private int fPosition; /** The previous position. */ private int fPreviousPos; /** The most recent token. */ private int fToken; /** The line of <code>fPosition</code>. */ private int fLine; /** * The scanner we will use to scan the document. It has to be installed on the same document as * the one we get. */ private final DartHeuristicScanner fScanner; /** * The Dart Core preferences. */ private final CorePrefs fPrefs; private int extraIndent = 0; /** * Creates a new instance. * * @param document the document to scan * @param scanner the {@link DartHeuristicScanner} to be used for scanning the document. It must * be installed on the same <code>IDocument</code>. */ public DartIndenter(IDocument document, DartHeuristicScanner scanner) { this(document, scanner, null); } /** * Creates a new instance. * * @param document the document to scan * @param scanner the {@link DartHeuristicScanner}to be used for scanning the document. It must be * installed on the same <code>IDocument</code>. * @param project the project to get the formatter preferences from, or <code>null</code> to use * the workspace settings */ public DartIndenter(IDocument document, DartHeuristicScanner scanner, IProject project) { Assert.isNotNull(document); Assert.isNotNull(scanner); fDocument = document; fScanner = scanner; fPrefs = new CorePrefs(project); } /** * Computes the indentation at <code>offset</code>. * * @param offset the offset in the document * @return a String which reflects the correct indentation for the line in which offset resides, * or <code>null</code> if it cannot be determined */ public StringBuffer computeIndentation(int offset) { return computeIndentation(offset, false); } /** * Computes the indentation at <code>offset</code>. * * @param offset the offset in the document * @param assumeOpeningBrace <code>true</code> if an opening brace should be assumed * @return a String which reflects the correct indentation for the line in which offset resides, * or <code>null</code> if it cannot be determined */ public StringBuffer computeIndentation(int offset, boolean assumeOpeningBrace) { StringBuffer reference = getReferenceIndentation(offset, assumeOpeningBrace); // handle special alignment if (fAlign != DartHeuristicScanner.NOT_FOUND) { try { // a special case has been detected. IRegion line = fDocument.getLineInformationOfOffset(fAlign); int lineOffset = line.getOffset(); return createIndent(lineOffset, fAlign, false); } catch (BadLocationException e) { return null; } } if (reference == null) { return null; } // add additional indent return createReusingIndent(reference, fIndent); } /** * Returns the reference position regarding to indentation for <code>offset</code>, or * <code>NOT_FOUND</code>. This method calls {@link #findReferencePosition(int, int) * findReferencePosition(offset, nextChar)} where <code>nextChar</code> is the next character * after <code>offset</code>. * * @param offset the offset for which the reference is computed * @return the reference statement relative to which <code>offset</code> should be indented, or * {@link DartHeuristicScanner#NOT_FOUND} */ public int findReferencePosition(int offset) { return findReferencePosition(offset, peekChar(offset)); } /** * Returns the reference position regarding to indentation for <code>position</code>, or * <code>NOT_FOUND</code>.<code>fIndent</code> will contain the relative indentation (in * indentation units, not characters) after the call. If there is a special alignment (e.g. for a * method declaration where parameters should be aligned), <code>fAlign</code> will contain the * absolute position of the alignment reference in <code>fDocument</code>, otherwise * <code>fAlign</code> is set to <code>DartHeuristicScanner.NOT_FOUND</code>. * * @param offset the offset for which the reference is computed * @param danglingElse whether a dangling else should be assumed at <code>position</code> * @param matchBrace whether the position of the matching brace should be returned instead of * doing code analysis * @param matchParen whether the position of the matching parenthesis should be returned instead * of doing code analysis * @param matchCase whether the position of a switch statement reference should be returned * (either an earlier case statement or the switch block brace) * @return the reference statement relative to which <code>position</code> should be indented, or * {@link DartHeuristicScanner#NOT_FOUND} */ public int findReferencePosition(int offset, boolean danglingElse, boolean matchBrace, boolean matchParen, boolean matchCase) { extraIndent = 0; fIndent = 0; // the indentation modification fAlign = DartHeuristicScanner.NOT_FOUND; fPosition = offset; // forward cases // an unindentation happens sometimes if the next token is special, namely // on braces, parens and case labels // align braces, but handle the case where we align with the method // declaration start instead of the opening brace. if (matchBrace) { if (skipScope(Symbols.TokenLBRACE, Symbols.TokenRBRACE)) { try { // align with the opening brace that is on a line by its own int lineOffset = fDocument.getLineOffset(fLine); if (lineOffset <= fPosition && fDocument.get(lineOffset, fPosition - lineOffset).trim().length() == 0) { return fPosition; } } catch (BadLocationException e) { // concurrent modification - walk default path } // if the opening brace is not on the start of the line, skip to the start int pos = skipToStatementStart(true, true); fIndent = 0; // indent is aligned with reference position return pos; } else { // if we can't find the matching brace, the heuristic is to unindent // by one against the normal position int pos = findReferencePosition(offset, danglingElse, false, matchParen, matchCase); fIndent--; return pos; } } // align parenthesis if (matchParen) { if (skipScope(Symbols.TokenLPAREN, Symbols.TokenRPAREN)) { // fIndent = fPrefs.prefContinuationIndent; // uncomment to indent initial ')' to continuation return fPosition; } else { // if we can't find the matching paren, the heuristic is to unindent // by one against the normal position int pos = findReferencePosition(offset, danglingElse, matchBrace, false, matchCase); fIndent--; return pos; } } // the only reliable way to get case labels aligned (due to many different // styles of using braces in a block) // is to go for another case statement, or the scope opening brace if (matchCase) { return matchCaseAlignment(); } nextToken(); switch (fToken) { case Symbols.TokenGREATERTHAN: case Symbols.TokenRBRACKET: // see below for alternative case Symbols.TokenRBRACE: // skip the block // if we can't complete the scope, reset the scan position int pos = fPosition; if (!skipScope()) { fPosition = pos; } return skipToStatementStart(danglingElse, false); case Symbols.TokenSEMICOLON: // this is the 90% case: after a statement block // the end of the previous statement / block previous.end // search to the end of the statement / block before the previous; the // token just after that is previous.start pos = fPosition; if (isForStatement()) { fIndent = fPrefs.prefContinuationIndent; return fPosition; } else { fPosition = pos; return skipToStatementStart(danglingElse, false); } // scope introduction case Symbols.TokenLPAREN: case Symbols.TokenLBRACE: case Symbols.TokenLBRACKET: return handleScopeIntroduction(offset + 1); case Symbols.TokenEOF: // trap when hitting start of document return DartHeuristicScanner.NOT_FOUND; case Symbols.TokenEQUAL: // indent assignments return handleEqual(); case Symbols.TokenDEFUN: // indent short-form functions fIndent = fPrefs.prefDefunIndent; return fPosition; case Symbols.TokenCOLON: // TODO handle ternary deep indentation // TODO handle initializers in constructor decl DartX.todo(); fIndent = fPrefs.prefCaseBlockIndent; return fPosition; case Symbols.TokenQUESTIONMARK: if (fPrefs.prefTernaryDeepAlign) { setFirstElementAlignment(fPosition, offset + 1); return fPosition; } else { fIndent = fPrefs.prefTernaryIndent; return fPosition; } // indentation for block-less introducers: case Symbols.TokenDO: case Symbols.TokenWHILE: case Symbols.TokenELSE: fIndent = fPrefs.prefSimpleIndent; return fPosition; case Symbols.TokenTRY: return skipToStatementStart(danglingElse, false); // case Symbols.TokenRBRACKET: // use this case to make initial ']' indent at continuation level // fIndent = fPrefs.prefContinuationIndent; // return fPosition; case Symbols.TokenRPAREN: int line = fLine; if (skipScope(Symbols.TokenLPAREN, Symbols.TokenRPAREN)) { int scope = fPosition; nextToken(); if (fToken == Symbols.TokenIF || fToken == Symbols.TokenWHILE || fToken == Symbols.TokenFOR) { fIndent = fPrefs.prefSimpleIndent; return fPosition; } fPosition = scope; if (looksLikeMethodDecl()) { return skipToStatementStart(danglingElse, false); } if (fToken == Symbols.TokenCATCH) { return skipToStatementStart(danglingElse, false); } fPosition = scope; if (looksLikeAnonymousTypeDecl()) { return skipToStatementStart(danglingElse, false); } } // restore fPosition = offset; fLine = line; return skipToPreviousListItemOrListStart(); case Symbols.TokenRETURN: fIndent = fPrefs.prefContinuationIndent; return fPosition; case Symbols.TokenCOMMA: // inside a list of some type // easy if there is already a list item before with its own indentation // - we just align // if not: take the start of the list ( LPAREN, LBRACE, LBRACKET ) and // either align or // indent by list-indent default: // inside whatever we don't know about: similar to the list case: // if we are inside a continued expression, then either align with a // previous line that has indentation // or indent from the expression start line (either a scope introducer // or the start of the expr). return skipToPreviousListItemOrListStart(); } } /** * Returns the reference position regarding to indentation for <code>position</code>, or * <code>NOT_FOUND</code>. * <p> * If <code>peekNextChar</code> is <code>true</code>, the next token after <code>offset</code> is * read and taken into account when computing the indentation. Currently, if the next token is the * first token on the line (i.e. only preceded by whitespace), the following tokens are specially * handled: * <ul> * <li><code>switch</code> labels are indented relative to the switch block</li> * <li>opening curly braces are aligned correctly with the introducing code</li> * <li>closing curly braces are aligned properly with the introducing code of the matching opening * brace</li> * <li>closing parenthesis' are aligned with their opening peer</li> * <li>the <code>else</code> keyword is aligned with its <code>if</code>, anything else is aligned * normally (i.e. with the base of any introducing statements).</li> * <li>if there is no token on the same line after <code>offset</code>, the indentation is the * same as for an <code>else</code> keyword</li> * </ul> * * @param offset the offset for which the reference is computed * @param nextToken the next token to assume in the document * @return the reference statement relative to which <code>offset</code> should be indented, or * {@link DartHeuristicScanner#NOT_FOUND} */ public int findReferencePosition(int offset, int nextToken) { extraIndent = 0; int cascadeIndent = 0; boolean danglingElse = false; boolean unindent = false; boolean indent = false; boolean matchBrace = false; boolean matchParen = false; boolean matchCase = false; // account for un-indentation characters already typed in, but after position // if they are on a line by themselves, the indentation gets adjusted accordingly // // also account for a dangling else if (offset < fDocument.getLength()) { try { boolean matchCascade = false; if (fScanner.isCurrentTokenCascade()) { matchCascade = true; cascadeIndent = 2; fAlign = -1; } IRegion line = fDocument.getLineInformationOfOffset(offset); int lineOffset = line.getOffset(); int prevPos = Math.max(offset - 1, 0); boolean isFirstTokenOnLine = fDocument.get(lineOffset, prevPos + 1 - lineOffset).trim().length() == 0; int prevToken = fScanner.previousToken(prevPos, DartHeuristicScanner.UNBOUND); boolean bracelessBlockStart = fScanner.isBracelessBlockStart( prevPos, DartHeuristicScanner.UNBOUND); if (fScanner.isCurrentTokenCascade() && matchCascade) { return fScanner.getPosition(); } switch (nextToken) { case Symbols.TokenELSE: danglingElse = true; break; case Symbols.TokenCASE: case Symbols.TokenDEFAULT: if (isFirstTokenOnLine) { matchCase = true; } break; case Symbols.TokenLBRACE: // for opening-brace-on-new-line style if (bracelessBlockStart && !fPrefs.prefIndentBracesForBlocks) { unindent = true; } else if ((prevToken == Symbols.TokenCOLON || prevToken == Symbols.TokenEQUAL || prevToken == Symbols.TokenRBRACKET) && !fPrefs.prefIndentBracesForArrays) { unindent = true; } else if (!bracelessBlockStart && fPrefs.prefIndentBracesForMethods) { indent = true; } break; case Symbols.TokenRBRACE: // closing braces get unindented if (isFirstTokenOnLine) { matchBrace = true; } break; case Symbols.TokenRPAREN: if (isFirstTokenOnLine) { matchParen = true; } break; case Symbols.TokenOTHER: // if (fScanner.isCurrentTokenCascade()) { // cascadeIndent = 1; // } break; } } catch (BadLocationException e) { } } else { // don't assume an else could come if we are at the end of file danglingElse = false; } int ref = findReferencePosition(offset, danglingElse, matchBrace, matchParen, matchCase); if (unindent) { fIndent--; } if (indent) { fIndent++; } extraIndent = cascadeIndent; return ref; } public String getBlockIndent() { extraIndent = 1; try { return createIndent(0, 0, false).toString(); } finally { extraIndent = 0; } } public String getCascadeIndent() { extraIndent = 2; try { return createIndent(0, 0, false).toString(); } finally { extraIndent = 0; } } /** * Computes the indentation at the reference point of <code>position</code>. * * @param offset the offset in the document * @return a String which reflects the indentation at the line in which the reference position to * <code>offset</code> resides, or <code>null</code> if it cannot be determined */ public StringBuffer getReferenceIndentation(int offset) { return getReferenceIndentation(offset, false); } /** * Computes the indentation at the reference point of <code>position</code>. * * @param offset the offset in the document * @param assumeOpeningBrace <code>true</code> if an opening brace should be assumed * @return a String which reflects the indentation at the line in which the reference position to * <code>offset</code> resides, or <code>null</code> if it cannot be determined */ public StringBuffer getReferenceIndentation(int offset, boolean assumeOpeningBrace) { int unit; if (assumeOpeningBrace) { unit = findReferencePosition(offset, Symbols.TokenLBRACE); } else { unit = findReferencePosition(offset, peekChar(offset)); } // if we were unable to find anything, return null if (unit == DartHeuristicScanner.NOT_FOUND) { return null; } return getLeadingWhitespace(unit); } // public boolean isAfterClassPrologue(int offset) { // fPosition = offset; // nextToken(); // if (fToken == Symbols.TokenLBRACE) { // while (true) { // nextToken(); // if (fToken == Symbols.TokenEOF) { // return false; // } // if (fToken == Symbols.TokenIDENT) { // nextToken(); // if (fToken == Symbols.TokenCLASS) { // return true; // } // if (fToken == Symbols.TokenEXTENDS || fToken == Symbols.TokenIMPLEMENTS // || fToken == Symbols.TokenCOMMA) { // continue; // } // return false; // } // } // } // return false; // } /** * Computes the length of a <code>CharacterSequence</code>, counting a tab character as the size * until the next tab stop and every other character as one. * * @param indent the string to measure * @return the visual length in characters */ private int computeVisualLength(CharSequence indent) { final int tabSize = fPrefs.prefTabSize; int length = 0; for (int i = 0; i < indent.length(); i++) { char ch = indent.charAt(i); switch (ch) { case '\t': if (tabSize > 0) { int reminder = length % tabSize; length += tabSize - reminder; } break; case ' ': length++; break; } } return length; } /** * Creates an indentation string of the length indent - start, consisting of the content in * <code>fDocument</code> in the range [start, indent), with every character replaced by a space * except for tabs, which are kept as such. * <p> * If <code>convertSpaceRunsToTabs</code> is <code>true</code>, every run of the number of spaces * that make up a tab are replaced by a tab character. If it is not set, no conversion takes * place, but tabs in the original range are still copied verbatim. * </p> * * @param start the start of the document region to copy the indent from * @param indent the exclusive end of the document region to copy the indent from * @param convertSpaceRunsToTabs whether to convert consecutive runs of spaces to tabs * @return the indentation corresponding to the document content specified by <code>start</code> * and <code>indent</code> */ private StringBuffer createIndent(int start, final int indent, final boolean convertSpaceRunsToTabs) { final boolean convertTabs = fPrefs.prefUseTabs && convertSpaceRunsToTabs; final int tabLen = fPrefs.prefTabSize; final StringBuffer ret = new StringBuffer(); try { int spaces = extraIndent * tabLen; while (start < indent) { char ch = fDocument.getChar(start); if (ch == '\t') { ret.append('\t'); spaces = 0; } else if (convertTabs) { spaces++; if (spaces == tabLen) { ret.append('\t'); spaces = 0; } } else { ret.append(' '); } start++; } // remainder while (spaces-- > 0) { ret.append(' '); } } catch (BadLocationException e) { } return ret; } /** * Creates a string with a visual length of the given <code>indentationSize</code>. * * @param buffer the original indent to reuse if possible * @param additional the additional indentation units to add or subtract to reference * @return the modified <code>buffer</code> reflecting the indentation adapted to * <code>additional</code> */ private StringBuffer createReusingIndent(StringBuffer buffer, int additional) { int refLength = computeVisualLength(buffer); int addLength = fPrefs.prefIndentationSize * additional; // may be < 0 int totalLength = Math.max(0, refLength + addLength); // copy the reference indentation for the indent up to the last tab // stop within the maxCopy area int minLength = Math.min(totalLength, refLength); int tabSize = fPrefs.prefTabSize; int maxCopyLength = tabSize > 0 ? minLength - minLength % tabSize : minLength; // maximum indent to copy stripExceedingChars(buffer, maxCopyLength); // add additional indent int missing = totalLength - maxCopyLength; final int tabs, spaces; if (!DartToolsPlugin.getDefault().getPreferenceStore().getBoolean( AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SPACES_FOR_TABS)) { tabs = tabSize > 0 ? missing / tabSize : 0; spaces = tabSize > 0 ? missing % tabSize : missing; } else if (JavaScriptCore.SPACE.equals(fPrefs.prefTabChar)) { tabs = 0; spaces = missing; } else if (JavaScriptCore.TAB.equals(fPrefs.prefTabChar)) { tabs = tabSize > 0 ? missing / tabSize : 0; spaces = tabSize > 0 ? missing % tabSize : missing; } else if (DefaultCodeFormatterConstants.MIXED.equals(fPrefs.prefTabChar)) { tabs = tabSize > 0 ? missing / tabSize : 0; spaces = tabSize > 0 ? missing % tabSize : missing; } else { Assert.isTrue(false); return null; } for (int i = 0; i < tabs; i++) { buffer.append('\t'); } for (int i = 0; i < spaces; i++) { buffer.append(' '); } return buffer; } private int getBlockIndent(boolean isMethodBody, boolean isTypeBody) { if (isTypeBody) { return fPrefs.prefTypeIndent + (fPrefs.prefIndentBracesForTypes ? 1 : 0); } else if (isMethodBody) { return fPrefs.prefMethodBodyIndent + (fPrefs.prefIndentBracesForMethods ? 1 : 0); } else { return fIndent; } } /** * Returns the indentation of the line at <code>offset</code> as a <code>StringBuffer</code>. If * the offset is not valid, the empty string is returned. * * @param offset the offset in the document * @return the indentation (leading whitespace) of the line in which <code>offset</code> is * located */ private StringBuffer getLeadingWhitespace(int offset) { StringBuffer indent = new StringBuffer(); try { IRegion line = fDocument.getLineInformationOfOffset(offset); int lineOffset = line.getOffset(); int nonWS = fScanner.findNonWhitespaceForwardInAnyPartition( lineOffset, lineOffset + line.getLength()); indent.append(fDocument.get(lineOffset, nonWS - lineOffset)); return indent; } catch (BadLocationException e) { return indent; } } /** * Returns the contents of the current token. * * @return the contents of the current token */ private CharSequence getTokenContent() { return new DocumentCharacterIterator(fDocument, fPosition, fPreviousPos); } /** * Checks if the statement at position is itself a continuation of the previous, else sets the * indentation to Continuation Indent. * * @return the position of the token */ private int handleEqual() { try { // If this line is itself continuation of the previous then do nothing IRegion line = fDocument.getLineInformationOfOffset(fPosition); int nonWS = fScanner.findNonWhitespaceBackward(line.getOffset(), DartHeuristicScanner.UNBOUND); if (nonWS != Symbols.TokenEOF) { int tokenAtPreviousLine = fScanner.nextToken(nonWS, nonWS + 1); if (tokenAtPreviousLine != Symbols.TokenSEMICOLON && tokenAtPreviousLine != Symbols.TokenRBRACE && tokenAtPreviousLine != Symbols.TokenLBRACE && tokenAtPreviousLine != Symbols.TokenEOF) { return fPosition; } } } catch (BadLocationException e) { return fPosition; } fIndent = fPrefs.prefContinuationIndent; return fPosition; } /** * Handles the introduction of a new scope. The current token must be one out of * <code>Symbols.TokenLPAREN</code>, <code>Symbols.TokenLBRACE</code>, and * <code>Symbols.TokenLBRACKET</code>. Returns as the reference position either the token * introducing the scope or - if available - the first token after that. * <p> * Depending on the type of scope introduction, the indentation will align (deep indenting) with * the reference position (<code>fAlign</code> will be set to the reference position) or * <code>fIndent</code> will be set to the number of indentation units. * </p> * * @param bound the bound for the search for the first token after the scope introduction. * @return the indent */ private int handleScopeIntroduction(int bound) { switch (fToken) { // scope introduction case Symbols.TokenLPAREN: int pos = fPosition; // store // special: method declaration deep indentation if (looksLikeMethodDecl() || looksLikeConstructorDecl()) { if (fPrefs.prefMethodDeclDeepIndent) { return setFirstElementAlignment(pos, bound); } else { fIndent = fPrefs.prefMethodDeclIndent; return pos; } } else { fPosition = pos; if (looksLikeMethodCall()) { if (fPrefs.prefMethodCallDeepIndent) { return setFirstElementAlignment(pos, bound); } else { fIndent = fPrefs.prefMethodCallIndent; return pos; } } else if (fPrefs.prefParenthesisDeepIndent) { return setFirstElementAlignment(pos, bound); } } // normal: return the parenthesis as reference fIndent = fPrefs.prefParenthesisIndent; return pos; case Symbols.TokenLBRACE: pos = fPosition; // store // special: map literal {} in list literal or argument list { nextToken(); if (fToken == Symbols.TokenCOMMA) { fIndent = fPrefs.prefBlockIndent; return pos; } fPosition = pos; } // special: array initializer if (looksLikeArrayInitializerIntro()) { fPosition = pos; fIndent = fPrefs.prefBlockIndent; } else { fIndent = fPrefs.prefBlockIndent; } // normal: skip to the statement start before the scope introducer // opening braces are often on differently ending indents than e.g. a // method definition if (looksLikeArrayInitializerIntro() && !fPrefs.prefIndentBracesForArrays || !fPrefs.prefIndentBracesForBlocks) { fPosition = pos; // restore return skipToStatementStart(true, true); // set to true to match the // first if } else { return pos; } case Symbols.TokenLBRACKET: pos = fPosition; // store fIndent = fPrefs.prefBlockIndent; return pos; // restore default: Assert.isTrue(false); return -1; // dummy } } /** * while(condition); is ambiguous when parsed backwardly, as it is a valid statement by its own, * so we have to check whether there is a matching do. A <code>do</code> can either be separated * from the while by a block, or by a single statement, which limits our search distance. * * @return <code>true</code> if the <code>while</code> currently in <code>fToken</code> has a * matching <code>do</code>. */ private boolean hasMatchingDo() { Assert.isTrue(fToken == Symbols.TokenWHILE); nextToken(); switch (fToken) { case Symbols.TokenRBRACE: skipScope(); // and fall thru //$FALL-THROUGH$ case Symbols.TokenSEMICOLON: skipToStatementStart(false, false); return fToken == Symbols.TokenDO; } return false; } private boolean isCascadeLine(int offset) { try { IRegion line = fDocument.getLineInformationOfOffset(offset); String lineStr = fDocument.get(line.getOffset(), line.getLength()); lineStr = lineStr.trim(); return lineStr.startsWith(".."); } catch (BadLocationException e) { return false; } } /** * Returns true if the colon at the current position is part of a conditional (ternary) * expression, false otherwise. * * @return true if the colon at the current position is part of a conditional */ private boolean isConditional() { while (true) { nextToken(); switch (fToken) { // search for case labels, which consist of (possibly qualified) // identifiers or numbers case Symbols.TokenIDENT: case Symbols.TokenOTHER: // dots for qualified constants continue; case Symbols.TokenCASE: case Symbols.TokenDEFAULT: return false; default: return true; } } } /** * Checks if the semicolon at the current position is part of a for statement. * * @return returns <code>true</code> if current position is part of for statement */ private boolean isForStatement() { int semiColonCount = 1; while (true) { nextToken(); switch (fToken) { case Symbols.TokenFOR: return true; case Symbols.TokenIN: return false; case Symbols.TokenLBRACE: return false; case Symbols.TokenSEMICOLON: semiColonCount++; if (semiColonCount > 2) { return false; } break; case Symbols.TokenEOF: return false; } } } /** * Returns <code>true</code> if the current tokens look like an anonymous type declaration header * (i.e. a type name (potentially qualified) and a new keyword). The heuristic calls * <code>nextToken</code> and expects a possibly qualified identifier (type name) and a new * keyword * * @return <code>true</code> if the current position looks like a anonymous type declaration * header. */ private boolean looksLikeAnonymousTypeDecl() { nextToken(); if (fToken == Symbols.TokenIDENT) { // type name nextToken(); while (fToken == Symbols.TokenOTHER) { // dot of qualification nextToken(); if (fToken != Symbols.TokenIDENT) { return false; } nextToken(); } return fToken == Symbols.TokenNEW; } return false; } private boolean looksLikeArgumentListOpen() { int fPosition_ = fPosition; int fPreviousPos_ = fPreviousPos; try { if (fToken == Symbols.TokenLPAREN) { nextToken(); if (fToken == Symbols.TokenIDENT) { // OK, probably closure arguments fPosition_ = fPosition; fPreviousPos_ = fPreviousPos; return true; } } return false; } finally { fPosition = fPosition_; fPreviousPos = fPreviousPos_; } } /** * Returns <code>true</code> if the next token received after calling <code>nextToken</code> is * either an equal sign or an array designator ('[]'). * * @return <code>true</code> if the next elements look like the start of an array definition */ private boolean looksLikeArrayInitializerIntro() { nextToken(); if (fToken == Symbols.TokenEQUAL || skipBrackets()) { return true; } return false; } private boolean looksLikeConstructorDecl() { int p = fPreviousPos; nextToken(); if (fToken != Symbols.TokenIDENT) { return false; } // check for dot and ident to make named constructor int pos = fPreviousPos; nextToken(); if (fToken == Symbols.TokenOTHER) { // dot pos = fPreviousPos; nextToken(); if (fToken == Symbols.TokenIDENT) { pos = fPreviousPos; nextToken(); } else { fPosition = p; return false; } } // check for possible modifiers if (fToken == Symbols.TokenCONST || fToken == Symbols.TokenFACTORY) { pos = fPreviousPos; nextToken(); if (fToken == Symbols.TokenCONST || fToken == Symbols.TokenFACTORY) { pos = fPreviousPos; nextToken(); } } fPosition = fPreviousPos; // backup one token fPreviousPos = pos; try { fLine = fDocument.getLineOfOffset(fPosition); } catch (BadLocationException e) { fLine = -1; } pos = fPosition; int prevPos = fPreviousPos; int line = fLine; // scan forward from p to verify formal param list; p is just to the right of the open paren int end = fScanner.findClosingPeer(p, '(', ')'); if (end == DartHeuristicScanner.NOT_FOUND) { return false; } try { fPosition = end + 1; fPreviousPos = end; boolean looksLikeParams = looksLikeFormalParamList(); return looksLikeParams; } finally { fPosition = pos; fPreviousPos = prevPos; fLine = line; } } private boolean looksLikeFormalParamList() { // ignores embedded comments // this may be too restrictive; might need to relax to skipToPreviousListItemOrListStart() nextToken(); if (fToken != Symbols.TokenRPAREN) { return false; } int angleBracketCount = 0, parenCount = 1, braceCount = 0, bracketCount = 0; while (true) { if (angleBracketCount < 0 || braceCount < 0 || bracketCount < 0) { return false; // mismatched bracket } nextToken(); switch (fToken) { // right-side (opening) group symbols case Symbols.TokenRBRACE: braceCount++; continue; case Symbols.TokenRBRACKET: bracketCount++; continue; case Symbols.TokenGREATERTHAN: angleBracketCount++; continue; case Symbols.TokenRPAREN: parenCount++; continue; case Symbols.TokenIDENT: case Symbols.TokenVAR: // illegal but common case Symbols.TokenVOID: // illegal but not uncommon case Symbols.TokenOTHER: // period case Symbols.TokenCOMMA: // param separator case Symbols.TokenEQUAL: // optional params case Symbols.TokenCOLON: // named params case Symbols.TokenTHIS: case Symbols.TokenTRUE: case Symbols.TokenFALSE: case Symbols.TokenNULL: case Symbols.TokenAT: // not handling complex metadata args continue; // left-side (closing) group symbols case Symbols.TokenLBRACE: braceCount--; continue; case Symbols.TokenLBRACKET: bracketCount--; continue; case Symbols.TokenLESSTHAN: angleBracketCount--; continue; case Symbols.TokenLPAREN: parenCount--; if (parenCount == 0) { return true; // exit on initial paren } else { continue; } // return false for anything not permitted in a formal parameter list default: return false; } } } /** * Returns <code>true</code> if the current tokens look like a method call header (i.e. an * identifier as opposed to a keyword taking parenthesized parameters such as <code>if</code>). * <p> * The heuristic calls <code>nextToken</code> and expects an identifier (method name). * * @return <code>true</code> if the current position looks like a method call header. */ private boolean looksLikeMethodCall() { // TODO add awareness for constructor calls with generic types: new List<String>() // TODO recognize function calls DartX.todo(); nextToken(); return fToken == Symbols.TokenIDENT; // method name } /** * Returns <code>true</code> if the current tokens look like a method declaration header (i.e. * only the return type and method name). The heuristic calls <code>nextToken</code> and expects * an identifier (method name) and a type declaration (an identifier with optional brackets) which * also covers the visibility modifier of constructors. * * @return <code>true</code> if the current position looks like a method declaration header. */ private boolean looksLikeMethodDecl() { /* * Note: This heuristic relies on indentation to recognize untyped method decls (including * constructors). Untyped methods defined at deep indentation levels will not be recognized. */ nextToken(); if (fToken == Symbols.TokenIDENT) { // method name int pos = fPosition; do { nextToken(); } while (skipBrackets()); // optional brackets for array valued return types switch (fToken) { case Symbols.TokenIDENT: case Symbols.TokenGET: case Symbols.TokenSET: case Symbols.TokenOPERATOR: case Symbols.TokenVOID: case Symbols.TokenVAR: // common mistake case Symbols.TokenEOF: return true; case Symbols.TokenRBRACE: // assume that whatever follows a close brace at 0 or 1 indent levels is a method decl StringBuffer buf = getLeadingWhitespace(pos); if (buf.length() == 0 || buf.length() == fPrefs.prefTabSize) { return true; } } } return false; } /** * Returns as a reference any previous <code>switch</code> labels ( <code>case</code> or * <code>default</code>) or the offset of the brace that scopes the switch statement. Sets * <code>fIndent</code> to <code>prefCaseIndent</code> upon a match. * * @return the reference offset for a <code>switch</code> label */ private int matchCaseAlignment() { while (true) { nextToken(); switch (fToken) { // invalid cases: another case label or an LBRACE must come before a case // -> bail out with the current position case Symbols.TokenLPAREN: case Symbols.TokenLBRACKET: case Symbols.TokenEOF: return fPosition; case Symbols.TokenLBRACE: // opening brace of switch statement fIndent = fPrefs.prefCaseIndent; return fPosition; case Symbols.TokenCASE: case Symbols.TokenDEFAULT: // align with previous label fIndent = 0; return fPosition; // scopes: skip them case Symbols.TokenRPAREN: case Symbols.TokenRBRACKET: case Symbols.TokenRBRACE: case Symbols.TokenGREATERTHAN: skipScope(); break; default: // keep searching continue; } } } /** * Reads the next token in backward direction from the heuristic scanner and sets the fields * <code>fToken, fPreviousPosition</code> and <code>fPosition</code> accordingly. */ private void nextToken() { nextToken(fPosition); } /** * Reads the next token in backward direction of <code>start</code> from the heuristic scanner and * sets the fields <code>fToken, fPreviousPosition</code> and <code>fPosition</code> accordingly. * * @param start the start offset from which to scan backwards */ private void nextToken(int start) { fToken = fScanner.previousToken(start - 1, DartHeuristicScanner.UNBOUND); fPreviousPos = start; fPosition = fScanner.getPosition() + 1; try { fLine = fDocument.getLineOfOffset(fPosition); } catch (BadLocationException e) { fLine = -1; } } /** * Peeks the next char in the document that comes after <code>offset</code> on the same line as * <code>offset</code>. * * @param offset the offset into document * @return the token symbol of the next element, or TokenEOF if there is none */ private int peekChar(int offset) { if (offset < fDocument.getLength()) { try { IRegion line = fDocument.getLineInformationOfOffset(offset); int lineOffset = line.getOffset(); int next = fScanner.nextToken(offset, lineOffset + line.getLength()); return next; } catch (BadLocationException e) { } } return Symbols.TokenEOF; } /** * Sets the deep indent offset (<code>fAlign</code>) to either the offset right after * <code>scopeIntroducerOffset</code> or - if available - the first token after * <code>scopeIntroducerOffset</code>, but before <code>bound</code>. * * @param scopeIntroducerOffset the offset of the scope introducer * @param bound the bound for the search for another element * @return the reference position */ private int setFirstElementAlignment(int scopeIntroducerOffset, int bound) { int firstPossible = scopeIntroducerOffset + 1; // align with the first position after the scope intro fAlign = fScanner.findNonWhitespaceForwardInAnyPartition(firstPossible, bound); if (fAlign == DartHeuristicScanner.NOT_FOUND) { fAlign = firstPossible; } return fAlign; } /** * Skips brackets if the current token is a RBRACKET. There can be nothing but whitespace in * between, this is only to be used for <code>[]</code> elements. * * @return <code>true</code> if a <code>[]</code> could be scanned, the current token is left at * the LBRACKET. */ private boolean skipBrackets() { if (fToken == Symbols.TokenRBRACKET) { nextToken(); if (fToken == Symbols.TokenLBRACKET) { return true; } } return false; } /** * Skips over the next <code>if</code> keyword. The current token when calling this method must be * an <code>else</code> keyword. Returns <code>true</code> if a matching <code>if</code> could be * found, <code>false</code> otherwise. The cursor (<code>fPosition</code>) is set to the offset * of the <code>if</code> token. * * @return <code>true</code> if a matching <code>if</code> token was found, <code>false</code> * otherwise */ private boolean skipNextIF() { Assert.isTrue(fToken == Symbols.TokenELSE); while (true) { nextToken(); switch (fToken) { // scopes: skip them case Symbols.TokenRPAREN: case Symbols.TokenRBRACKET: case Symbols.TokenRBRACE: case Symbols.TokenGREATERTHAN: skipScope(); break; case Symbols.TokenIF: // found it, return return true; case Symbols.TokenELSE: // recursively skip else-if blocks skipNextIF(); break; // shortcut scope starts case Symbols.TokenLPAREN: case Symbols.TokenLBRACE: case Symbols.TokenLBRACKET: case Symbols.TokenEOF: return false; } } } /** * Skips a scope and positions the cursor (<code>fPosition</code>) on the token that opens the * scope. Returns <code>true</code> if a matching peer could be found, <code>false</code> * otherwise. The current token when calling must be one out of <code>Symbols.TokenRPAREN</code>, * <code>Symbols.TokenRBRACE</code>, and <code>Symbols.TokenRBRACKET</code>. * * @return <code>true</code> if a matching peer was found, <code>false</code> otherwise */ private boolean skipScope() { switch (fToken) { case Symbols.TokenRPAREN: return skipScope(Symbols.TokenLPAREN, Symbols.TokenRPAREN); case Symbols.TokenRBRACKET: return skipScope(Symbols.TokenLBRACKET, Symbols.TokenRBRACKET); case Symbols.TokenRBRACE: return skipScope(Symbols.TokenLBRACE, Symbols.TokenRBRACE); case Symbols.TokenGREATERTHAN: if (!fPrefs.prefHasGenerics) { return false; } int storedPosition = fPosition; int storedToken = fToken; nextToken(); switch (fToken) { case Symbols.TokenIDENT: if (!DartHeuristicScanner.isGenericStarter(getTokenContent())) { break; } //$FALL-THROUGH$ case Symbols.TokenQUESTIONMARK: case Symbols.TokenGREATERTHAN: if (skipScope(Symbols.TokenLESSTHAN, Symbols.TokenGREATERTHAN)) { return true; } } // <> are harder to detect - restore the position if we fail fPosition = storedPosition; fToken = storedToken; return false; default: Assert.isTrue(false); return false; } } /** * Scans tokens for the matching opening peer. The internal cursor ( <code>fPosition</code>) is * set to the offset of the opening peer if found. * * @param openToken the opening peer token * @param closeToken the closing peer token * @return <code>true</code> if a matching token was found, <code>false</code> otherwise */ private boolean skipScope(int openToken, int closeToken) { int depth = 1; while (true) { nextToken(); if (fToken == closeToken) { depth++; } else if (fToken == openToken) { depth--; if (depth == 0) { return true; } } else if (fToken == Symbols.TokenEOF) { return false; } } } /** * Returns the reference position for a list element. The algorithm tries to match any previous * indentation on the same list. If there is none, the reference position returned is determined * depending on the type of list: The indentation will either match the list scope introducer * (e.g. for method declarations), so called deep indents, or simply increase the indentation by a * number of standard indents. See also {@link #handleScopeIntroduction(int)}. * * @return the reference position for a list item: either a previous list item that has its own * indentation, or the list introduction start. */ private int skipToPreviousListItemOrListStart() { int startLine = fLine; int startPosition = fPosition; while (true) { nextToken(); // if any line item comes with its own indentation, adapt to it if (fLine < startLine) { try { int lineOffset = fDocument.getLineOffset(startLine); int bound = Math.min(fDocument.getLength(), startPosition + 1); fAlign = fScanner.findNonWhitespaceForwardInAnyPartition(lineOffset, bound); } catch (BadLocationException e) { // ignore and return just the position } return startPosition; } switch (fToken) { // scopes: skip them case Symbols.TokenRPAREN: case Symbols.TokenRBRACKET: case Symbols.TokenRBRACE: case Symbols.TokenGREATERTHAN: skipScope(); break; // scope introduction case Symbols.TokenLPAREN: case Symbols.TokenLBRACE: case Symbols.TokenLBRACKET: return handleScopeIntroduction(startPosition + 1); case Symbols.TokenSEMICOLON: int savedPosition = fPosition; if (isForStatement()) { fIndent = fPrefs.prefContinuationIndent; } else { fPosition = savedPosition; } return fPosition; case Symbols.TokenQUESTIONMARK: if (fPrefs.prefTernaryDeepAlign) { setFirstElementAlignment(fPosition - 1, fPosition + 1); return fPosition; } else { fIndent = fPrefs.prefTernaryIndent; return fPosition; } case Symbols.TokenRETURN: fIndent = fPrefs.prefContinuationIndent; return fPosition; case Symbols.TokenEQUAL: return handleEqual(); case Symbols.TokenEOF: return 0; } } } /** * Skips to the start of a statement that ends at the current position. * * @param danglingElse whether to indent aligned with the last <code>if</code> * @param isInBlock whether the current position is inside a block, which limits the search scope * to the next scope introducer * @return the reference offset of the start of the statement */ private int skipToStatementStart(boolean danglingElse, boolean isInBlock) { final int NOTHING = 0; final int READ_PARENS = 1; final int READ_IDENT = 2; int mayBeMethodBody = NOTHING; boolean isTypeBody = false; int prevToken = -1; while (true) { nextToken(); if (isInBlock) { switch (fToken) { // exit on all block introducers case Symbols.TokenIF: case Symbols.TokenELSE: case Symbols.TokenCATCH: case Symbols.TokenDO: case Symbols.TokenWHILE: case Symbols.TokenFINALLY: case Symbols.TokenFOR: case Symbols.TokenTRY: return fPosition; case Symbols.TokenCOLON: // recognize constructor; else return pos int pos = fPosition; if (!looksLikeFormalParamList()) { return pos; } if (!looksLikeConstructorDecl()) { return pos; } return fPosition; case Symbols.TokenSTATIC: mayBeMethodBody = READ_IDENT; // treat static blocks like methods break; case Symbols.TokenCLASS: isTypeBody = true; break; case Symbols.TokenSWITCH: fIndent = fPrefs.prefCaseIndent; return fPosition; } } switch (fToken) { // scope introduction through: LPAREN, LBRACE, LBRACKET // search stop on SEMICOLON, RBRACE, COLON, EOF // -> the next token is the start of the statement (i.e. previousPos when backward scanning) case Symbols.TokenLPAREN: case Symbols.TokenLBRACE: case Symbols.TokenLBRACKET: case Symbols.TokenSEMICOLON: case Symbols.TokenEOF: if (isInBlock) { fIndent = getBlockIndent(mayBeMethodBody == READ_IDENT, isTypeBody); } if (looksLikeArgumentListOpen()) { break; } // else: fIndent set by previous calls return fPreviousPos; case Symbols.TokenCOLON: int prevPos = fPreviousPos; int pos = fPosition; if (!isConditional()) { return prevPos; } // conditionals and constructors are hard to tell apart so look harder for constructor fPosition = pos; fPreviousPos = prevPos; if (looksLikeFormalParamList()) { if (looksLikeConstructorDecl()) { return fPosition; } } fPosition = pos; fPreviousPos = prevPos; break; case Symbols.TokenRBRACE: // RBRACE is a little tricky: it can be the end of a map definition, but // usually it is the end of a previous block pos = fPreviousPos; // store state if (skipScope() && looksLikeArrayInitializerIntro()) { continue; // it's a map } else { if (isInBlock) { fIndent = getBlockIndent(mayBeMethodBody == READ_IDENT, isTypeBody); } return pos; // it's not - do as with all the above } // scopes: skip them case Symbols.TokenRPAREN: if (isInBlock) { mayBeMethodBody = READ_PARENS; } //$FALL-THROUGH$ case Symbols.TokenRBRACKET: case Symbols.TokenGREATERTHAN: if (prevToken == Symbols.TokenEQUAL && fToken == Symbols.TokenGREATERTHAN) { break; } pos = fPreviousPos; if (skipScope()) { break; } else { return pos; } // IF / ELSE: align the position after the conditional block with the if // so we are ready for an else, except if danglingElse is false // in order for this to work, we must skip an else to its if case Symbols.TokenIF: if (danglingElse) { return fPosition; } else { break; } case Symbols.TokenELSE: // skip behind the next if, as we have that one covered pos = fPosition; if (skipNextIF()) { break; } else { return pos; } case Symbols.TokenDO: // align the WHILE position with its do return fPosition; case Symbols.TokenWHILE: // this one is tricky: while can be the start of a while loop // or the end of a do - while pos = fPosition; if (hasMatchingDo()) { // continue searching from the DO on break; } else { // continue searching from the WHILE on fPosition = pos; break; } case Symbols.TokenIDENT: if (mayBeMethodBody == READ_PARENS) { mayBeMethodBody = READ_IDENT; } break; default: // keep searching } prevToken = fToken; } } /** * Strips any characters off the end of <code>reference</code> that exceed * <code>indentLength</code>. * * @param reference the string to measure * @param indentLength the maximum visual indentation length * @return the stripped <code>reference</code> */ private StringBuffer stripExceedingChars(StringBuffer reference, int indentLength) { final int tabSize = fPrefs.prefTabSize; int measured = 0; int chars = reference.length(); int i = 0; for (; measured < indentLength && i < chars; i++) { char ch = reference.charAt(i); switch (ch) { case '\t': if (tabSize > 0) { int reminder = measured % tabSize; measured += tabSize - reminder; } break; case ' ': measured++; break; } } int deleteFrom = measured > indentLength ? i - 1 : i; return reference.delete(deleteFrom, chars); } }