| /******************************************************************************* |
| * Copyright (c) 2009, 2017 Red Hat 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: |
| * Alexander Kurtakov - initial API and implementation |
| * Mat Booth |
| *******************************************************************************/ |
| package org.eclipse.dltk.sh.internal.ui.text; |
| |
| import org.eclipse.dltk.sh.internal.ui.Activator; |
| import org.eclipse.dltk.ui.CodeFormatterConstants; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DocumentCommand; |
| import org.eclipse.jface.text.IAutoEditStrategy; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jface.text.rules.IRule; |
| import org.eclipse.jface.text.rules.IToken; |
| |
| /** |
| * An indent strategy capable of indenting and unindenting on any set of words, |
| * depending on the rules that are set. |
| * |
| * @see #setRules(IRule[]) |
| */ |
| public class ScriptAutoIndentStrategy implements IAutoEditStrategy { |
| /** |
| * Document scanner used to identify indentations. |
| */ |
| private final DocumentAndCommandScanner scanner = new DocumentAndCommandScanner(); |
| |
| /** |
| * Set the rules that will be used in a document scanner to identify where |
| * indentations should occur. Typically you'd have one rule to describe each |
| * type of indentation. |
| * |
| * @param rules |
| * the list of rules |
| * @see IndentType |
| */ |
| public void setRules(IRule[] rules) { |
| scanner.setRules(rules); |
| } |
| |
| /** |
| * This implementation attempts to auto-indent and auto-unindent after keywords |
| * that require it. |
| */ |
| @Override |
| public void customizeDocumentCommand(IDocument d, DocumentCommand c) { |
| int delim = TextUtilities.endsWith(d.getLegalLineDelimiters(), c.text); |
| if ((c.length == 0) && (c.text != null) && (delim != -1)) { |
| smartIndentAfterNewLine(d, c); |
| } else if ((c.text.length() == 1) && !c.text.equals("#")) { |
| smartIndentAfterKeypress(d, c); |
| } |
| } |
| |
| /** |
| * Set the indent of a new line when the user hits carriage return. The new |
| * indent will either be the same as the previous line or incremented if the |
| * user has hit carriage return on a line that contains a incrementing keyword. |
| * |
| * @param document |
| * the document being parsed |
| * @param c |
| * the command being performed |
| */ |
| protected void smartIndentAfterNewLine(IDocument document, DocumentCommand c) { |
| if ((c.offset == -1) || (document.getLength() == 0)) { |
| return; |
| } |
| try { |
| StringBuilder buf = new StringBuilder(c.text); |
| int p = c.offset == document.getLength() ? c.offset - 1 : c.offset; |
| int line = document.getLineOfOffset(p); |
| int start = document.getLineOffset(line); |
| int bracketCount = getBracketCount(document, null, start, c.offset, true); |
| buf.append(generateIndentation(getIndentOfLine(document, line), bracketCount <= 0 ? 0 : 1)); |
| c.text = buf.toString(); |
| } catch (BadLocationException x) { |
| // ignore |
| } |
| } |
| |
| /** |
| * Set the indent of the current line when the user hits a key. The indent will |
| * either be unchanged or decremented if the user types |
| * |
| * @param document |
| * the document being parsed |
| * @param c |
| * the command being performed |
| */ |
| protected void smartIndentAfterKeypress(IDocument document, DocumentCommand c) { |
| if ((c.offset == -1) || (document.getLength() == 0) || c.text.trim().isEmpty()) { |
| return; |
| } |
| try { |
| StringBuilder buf = new StringBuilder(); |
| int p = c.offset == document.getLength() ? c.offset - 1 : c.offset; |
| int line = document.getLineOfOffset(p); |
| int start = document.getLineOffset(line); |
| int whiteEnd = findEndOfWhiteSpace(document, start, c.offset); |
| |
| int bracketCount = getBracketCount(document, c, start, c.offset, false); |
| buf.append(generateIndentation(getIndentOfLine(document, line), bracketCount >= 0 ? 0 : -1)); |
| buf.append(document.get(whiteEnd, c.offset - whiteEnd)); |
| buf.append(c.text); |
| // Alter the command |
| c.length = (c.offset - start) + c.length; |
| c.offset = start; |
| c.text = buf.toString(); |
| } catch (BadLocationException x) { |
| // ignore |
| } |
| } |
| |
| /** |
| * Returns the first offset greater than <code>offset</code> and smaller than |
| * <code>end</code> whose character is not a space or tab character. If no such |
| * offset is found, <code>end</code> is returned. |
| * |
| * @param document |
| * the document to search in |
| * @param offset |
| * the offset at which searching start |
| * @param end |
| * the offset at which searching stops |
| * @return the offset in the specified range whose character is not a space or |
| * tab |
| * @exception BadLocationException |
| * if offset is an invalid position in the given document |
| */ |
| private static int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException { |
| while (offset < end) { |
| char c = document.getChar(offset); |
| if ((c != ' ') && (c != '\t')) { |
| return offset; |
| } |
| offset++; |
| } |
| return end; |
| } |
| |
| /** |
| * Returns the indentation of the specified line in <code>document</code>. |
| * |
| * @param document |
| * - the document being parsed |
| * @param line |
| * - the line number being searched |
| * @return the string containing the indentation from the specified line |
| */ |
| private static String getIndentOfLine(IDocument document, int line) throws BadLocationException { |
| if (line > -1) { |
| int start = document.getLineOffset(line); |
| int end = start + document.getLineLength(line); |
| int whiteend = findEndOfWhiteSpace(document, start, end); |
| return document.get(start, whiteend - start); |
| } else { |
| return ""; |
| } |
| } |
| |
| /** |
| * Returns the bracket count of a section of text. The count is incremented when |
| * an opening bracket is encountered and decremented when a closing bracket is |
| * encountered. |
| * |
| * @param document |
| * - the document being parsed |
| * @param command |
| * - if not null, the inserted text specified by the command will be |
| * taken into account as if it were part of the document |
| * @param start |
| * - the start position for the search |
| * @param end |
| * - the end position for the search |
| * @param ignoreInflexions |
| * - whether or not to ignore inflexions in the count |
| * @return the resulting bracket count, a positive value means we've encountered |
| * more opening than closing brackets |
| */ |
| private int getBracketCount(IDocument document, DocumentCommand command, int start, int end, |
| boolean ignoreInflexions) { |
| int bracketcount = 0; |
| if (command != null) { |
| scanner.setRange(document, command, start, end - start); |
| } else { |
| scanner.setRange(document, start, end - start); |
| } |
| |
| while (true) { |
| IToken token = scanner.nextToken(); |
| if (token.isEOF()) { |
| break; |
| } |
| |
| if (token.isOther()) { |
| IndentType type = (IndentType) token.getData(); |
| if (type == IndentType.INCREMENT) { |
| ++bracketcount; |
| } else if (type == IndentType.DECREMENT) { |
| --bracketcount; |
| } else if ((type == IndentType.INFLEXION) && ignoreInflexions) { |
| ++bracketcount; |
| } else if ((type == IndentType.INFLEXION) && !ignoreInflexions) { |
| --bracketcount; |
| } |
| } |
| } |
| return bracketcount; |
| } |
| |
| /** |
| * Calculate the indentation needed for a new line based on the contents of the |
| * previous line. |
| * |
| * @param previous |
| * a string containing the indentation of the previous line |
| * @param additional |
| * number of desired addition indentations, may be negative |
| * @return a string containing the indentation to use on the new line |
| */ |
| private static String generateIndentation(String previous, int additional) { |
| // Get the indentation preferences |
| IPreferenceStore prefs = Activator.getDefault().getPreferenceStore(); |
| String tabChar = prefs.getString(CodeFormatterConstants.FORMATTER_TAB_CHAR); |
| int indentSize = prefs.getInt(CodeFormatterConstants.FORMATTER_INDENTATION_SIZE); |
| int tabSize = prefs.getInt(CodeFormatterConstants.FORMATTER_TAB_SIZE); |
| |
| // Size in characters of the indentation of the previous line |
| int preLength = computeVisualLength(previous, tabSize); |
| |
| // Number of addition characters needed |
| int addLength = indentSize * additional; |
| |
| // Target size of the indentation for the new line |
| int endLength = Math.max(0, preLength + addLength); |
| |
| // Trim previous indentation back to nearest tab stop |
| int minLength = Math.min(endLength, preLength); |
| // indent |
| // to |
| // copy |
| String indent = stripExtraChars(previous, minLength, tabSize); |
| |
| // Add additional indentation |
| int missing = endLength - minLength; |
| final int tabs, spaces; |
| if (CodeFormatterConstants.SPACE.equals(tabChar)) { |
| // Each indent is a number of spaces equal to indent size |
| tabs = 0; |
| spaces = missing; |
| } else if (CodeFormatterConstants.TAB.equals(tabChar)) { |
| // Missing should always be in multiples of indent size, so this |
| // means "one tab per indent" and indent size is essentially ignored |
| tabs = missing / indentSize; |
| spaces = 0; |
| } else if (CodeFormatterConstants.MIXED.equals(tabChar)) { |
| // If the missing indent is a multiple of tab size then tabs will be |
| // used, otherwise use spaces |
| tabs = tabSize > 0 ? missing / tabSize : 0; |
| spaces = tabSize > 0 ? missing % tabSize : missing; |
| } else { |
| tabs = 0; |
| spaces = 0; |
| } |
| for (int i = 0; i < tabs; i++) { |
| indent += "\t"; |
| } |
| for (int i = 0; i < spaces; i++) { |
| indent += " "; |
| } |
| return indent; |
| } |
| |
| /** |
| * Computes the length of a an indentation, counting a tab character as the size |
| * until the next tab stop and every other character as one. |
| * |
| * @param indent |
| * the string containing the indentation to measure |
| * @param tabSize |
| * the visual size of tab characters |
| * @return the visual length in number of characters |
| */ |
| private static int computeVisualLength(String indent, int tabSize) { |
| int length = 0; |
| for (int i = 0; i < indent.length(); i++) { |
| char ch = indent.charAt(i); |
| switch (ch) { |
| case '\t': |
| if (tabSize > 0) { |
| length += tabSize; |
| } |
| break; |
| case ' ': |
| length++; |
| break; |
| } |
| } |
| return length; |
| } |
| |
| /** |
| * Strips any characters off the end of an indentation that exceed a specified |
| * maximum visual indentation length. |
| * |
| * @param indent |
| * the string containing the indentation to measure |
| * @param max |
| * the maximum visual indentation length |
| * @param tabSize |
| * the visual size of tab characters |
| * @return a string containing the stripped indentation |
| */ |
| private static String stripExtraChars(String indent, int max, int tabSize) { |
| int measured = 0; |
| int i = 0; |
| for (; (measured < max) && (i < indent.length()); i++) { |
| char ch = indent.charAt(i); |
| switch (ch) { |
| case '\t': |
| if (tabSize > 0) { |
| int reminder = measured % tabSize; |
| measured += tabSize - reminder; |
| } |
| break; |
| case ' ': |
| measured++; |
| break; |
| } |
| } |
| return indent.substring(0, measured > max ? i - 1 : i); |
| } |
| } |