/**
* Copyright (c) 2005-2013 by Appcelerator, Inc and Fabio Zadrozny. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
/*
* @author: Fabio Zadrozny
* Created: July 2004
*/
package org.python.pydev.shared_core.auto_edit;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.python.pydev.shared_core.log.Log;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.string.TextSelectionUtils;
import org.python.pydev.shared_core.structure.Tuple;
/**
* @author Fabio Zadrozny
*
* Makes a backspace happen...
*
* We can:
* - go to the indentation from some uncommented previous line (if we
* only have whitespaces in the current line).
* - erase all whitespace characters until we find some character.
* - erase a single character.
*/
public class AutoEditStrategyBackspaceHelper {
private int dontEraseMoreThan = -1;
private IIndentationStringProvider indentationStringProvider;
/**
* Returns the position of the last non whitespace char in the current line.
* @param doc
* @param cursorOffset
* @return position of the last character of the line (returned as an absolute
* offset)
*
* @throws BadLocationException
*/
protected int getLastCharPosition(IDocument doc, int cursorOffset) throws BadLocationException {
IRegion region;
region = doc.getLineInformationOfOffset(cursorOffset);
int offset = region.getOffset();
String src = doc.get(offset, region.getLength());
int i = src.length();
boolean breaked = false;
while (i > 0) {
i--;
//we have to break if we find a character that is not a whitespace or a tab.
if (Character.isWhitespace(src.charAt(i)) == false && src.charAt(i) != '\t') {
breaked = true;
break;
}
}
if (!breaked) {
i--;
}
return (offset + i);
}
public void perform(TextSelectionUtils ps) {
// Perform the action
try {
ITextSelection textSelection = ps.getTextSelection();
if (textSelection.getLength() != 0) {
eraseSelection(ps);
return;
}
int lastCharPosition = getLastCharPosition(ps.getDoc(), ps.getLineOffset());
int cursorOffset = textSelection.getOffset();
IRegion lastCharRegion = ps.getDoc().getLineInformationOfOffset(lastCharPosition + 1);
//System.out.println("cursorOffset: "+ cursorOffset);
//System.out.println("lastCharPosition: "+lastCharPosition);
//System.out.println("lastCharRegion.getOffset(): "
// +lastCharRegion.getOffset());
// IRegion cursorRegion =
// ps.doc.getLineInformationOfOffset(cursorOffset);
if (cursorOffset == lastCharRegion.getOffset()) {
//System.out.println("We are in the beginning of the line.");
//in this situation, we are in the first character of the
// line...
//so, we have to get the end of the other line and delete it.
if (cursorOffset != 0) {
// the first line.
eraseLineDelimiter(ps);
}
} else if (cursorOffset <= lastCharPosition) {
//System.out.println("cursorOffset <= lastCharPosition");
//this situation is:
// |a (delete to previous indentation - considers cursor
// position)
//or
// as | as (delete single char)
//or
// | a (delete to previous indentation - considers cursor
// position)
//so, we have to treat it carefully
eraseToPreviousIndentation(ps, false, lastCharRegion);
} else if (lastCharRegion.getOffset() == lastCharPosition + 1) {
//System.out.println("Only whitespaces in the line.");
//in this situation, this line only has whitespaces,
//so, we have to erase depending on the previous indentation.
eraseToPreviousIndentation(ps, true, lastCharRegion);
} else {
if (cursorOffset - lastCharPosition == 1) {
//System.out.println("Erase single char.");
//last char and cursor are in the same line.
//this situation is:
// a|
eraseSingleChar(ps);
} else if (cursorOffset - lastCharPosition > 1) {
//this situation is:
// a |
//System.out.println("Erase until last char is found.");
eraseUntilLastChar(ps, lastCharPosition);
}
}
} catch (Exception e) {
Log.log(e);
}
}
/**
* @param ps
* @param hasOnlyWhitespaces
* @param lastCharRegion
* @throws BadLocationException
*/
private void eraseToPreviousIndentation(TextSelectionUtils ps, boolean hasOnlyWhitespaces, IRegion lastCharRegion)
throws BadLocationException {
String lineContentsToCursor = ps.getLineContentsToCursor();
if (hasOnlyWhitespaces) {
//System.out.println("only whitespaces");
eraseToIndentation(ps, lineContentsToCursor);
} else {
//System.out.println("not only whitespaces");
//this situation is:
// |a (delete to previous indentation - considers cursor position)
//
//or
//
// as | as (delete single char)
//
//so, we have to treat it carefully
//TODO: use the conditions above and not just erase a single
// char.
if (TextSelectionUtils.containsOnlyWhitespaces(lineContentsToCursor)) {
eraseToIndentation(ps, lineContentsToCursor);
} else {
eraseSingleChar(ps);
}
}
}
private void eraseSingleChar(TextSelectionUtils ps) throws BadLocationException {
ITextSelection textSelection = ps.getTextSelection();
int replaceLength = 1;
int replaceOffset = textSelection.getOffset() - replaceLength;
IDocument doc = ps.getDoc();
String contentType = doc.getContentType(replaceOffset);
if (replaceOffset >= 0 && replaceOffset + replaceLength < doc.getLength()) {
char c = doc.getChar(replaceOffset);
if (c == '(' || c == '[' || c == '{' || c == '<') {
//When removing a (, check if we have to delete the corresponding ) too.
char peer = StringUtils.getPeer(c);
if (replaceOffset + replaceLength < doc.getLength()) {
char c2 = doc.getChar(replaceOffset + 1);
if (c2 == peer) {
//Ok, there's a closing one right next to it, now, what we have to do is
//check if the user was actually removing that one because there's an opening
//one without a match.
//To do that, we go backwards in the document searching for an opening match and then
//search its match. If it's found, it means we can delete both, otherwise, this
//delete will make things correct.
//Create a matcher only matching this char
AutoEditPairMatcher pairMatcher = new AutoEditPairMatcher(
new char[] { c, peer }, contentType);
int openingPeerOffset = pairMatcher.searchForAnyOpeningPeer(replaceOffset, doc);
if (openingPeerOffset == -1) {
replaceLength += 1;
} else {
int closingPeerOffset = pairMatcher.searchForClosingPeer(openingPeerOffset + 1, c, peer,
doc);
if (closingPeerOffset != -1) {
//we have a match, so, things are balanced and we can delete the next
replaceLength += 1;
}
}
}
}
} else if (c == '\'' || c == '"') {
//when removing a ' or ", check if we have to delete another ' or " too.
Tuple<String, String> beforeAndAfterMatchingChars = ps.getBeforeAndAfterMatchingChars(c);
int matchesBefore = beforeAndAfterMatchingChars.o1.length();
int matchesAfter = beforeAndAfterMatchingChars.o2.length();
if (matchesBefore == 1 && matchesBefore == matchesAfter) {
replaceLength += 1;
}
}
}
makeDelete(doc, replaceOffset, replaceLength);
}
/**
*
* @param ps
* @throws BadLocationException
*/
private void eraseLineDelimiter(TextSelectionUtils ps) throws BadLocationException {
ITextSelection textSelection = ps.getTextSelection();
int length = TextSelectionUtils.getDelimiter(ps.getDoc()).length();
int offset = textSelection.getOffset() - length;
//System.out.println("Replacing offset: "+(offset) +" lenght: "+
// (length));
makeDelete(ps.getDoc(), offset, length);
}
/**
*
* @param ps
* @throws BadLocationException
*/
private void eraseSelection(TextSelectionUtils ps) throws BadLocationException {
ITextSelection textSelection = ps.getTextSelection();
makeDelete(ps.getDoc(), textSelection.getOffset(), textSelection.getLength());
}
/**
* @param ps
* @param lastCharPosition
* @throws BadLocationException
*/
private void eraseUntilLastChar(TextSelectionUtils ps, int lastCharPosition) throws BadLocationException {
ITextSelection textSelection = ps.getTextSelection();
int cursorOffset = textSelection.getOffset();
int offset = lastCharPosition + 1;
int length = cursorOffset - lastCharPosition - 1;
//System.out.println("Replacing offset: "+(offset) +" lenght: "+
// (length));
makeDelete(ps.getDoc(), offset, length);
}
/**
* TODO: Make use of the indentation gotten previously. This implementation
* just uses the indentation string and erases the number of chars from it.
*
* @param ps
* @param indentation this is in number of characters.
* @throws BadLocationException
*/
private void eraseToIndentation(TextSelectionUtils ps, String lineContentsToCursor) throws BadLocationException {
final int cursorOffset = ps.getAbsoluteCursorOffset();
final int lineContentsToCursorLen = lineContentsToCursor.length();
if (lineContentsToCursorLen > 0) {
char c = lineContentsToCursor.charAt(lineContentsToCursorLen - 1);
if (c == '\t') {
eraseSingleChar(ps);
return;
}
}
String indentationString = this.indentationStringProvider.getIndentationString();
int replaceLength;
int replaceOffset;
final int indentationLength = indentationString.length();
final int modLen = lineContentsToCursorLen % indentationLength;
if (modLen == 0) {
replaceOffset = cursorOffset - indentationLength;
replaceLength = indentationLength;
} else {
replaceOffset = cursorOffset - modLen;
replaceLength = modLen;
}
IDocument doc = ps.getDoc();
// final int cursorLine = ps.getCursorLine();
// if (cursorLine > 0) {
// IRegion prevLineInfo = doc.getLineInformation(cursorLine - 1);
// int prevLineEndOffset = prevLineInfo.getOffset() + prevLineInfo.getLength();
// Tuple<Integer, Boolean> tup = PyAutoIndentStrategy.determineSmartIndent(prevLineEndOffset, doc, prefs);
// Integer previousContextSmartIndent = tup.o1;
// if (previousContextSmartIndent > 0 && lineContentsToCursorLen > previousContextSmartIndent) {
// int initialLineOffset = cursorOffset - lineContentsToCursorLen;
// if (replaceOffset < initialLineOffset + previousContextSmartIndent) {
// int newReplaceOffset = initialLineOffset + previousContextSmartIndent + 1;
// if (newReplaceOffset != cursorOffset) {
// replaceOffset = newReplaceOffset;
// replaceLength = cursorOffset - replaceOffset;
// }
// }
// }
// }
//now, check what we're actually removing here... we can only remove chars if they are the
//same, so, if we have a replace for '\t ', we should only remove the ' ', and not the '\t'
if (replaceLength > 1) {
String strToReplace = doc.get(replaceOffset, replaceLength);
char prev = 0;
for (int i = strToReplace.length() - 1; i >= 0; i--) {
char c = strToReplace.charAt(i);
if (prev != 0) {
if (c != prev) {
replaceOffset += (i + 1);
replaceLength -= (i + 1);
break;
}
}
prev = c;
}
}
makeDelete(doc, replaceOffset, replaceLength);
}
private void makeDelete(IDocument doc, int replaceOffset, int replaceLength) throws BadLocationException {
if (replaceOffset < dontEraseMoreThan) {
int delta = dontEraseMoreThan - replaceOffset;
replaceOffset = dontEraseMoreThan;
replaceLength -= delta;
if (replaceLength <= 0) {
return;
}
}
doc.replace(replaceOffset, replaceLength, "");
}
public void setDontEraseMoreThan(int offset) {
this.dontEraseMoreThan = offset;
}
/**
* Creates a handler that will properly treat backspaces considering python code.
*/
public static VerifyKeyListener createVerifyKeyListener(final TextViewer viewer,
final IIndentationStringProvider indentationStringProvider) {
return new VerifyKeyListener() {
@Override
public void verifyKey(VerifyEvent event) {
if ((event.doit && event.character == SWT.BS && event.stateMask == 0 && viewer != null && viewer
.isEditable())) { //isBackspace
boolean blockSelection = false;
try {
blockSelection = viewer.getTextWidget().getBlockSelection();
} catch (Throwable e) {
//that's OK (only available in eclipse 3.5)
}
if (!blockSelection) {
ISelection selection = viewer.getSelection();
if (selection instanceof ITextSelection) {
//Only do our custom backspace if we're not in block selection mode.
AutoEditStrategyBackspaceHelper pyBackspace = new AutoEditStrategyBackspaceHelper();
pyBackspace.setIndentationStringProvider(indentationStringProvider);
TextSelectionUtils ps = new TextSelectionUtils(viewer.getDocument(),
(ITextSelection) selection);
pyBackspace.perform(ps);
event.doit = false;
}
}
}
}
};
}
protected void setIndentationStringProvider(IIndentationStringProvider indentationStringProvider) {
this.indentationStringProvider = indentationStringProvider;
}
}