/**
* Copyright (c) 2005-2011 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.
*/
/*
* Created on Jul 1, 2006
* @author Fabio
*/
package org.python.pydev.editor.codecompletion;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.python.pydev.core.IToken;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.codecompletion.revisited.modules.SourceToken;
import org.python.pydev.editor.hover.PyTextHover;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.parser.jython.ast.ClassDef;
import org.python.pydev.parser.jython.ast.FunctionDef;
import com.aptana.shared_core.string.FastStringBuffer;
import com.aptana.shared_core.utils.RunInUiThread;
public final class PyLinkedModeCompletionProposal extends AbstractPyCompletionProposalExtension2 implements
ICompletionProposalExtension {
private int firstParameterLen = 0;
/**
* The number of positions that we should add to the original position.
*
* Used so that when we enter '.', we add an additional position (because '.' will be added when applying the completion)
* or so that we can go back one position when the toggle mode (ctrl) is on and a completion with parameters is applied (and they
* are removed)
*/
private int nPositionsAdded = 0;
/**
* If true, we'll go to the linked mode after applying a completion.
*/
private boolean goToLinkedMode = true;
/**
* This is the token from where we should get the image and additional info.
*/
private IToken element;
/**
* Offset forced to be returned (only valid if >= 0)
*/
private int newForcedOffset = -1;
/**
* Constructor where the image and the docstring are lazily computed (initially added for the java integration).
*/
public PyLinkedModeCompletionProposal(String replacementString, int replacementOffset, int replacementLength,
int cursorPosition, IToken element, String displayString, IContextInformation contextInformation,
int priority, int onApplyAction, String args) {
super(replacementString, replacementOffset, replacementLength, cursorPosition, null, displayString,
contextInformation, "", priority, onApplyAction, args);
this.element = element;
}
/**
* @return the element
*/
public IToken getElement() {
return element;
}
/**
* Constructor where all the info is passed.
*/
public PyLinkedModeCompletionProposal(String replacementString, int replacementOffset, int replacementLength,
int cursorPosition, Image image, String displayString, IContextInformation contextInformation,
String additionalProposalInfo, int priority, int onApplyAction, String args) {
super(replacementString, replacementOffset, replacementLength, cursorPosition, image, displayString,
contextInformation, additionalProposalInfo, priority, onApplyAction, args);
}
/**
* Constructor where all the info is passed.
*/
public PyLinkedModeCompletionProposal(String replacementString, int replacementOffset, int replacementLength,
int cursorPosition, Image image, String displayString, IContextInformation contextInformation,
String additionalProposalInfo, int priority, int onApplyAction, String args, boolean goToLinkedMode) {
super(replacementString, replacementOffset, replacementLength, cursorPosition, image, displayString,
contextInformation, additionalProposalInfo, priority, onApplyAction, args);
this.goToLinkedMode = goToLinkedMode;
}
//methods overriden to be lazily gotten ----------------------------------------------------------------------------
@Override
public Image getImage() {
if (element != null) {
return element.getImage();
} else {
return super.getImage();
}
}
private String computedInfo = null;
@Override
public String getAdditionalProposalInfo() {
if (computedInfo != null) {
return computedInfo;
}
if (element != null) {
if (element instanceof SourceToken) {
SourceToken sourceToken = (SourceToken) element;
SimpleNode ast = sourceToken.getAst();
if (ast != null && (ast instanceof FunctionDef || ast instanceof ClassDef)) {
computedInfo = PyTextHover.printAst(null, ast);
}
if (computedInfo != null) {
return computedInfo;
}
}
computedInfo = element.getDocStr();
return computedInfo;
} else {
return super.getAdditionalProposalInfo();
}
}
//end methods overriden to be lazily gotten ------------------------------------------------------------------------
/*
* @see ICompletionProposal#getSelection(IDocument)
*/
public Point getSelection(IDocument document) {
if (newForcedOffset >= 0) {
return new Point(newForcedOffset, 0);
}
if (onApplyAction == ON_APPLY_JUST_SHOW_CTX_INFO) {
return null;
}
if (onApplyAction == ON_APPLY_SHOW_CTX_INFO_AND_ADD_PARAMETETRS) {
if (fArgs.length() > 0) {
return new Point(fReplacementOffset + fCursorPosition - 1, firstParameterLen); //the difference is the firstParameterLen here (instead of 0)
}
return null;
}
if (onApplyAction == ON_APPLY_DEFAULT) {
return new Point(fReplacementOffset + fCursorPosition + nPositionsAdded, firstParameterLen); //the difference is the firstParameterLen here (instead of 0)
}
throw new RuntimeException("Unexpected apply mode:" + onApplyAction);
}
public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
boolean eat = (stateMask & SWT.MOD1) != 0;
IDocument doc = viewer.getDocument();
if (!triggerCharAppliesCurrentCompletion(trigger, doc, offset)) {
newForcedOffset = offset + 1; //+1 because that's the len of the trigger
return;
}
if (onApplyAction == ON_APPLY_JUST_SHOW_CTX_INFO) {
return;
}
if (onApplyAction == ON_APPLY_SHOW_CTX_INFO_AND_ADD_PARAMETETRS) {
try {
String args;
if (fArgs.length() > 0) {
args = fArgs.substring(1, fArgs.length() - 1); //remove the parenthesis
} else {
args = "";
}
super.apply(doc);
if (!goToLinkedMode) {
return;
}
int iPar = -1;
int exitPos = offset + args.length() + 1;
goToLinkedModeFromArgs(viewer, offset, doc, exitPos, iPar, args);
} catch (BadLocationException e) {
Log.log(e);
}
return;
}
if (onApplyAction == ON_APPLY_DEFAULT) {
try {
int dif = offset - fReplacementOffset;
String strToAdd = fReplacementString.substring(dif);
boolean doReturn = applyOnDoc(offset, eat, doc, dif, trigger);
if (doReturn || !goToLinkedMode) {
return;
}
//ok, now, on to the linking part
int iPar = strToAdd.indexOf('(');
if (iPar != -1 && strToAdd.charAt(strToAdd.length() - 1) == ')') {
String newStr = strToAdd.substring(iPar + 1, strToAdd.length() - 1);
goToLinkedModeFromArgs(viewer, offset, doc, offset + strToAdd.length() + nPositionsAdded, iPar,
newStr);
}
} catch (BadLocationException e) {
Log.log(e);
}
return;
}
throw new RuntimeException("Unexpected apply mode:" + onApplyAction);
}
/**
* Applies the changes in the document (useful for testing)
*
* @param offset the offset where the change should be applied
* @param eat whether we should 'eat' the selection (on toggle)
* @param doc the document where the changes should be applied
* @param dif the difference between the offset and and the replacement offset
*
* @return whether we should return (and not keep on with the linking mode)
* @throws BadLocationException
*/
public boolean applyOnDoc(int offset, boolean eat, IDocument doc, int dif, char trigger)
throws BadLocationException {
boolean doReturn = false;
String rep = fReplacementString;
int iPar = rep.indexOf('(');
if (eat) {
//behavior change: when we have a parenthesis and we're in toggle (eat) mode, let's not add the
//parenthesis anymore.
if (/*fLastIsPar &&*/iPar != -1) {
rep = rep.substring(0, iPar);
doc.replace(offset - dif, dif + this.fLen, rep);
//if the last was a parenthesis, there's nothing to link, so, let's return
if (!fLastIsPar) {
nPositionsAdded = -1;
}
doReturn = true;
} else {
int sumReplace = 0;
if (rep.endsWith("=")) {
//Special case when applying keyword completions (i.e.: call(param=10)), where the text added is "param=")
try {
char c = doc.getChar(offset + this.fLen);
if (c == '=') {
sumReplace++;
}
} catch (BadLocationException e) {
//just ignore it!
}
}
doc.replace(offset - dif, dif + this.fLen + sumReplace, rep);
}
} else {
if (trigger == '.' || trigger == '(') {
if (iPar != -1) {
//if we had a completion with parameters, we should just remove everything that would appear after
//the parenthesis -- and we don't need to raise nTriggerCharsAdded because the cursor position would
//already be after the '(. -- which in this case we'll substitute for a '.'
rep = rep.substring(0, iPar);
} else {
nPositionsAdded = 1;
}
rep = rep + trigger;
if (trigger == '(') {
rep += ')';
}
//linking should not happen when applying '.'
doReturn = true;
}
//if the trigger is ')', just let it apply regularly -- so, ')' will only be added if it's already in the completion.
doc.replace(offset - dif, dif, rep);
}
return doReturn;
}
private void goToLinkedModeFromArgs(ITextViewer viewer, int offset, IDocument doc, int exitPos, int iPar,
String newStr) throws BadLocationException {
if (!goToLinkedMode) {
return;
}
List<Integer> offsetsAndLens = new ArrayList<Integer>();
FastStringBuffer buffer = new FastStringBuffer();
for (int i = 0; i < newStr.length(); i++) {
char c = newStr.charAt(i);
if (Character.isJavaIdentifierPart(c)) {
if (buffer.length() == 0) {
offsetsAndLens.add(i);
buffer.append(c);
} else {
buffer.append(c);
}
} else {
if (buffer.length() > 0) {
offsetsAndLens.add(buffer.length());
buffer.clear();
}
}
}
if (buffer.length() > 0) {
offsetsAndLens.add(buffer.length());
}
buffer = null;
goToLinkedMode(viewer, offset, doc, exitPos, iPar, offsetsAndLens);
}
private void goToLinkedMode(ITextViewer viewer, int offset, IDocument doc, int exitPos, int iPar,
List<Integer> offsetsAndLens) throws BadLocationException {
if (!goToLinkedMode) {
return;
}
if (offsetsAndLens.size() > 0) {
LinkedModeModel model = new LinkedModeModel();
for (int i = 0; i < offsetsAndLens.size(); i++) {
Integer offs = offsetsAndLens.get(i);
i++;
Integer len = offsetsAndLens.get(i);
if (i == 1) {
firstParameterLen = len;
}
int location = offset + iPar + offs + 1;
LinkedPositionGroup group = new LinkedPositionGroup();
ProposalPosition proposalPosition = new ProposalPosition(doc, location, len, 0,
new ICompletionProposal[0]);
group.addPosition(proposalPosition);
model.addGroup(group);
}
model.forceInstall();
final LinkedModeUI ui = new EditorLinkedModeUI(model, viewer);
ui.setDoContextInfo(true); //set it to request the ctx info from the completion processor
ui.setExitPosition(viewer, exitPos, 0, Integer.MAX_VALUE);
Runnable r = new Runnable() {
public void run() {
ui.enter();
}
};
RunInUiThread.async(r);
}
}
//-------------------------------------------- ICompletionProposalExtension
/**
* We want to apply it on \n or on '.'
*
* When . is entered, the user will finish (and apply) the current completion
* and request a new one with '.'
*
* If not added, it won't request the new one (and will just stop the current)
*/
public char[] getTriggerCharacters() {
if (onApplyAction != ON_APPLY_DEFAULT) {
return null;
}
return super.getTriggerCharacters();
}
@Override
public String toString() {
return "PyLinkedModeCompletionProposal(" + this.getDisplayString() + ")";
}
//testing
void setLen(int i) {
this.fLen = i;
}
}