blob: e6815fd7a3385febcfa2e1b7c1b5e64693aad7b4 [file] [log] [blame]
/*******************************************************************************
* 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);
}
}