/*******************************************************************************
* Copyright (c) 2014 Salesforce.com, inc..
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Salesforce.com, inc. - initial API and implementation
******************************************************************************/
package com.salesforce.ide.ui.editors.apex.assistance;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import com.salesforce.ide.core.internal.utils.Utils;
import com.salesforce.ide.ui.editors.ForceIdeEditorsPlugin;
import com.salesforce.ide.ui.editors.apex.preferences.PreferenceConstants;
import com.salesforce.ide.ui.editors.internal.utils.EditorMessages;
/**
* Auto indent line strategy sensitive to brackets.
*/
public class ApexAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy {
private static final Logger logger = Logger.getLogger(ApexAutoIndentStrategy.class);
private String indent;
public ApexAutoIndentStrategy() {
indent = indentStringFromEditorsUIPreferences();
}
/**
* Returns the String to use for indenting based on the General/Editors/Text Editors preferences
* that are respected by the underlying platform editing code.
*
* @return the String to use for indenting
*/
private String indentStringFromEditorsUIPreferences() {
IPreferencesService ps = Platform.getPreferencesService();
boolean spacesForTabs = ps.getBoolean(
EditorsUI.PLUGIN_ID,
AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SPACES_FOR_TABS,
false,
null
);
if (spacesForTabs) {
int tabWidth = ps.getInt(
EditorsUI.PLUGIN_ID,
AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH,
4,
null
);
StringBuilder sb = new StringBuilder(tabWidth);
for (int i = 0; i < tabWidth; i++) {
sb.append(" ");
}
return sb.toString();
} else {
return "\t";
}
}
/*
* (non-Javadoc) Method declared on IAutoIndentStrategy
*/
@Override
public void customizeDocumentCommand(IDocument d, DocumentCommand c) {
if ((c.length == 0) && (c.text != null) && endsWithDelimiter(d, c.text)) {
smartIndentAfterNewLine(d, c);
} else if ("}".equals(c.text)) { //$NON-NLS-1$
smartInsertAfterBracket(d, c);
}
}
/**
* Returns whether or not the given text ends with one of the documents legal line delimiters.
*
* @param d
* the document
* @param txt
* the text
* @return <code>true</code> if <code>txt</code> ends with one of the document's line delimiters,
* <code>false</code> otherwise
*/
private boolean endsWithDelimiter(IDocument d, String txt) {
String[] delimiters = d.getLegalLineDelimiters();
if (delimiters != null) {
return TextUtilities.endsWith(delimiters, txt) > -1;
}
return false;
}
/**
* Returns the line number of the next bracket after end.
*
* @param document -
* the document being parsed
* @param line -
* the line to start searching back from
* @param end -
* the end position to search back from
* @param closingBracketIncrease -
* the number of brackets to skip
* @return the line number of the next matching bracket after end
* @throws BadLocationException
* in case the line numbers are invalid in the document
*/
protected int findMatchingOpenBracket(IDocument document, int line, int end, int closingBracketIncrease)
throws BadLocationException {
int start = document.getLineOffset(line);
int brackcount = getBracketCount(document, start, end, false) - closingBracketIncrease;
// sum up the brackets counts of each line (closing brackets count
// negative,
// opening positive) until we find a line the brings the count to zero
while (brackcount < 0) {
line--;
if (line < 0) {
return -1;
}
start = document.getLineOffset(line);
end = start + document.getLineLength(line) - 1;
brackcount += getBracketCount(document, start, end, false);
}
return line;
}
/**
* Returns the bracket value of a section of text. Closing brackets have a value of -1 and open brackets have a
* value of 1.
*
* The braces in the commented region (// or /* .. are ignored ( not counted )
*
* @param document -
* the document being parsed
* @param start -
* the start position for the search
* @param end -
* the end position for the search
* @param ignoreCloseBrackets -
* whether or not to ignore closing brackets in the count
* @return the bracket value of a section of text
* @throws BadLocationException
* in case the positions are invalid in the document
*/
private int getBracketCount(IDocument document, int start, int end, boolean ignoreCloseBrackets)
throws BadLocationException {
int begin = start;
int bracketcount = 0;
while (begin < end) {
char curr = document.getChar(begin);
begin++;
switch (curr) {
case '/':
if (begin < end) {
char next = document.getChar(begin);
if (next == '*') {
// a comment starts, advance to the comment end
begin = getCommentEnd(document, begin + 1, end);
} else if (next == '/') {
// '//'-comment: nothing to do anymore on this line
// change the begin index to the start of the next line if it exists
// else change the begin index to end.
int lineNumber = document.getLineOfOffset(begin);
if(lineNumber + 1 >= document.getNumberOfLines()){
begin = end;
}else{
begin = document.getLineOffset(lineNumber+1);
}
}
}
break;
case '*':
if (begin < end) {
char next = document.getChar(begin);
if (next == '/') {
// we have been in a comment: forget what we read before
bracketcount = 0;
begin++;
}
}
break;
case '{':
bracketcount++;
ignoreCloseBrackets = false;
break;
case '}':
if (!ignoreCloseBrackets) {
bracketcount--;
}
break;
case '"':
case '\'':
begin = getStringEnd(document, begin, end, curr);
break;
default:
}
}
return bracketcount;
}
/**
* Returns the end position of a comment starting at the given <code>position</code>.
*
* @param document -
* the document being parsed
* @param position -
* the start position for the search
* @param end -
* the end position for the search
* @return the end position of a comment starting at the given <code>position</code>
* @throws BadLocationException
* in case <code>position</code> and <code>end</code> are invalid in the document
*/
private int getCommentEnd(IDocument document, int position, int end) throws BadLocationException {
int currentPosition = position;
while (currentPosition < end) {
char curr = document.getChar(currentPosition);
currentPosition++;
if (curr == '*') {
if ((currentPosition < end) && (document.getChar(currentPosition) == '/')) {
return currentPosition + 1;
}
}
}
return end;
}
/**
* Returns the content of the given line without the leading whitespace.
*
* @param document -
* the document being parsed
* @param line -
* the line being searched
* @return the content of the given line without the leading whitespace
* @throws BadLocationException
* in case <code>line</code> is invalid in the document
*/
protected String getIndentOfLine(IDocument document, int line) throws BadLocationException {
if (line > -1) {
int start = document.getLineOffset(line);
int end = start + document.getLineLength(line) - 1;
int whiteend = findEndOfWhiteSpace(document, start, end);
return document.get(start, whiteend - start);
}
return ""; //$NON-NLS-1$
}
/**
* Returns the position of the <code>character</code> in the <code>document</code> after <code>position</code>.
*
* @param document -
* the document being parsed
* @param position -
* the position to start searching from
* @param end -
* the end of the document
* @param character -
* the character you are trying to match
* @return the next location of <code>character</code>
* @throws BadLocationException
* in case <code>position</code> is invalid in the document
*/
private int getStringEnd(IDocument document, int position, int end, char character) throws BadLocationException {
int currentPosition = position;
while (currentPosition < end) {
char currentCharacter = document.getChar(currentPosition);
currentPosition++;
if (currentCharacter == '\\') {
// ignore escaped characters
currentPosition++;
} else if (currentCharacter == character) {
return currentPosition;
}
}
return end;
}
/**
* Set the indent of a new line based on the command provided in the supplied document.
*
* @param document -
* the document being parsed
* @param command -
* the command being performed
*/
protected void smartIndentAfterNewLine(IDocument document, DocumentCommand command) {
int docLength = document.getLength();
if ((command.offset == -1) || (docLength == 0)) {
return;
}
try {
int p = (command.offset == docLength ? command.offset - 1 : command.offset);
int line = document.getLineOfOffset(p);
StringBuffer buf = new StringBuffer(command.text);
int start = document.getLineOffset(line);
IPreferenceStore preferenceStore = getPreferenceStore();
boolean closeBraces = preferenceStore.getBoolean(PreferenceConstants.EDITOR_CLOSE_BRACES);
int lastChar = findLastNonWhiteSpace(document, start, command.offset);
int whiteend = findEndOfWhiteSpace(document, start, command.offset);
// insert closing brace on new line after an unclosed opening brace
if (getBracketCount(document, 0, docLength, false) > 0 && closeBraces
&& (document.getChar(lastChar) == '{')) {
buf.append(document.get(start, whiteend - start));
buf.append(indent);
command.caretOffset = command.offset + buf.length();
command.shiftsCaret = false;
buf.append('\n');
buf.append(document.get(start, whiteend - start));
buf.append('}');
} else if ((command.offset < docLength) && (document.getChar(command.offset) == '}')) {
int indLine = findMatchingOpenBracket(document, line, command.offset, 0);
if (indLine == -1) {
indLine = line;
}
buf.append(getIndentOfLine(document, indLine));
} else {
buf.append(document.get(start, whiteend - start));
if (getBracketCount(document, start, command.offset, true) > 0) {
buf.append(indent);
}
}
command.text = buf.toString();
} catch (BadLocationException excp) {
logger.warn(EditorMessages.getString("ApexEditor.AutoIndent.error.bad_location_1"));
}
}
/**
* Set the indent of a bracket based on the command provided in the supplied document.
*
* @param document -
* the document being parsed
* @param command -
* the command being performed
*/
protected void smartInsertAfterBracket(IDocument document, DocumentCommand command) {
if ((command.offset == -1) || (document.getLength() == 0)) {
return;
}
try {
int p = (command.offset == document.getLength() ? command.offset - 1 : command.offset);
int line = document.getLineOfOffset(p);
int start = document.getLineOffset(line);
int whiteend = findEndOfWhiteSpace(document, start, command.offset);
// shift only when line does not contain any text up to the closing
// bracket
if (whiteend == command.offset) {
// evaluate the line with the opening bracket that matches out
// closing bracket
int indLine = findMatchingOpenBracket(document, line, command.offset, 1);
if ((indLine != -1) && (indLine != line)) {
// take the indent of the found line
StringBuffer replaceText = new StringBuffer(getIndentOfLine(document, indLine));
// add the rest of the current line including the just added
// close bracket
replaceText.append(document.get(whiteend, command.offset - whiteend));
replaceText.append(command.text);
// modify document command
command.length = command.offset - start;
command.offset = start;
command.text = replaceText.toString();
}
}
} catch (BadLocationException excp) {
logger.warn(EditorMessages.getString("ApexEditor.AutoIndent.error.bad_location_2")); //$NON-NLS-1$
}
}
private static IPreferenceStore getPreferenceStore() {
return ForceIdeEditorsPlugin.getDefault().getPreferenceStore();
}
protected int findLastNonWhiteSpace(IDocument document, int offset, int end) throws BadLocationException {
try {
IRegion region = document.getLineInformationOfOffset(offset);
String lineText = document.get(offset, region.getOffset() + region.getLength() - offset);
String[] tokens = lineText.split("\\s+"); //$NON-NLS-1$
if (!Utils.isEmpty(tokens)) {
for(int i = tokens.length - 1; i > -1; i--)
{
String token = tokens[i];
if (!Utils.isEmpty(token)) {
offset += lineText.lastIndexOf(token) + token.length() - 1 ;
break;
}
}
}
} catch (BadLocationException e) {
logger.error("Unable to compute offset", e); //$NON-NLS-1$
}
return offset;
}
}