/**
* Copyright (c) 2005-2013 by Appcelerator, Inc. 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.
*/
package org.python.pydev.editor.actions;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentCommand;
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.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.python.pydev.core.IIndentPrefs;
import org.python.pydev.core.docutils.ParsingUtils;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.core.docutils.SyntaxErrorException;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.autoedit.DefaultIndentPrefs;
import org.python.pydev.editor.autoedit.PyAutoIndentStrategy;
import org.python.pydev.shared_core.string.FastStringBuffer;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.utils.DocCmd;
import org.python.pydev.shared_ui.editor.ITextViewerExtensionAutoEditions;
/**
* Something similar org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor.BracketInserter (but not too similar).
*
* @author Fabio Zadrozny
*/
public class PyPeerLinker {
private IIndentPrefs prefs;
public void setIndentPrefs(IIndentPrefs prefs) {
this.prefs = prefs;
}
private int linkOffset;
private int linkExitPos;
private int linkLen;
/**
* Creates a handler that will properly treat backspaces considering python code.
*/
public static VerifyKeyListener createVerifyKeyListener(final TextViewer viewer) {
return new VerifyKeyListener() {
private final PyPeerLinker pyPeerLinker = new PyPeerLinker();
@Override
public void verifyKey(VerifyEvent event) {
if (!event.doit) {
return;
}
switch (event.character) {
case '\'':
case '\"':
case '[':
case '{':
case '(':
break;
default:
return;
}
if (viewer != null && viewer.isEditable()) {
boolean blockSelection = false;
try {
blockSelection = viewer.getTextWidget().getBlockSelection();
} catch (Throwable e) {
//that's OK (only available in eclipse 3.5)
}
if (!blockSelection) {
if (viewer instanceof ITextViewerExtensionAutoEditions) {
ITextViewerExtensionAutoEditions autoEditions = (ITextViewerExtensionAutoEditions) viewer;
if (!autoEditions.getAutoEditionsEnabled()) {
return;
}
}
ISelection selection = viewer.getSelection();
if (selection instanceof ITextSelection) {
IAdaptable adaptable;
if (viewer instanceof IAdaptable) {
adaptable = (IAdaptable) viewer;
} else {
adaptable = new IAdaptable() {
@Override
public <T> T getAdapter(Class<T> adapter) {
return null;
}
};
}
//Don't bother in getting the indent prefs from the editor: the default indent prefs are
//always global for the settings we want.
pyPeerLinker.setIndentPrefs(new DefaultIndentPrefs(adaptable));
PySelection ps = new PySelection(viewer.getDocument(), (ITextSelection) selection);
if (pyPeerLinker.perform(ps, event.character, viewer)) {
event.doit = false;
}
}
}
}
}
};
}
/**
* @param ps
*/
protected boolean perform(PySelection ps, final char c, TextViewer viewer) {
linkOffset = -1;
linkExitPos = -1;
linkLen = 0;
boolean literal = true;
switch (c) {
case '\'':
case '\"':
break;
case '[':
case '{':
case '(':
literal = false;
break;
default:
return false;
}
if (literal) {
if (!prefs.getAutoLiterals()) {
return false;
}
} else {
if (!prefs.getAutoParentesis()) {
return false;
}
}
try {
IDocument doc = ps.getDoc();
String contentType = ParsingUtils.getContentType(ps.getDoc(), ps.getAbsoluteCursorOffset());
boolean isDefaultContext = contentType.equals(ParsingUtils.PY_DEFAULT);
if (!isDefaultContext) {
//not handled: leave it up to the auto-indent (if we're in link mode already it may delete the selected text and add a ', which is what we want).
return false;
}
DocCmd docCmd = new DocCmd(ps.getAbsoluteCursorOffset(), ps.getSelLength(), "" + c);
if (literal) {
if (!handleLiteral(doc, docCmd, ps, isDefaultContext, prefs)) {
return false; //not handled
}
} else {
if (!handleBrackets(ps, c, doc, docCmd, viewer)) {
return false; //not handled
}
}
if (linkOffset == -1 || linkExitPos == -1) {
return true; //it was handled (without the link)
}
if (prefs.getAutoLink()) {
LinkedPositionGroup group = new LinkedPositionGroup();
group.addPosition(new LinkedPosition(doc, linkOffset, linkLen, LinkedPositionGroup.NO_STOP));
LinkedModeModel model = new LinkedModeModel();
model.addGroup(group);
model.forceInstall();
if (viewer == null) {
return true; //don't actually do the link.
}
LinkedModeUI ui = new EditorLinkedModeUI(model, viewer);
ui.setSimpleMode(true);
IExitPolicy policy = new IExitPolicy() {
@Override
public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
//Yes, no special exit, if ' is entered again, let's do the needed treatment again instead of going
//to the end (only <return> goes to the end).
//if (event.character == c) {
// return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
//}
return null;
}
};
ui.setExitPolicy(policy);
ui.setExitPosition(viewer, linkExitPos, 0, Integer.MAX_VALUE);
ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
ui.enter();
IRegion newSelection = ui.getSelectedRegion();
viewer.setSelectedRange(newSelection.getOffset(), newSelection.getLength());
} else {
viewer.setSelectedRange(linkOffset, linkLen);
}
} catch (Exception e) {
Log.log(e);
}
return true;
}
private boolean handleBrackets(PySelection ps, final char c, IDocument doc, DocCmd docCmd, TextViewer viewer)
throws BadLocationException {
if (c == '(') {
PyAutoIndentStrategy.handleParens(doc, docCmd, prefs);
docCmd.doExecute(doc);
//Note that this is done with knowledge on how the handleParens deals with the doc command (not meant as a
//general thing to apply a doc command).
if (docCmd.shiftsCaret) {
//Regular stuff: just shift it and don't link
if (viewer != null) {
viewer.setSelectedRange(docCmd.offset + docCmd.text.length(), 0);
}
} else {
linkOffset = docCmd.caretOffset;
linkLen = 0;
linkExitPos = docCmd.offset + docCmd.text.length();
}
} else { // [ or {
char peer = StringUtils.getPeer(c);
if (PyAutoIndentStrategy.shouldClose(ps, c, peer)) {
int offset = ps.getAbsoluteCursorOffset();
doc.replace(offset, ps.getSelLength(), StringUtils.getWithClosedPeer(c));
linkOffset = offset + 1;
linkLen = 0;
linkExitPos = linkOffset + linkLen + 1;
} else {
//No link, just add the char and set the new selected range (if possible)
docCmd.doExecute(doc);
if (viewer != null) {
viewer.setSelectedRange(docCmd.offset + docCmd.text.length(), 0);
}
}
}
//Yes, in this situation, all cases are handled.
return true;
}
/**
* Called right after a ' or "
*
* @return false if we should leave the handling to the auto-indent and true if it handled things properly here.
*/
private boolean handleLiteral(IDocument document, DocumentCommand command, PySelection ps,
boolean isDefaultContext, IIndentPrefs prefs) throws BadLocationException {
int offset = ps.getAbsoluteCursorOffset();
if (command.length > 0) {
String selectedText = ps.getSelectedText();
if (selectedText.indexOf('\r') != -1 || selectedText.indexOf('\n') != -1) {
//we have a new line
FastStringBuffer buf = new FastStringBuffer(selectedText.length() + 10);
buf.appendN(command.text, 3);
buf.append(selectedText);
buf.appendN(command.text, 3);
document.replace(offset, ps.getSelLength(), buf.toString());
linkOffset = offset + 3;
linkLen = selectedText.length();
linkExitPos = linkOffset + linkLen + 3;
} else {
document.replace(offset, ps.getSelLength(), command.text + selectedText + command.text);
linkOffset = offset + 1;
linkLen = selectedText.length();
linkExitPos = linkOffset + linkLen + 1;
}
return true;
}
char literalChar = command.text.charAt(0);
try {
char nextChar = ps.getCharAfterCurrentOffset();
if (Character.isJavaIdentifierPart(nextChar)) {
//we're just before a word (don't try to do anything in this case)
//e.g. |var (| is cursor position)
return false;
}
} catch (BadLocationException e) {
}
String cursorLineContents = ps.getCursorLineContents();
if (cursorLineContents.indexOf(literalChar) == -1) {
if (!isDefaultContext) {
//only add additional chars if on default context.
return false;
}
document.replace(offset, ps.getSelLength(), command.text + command.text);
linkOffset = offset + 1;
linkLen = 0;
linkExitPos = linkOffset + linkLen + 1;
return true;
}
boolean balanced = isLiteralBalanced(cursorLineContents);
Tuple<String, String> beforeAndAfterMatchingChars = ps.getBeforeAndAfterMatchingChars(literalChar);
int matchesBefore = beforeAndAfterMatchingChars.o1.length();
int matchesAfter = beforeAndAfterMatchingChars.o2.length();
boolean hasMatchesBefore = matchesBefore != 0;
boolean hasMatchesAfter = matchesAfter != 0;
if (!hasMatchesBefore && !hasMatchesAfter) {
//if it's not balanced, this char would be the closing char.
if (balanced) {
if (!isDefaultContext) {
//only add additional chars if on default context.
return false;
}
document.replace(offset, ps.getSelLength(), command.text + command.text);
linkOffset = offset + 1;
linkLen = 0;
linkExitPos = linkOffset + linkLen + 1;
return true;
}
} else {
//we're right after or before a " or '
return false;
}
return false;
}
/**
* @return true if the passed string has balanced ' and "
*/
private boolean isLiteralBalanced(String cursorLineContents) {
ParsingUtils parsingUtils = ParsingUtils.create(cursorLineContents, true);
int offset = 0;
int end = cursorLineContents.length();
boolean balanced = true;
while (offset < end) {
char curr = cursorLineContents.charAt(offset++);
if (curr == '"' || curr == '\'') {
int eaten;
try {
eaten = parsingUtils.eatLiterals(null, offset - 1) + 1;
} catch (SyntaxErrorException e) {
balanced = false;
break;
}
if (eaten > offset) {
offset = eaten;
}
}
}
return balanced;
}
/**
* In default namespace (used for testing)
*/
int getLinkLen() {
return linkLen;
}
int getLinkExitPos() {
return linkExitPos;
}
int getLinkOffset() {
return linkOffset;
}
}