/*******************************************************************************
* Copyright (c) 2000, 2008 IBM Corporation 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:
* Andrew McCullough - initial API and implementation
* IBM Corporation - general improvement and bug fixes, partial reimplementation
*******************************************************************************/
package org.eclipse.php.internal.ui.editor.contentassist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.dltk.core.*;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.*;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.link.*;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.php.core.PHPVersion;
import org.eclipse.php.core.compiler.ast.nodes.NamespaceReference;
import org.eclipse.php.core.project.ProjectOptions;
import org.eclipse.php.internal.core.PHPCoreConstants;
import org.eclipse.php.internal.core.PHPCorePlugin;
import org.eclipse.php.internal.core.codeassist.AliasMethod;
import org.eclipse.php.internal.core.codeassist.AliasType;
import org.eclipse.php.internal.core.codeassist.ProposalExtraInfo;
import org.eclipse.php.internal.core.typeinference.FakeConstructor;
import org.eclipse.php.internal.core.typeinference.PHPModelUtils;
import org.eclipse.php.internal.ui.PHPUiPlugin;
import org.eclipse.php.internal.ui.text.template.contentassist.PositionBasedCompletionProposal;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
/**
* This is a
* {@link org.eclipse.jdt.internal.ui.text.java.JavaCompletionProposal} which
* includes templates that represent the best guess completion for each
* parameter of a method.
*/
public final class ParameterGuessingProposal extends PHPOverrideCompletionProposal
implements IPHPCompletionProposalExtension {
private static final char[] NO_TRIGGERS = new char[0];
protected static final char LPAREN = '('; // $NON-NLS-1$
protected static final char RPAREN = ')'; // $NON-NLS-1$
protected static final String COMMA = ", "; //$NON-NLS-1$
private CompletionProposal fProposal;
private IMethod method;
private IMethod guessingMethod;
private final boolean fFillBestGuess;
private boolean fReplacementStringComputed = false;
private Object extraInfo;
private boolean fReplacementLengthComputed;
private String alias = null;
private IDocument document = null;
private IScriptProject sProject = null;
private ICompletionProposal[][] fChoices; // initialized by
// guessParameters()
private Position[] fPositions; // initialized by guessParameters()
private IRegion fSelectedRegion; // initialized by apply()
private IPositionUpdater fUpdater;
public ParameterGuessingProposal(CompletionProposal proposal, IScriptProject jproject, ISourceModule cu,
String methodName, String[] paramTypes, int start, int length, StyledString displayName,
String completionProposal, boolean fillBestGuess, Object extraInfo, IDocument document) {
super(jproject, cu, methodName, paramTypes, start, length, displayName, completionProposal);
this.fProposal = proposal;
method = (IMethod) fProposal.getModelElement();
guessingMethod = method;
this.fFillBestGuess = fillBestGuess;
this.extraInfo = extraInfo;
this.document = document;
this.sProject = jproject;
}
/*
* @see ICompletionProposalExtension#apply(IDocument, char)
*/
@Override
public void apply(IDocument document, char trigger, int offset) {
try {
dealPrefix();
dealSuffix(document, offset);
// TODO lengthChange is workaround for replacement string changed by
// super class, this needs better solution
int lengthChange = getReplacementString().length();
super.apply(document, trigger, offset);
lengthChange = Math.max(0, getReplacementString().length() - lengthChange);
int baseOffset = getReplacementOffset();
String replacement = getReplacementString();
boolean hasParameters = false;
try {
hasParameters = method.getParameters().length != 0;
} catch (ModelException e) {
PHPUiPlugin.log(e);
}
if (hasParameters && getTextViewer() != null) {
LinkedModeModel model = new LinkedModeModel();
if ((fPositions != null && fPositions.length > 0)) {
for (int i = 0; i < fPositions.length; i++) {
LinkedPositionGroup group = new LinkedPositionGroup();
int positionOffset = fPositions[i].getOffset() + lengthChange;
int positionLength = fPositions[i].getLength();
if (fChoices[i].length < 2) {
group.addPosition(new LinkedPosition(document, positionOffset, positionLength,
LinkedPositionGroup.NO_STOP));
} else {
ensurePositionCategoryInstalled(document, model);
document.addPosition(getCategory(), fPositions[i]);
group.addPosition(new ProposalPosition(document, positionOffset, positionLength,
LinkedPositionGroup.NO_STOP, fChoices[i]));
}
model.addGroup(group);
}
} else {
LinkedPositionGroup group = new LinkedPositionGroup();
group.addPosition(new LinkedPosition(document, getReplacementOffset() + getCursorPosition(), 0,
LinkedPositionGroup.NO_STOP));
model.addGroup(group);
}
model.forceInstall();
LinkedModeUI ui = new EditorLinkedModeUI(model, getTextViewer());
ui.setExitPosition(getTextViewer(), baseOffset + replacement.length(), 0, Integer.MAX_VALUE);
ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT);
ui.setDoContextInfo(true);
ui.enter();
fSelectedRegion = ui.getSelectedRegion();
} else {
fSelectedRegion = new Region(baseOffset + getCursorPosition(), 0);
}
} catch (BadLocationException e) {
ensurePositionCategoryRemoved(document);
PHPUiPlugin.log(e);
openErrorDialog(e);
} catch (BadPositionCategoryException e) {
ensurePositionCategoryRemoved(document);
PHPUiPlugin.log(e);
openErrorDialog(e);
}
}
private void dealPrefix() {
String prefix = ""; //$NON-NLS-1$
if (shouldHaveGlobalNamespace()) {
prefix += NamespaceReference.NAMESPACE_SEPARATOR;
}
if (ProposalExtraInfo.isMethodOnly(extraInfo)) {
setReplacementString(prefix + method.getElementName());
return;
}
IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(PHPCorePlugin.ID);
boolean fileArgumentNames = prefs.getBoolean(PHPCoreConstants.CODEASSIST_FILL_ARGUMENT_NAMES, true);
if (fileArgumentNames && !fReplacementStringComputed)
setReplacementString(computeReplacementString(prefix));
if (!fileArgumentNames)
setReplacementString(prefix + super.getReplacementString());
}
private boolean shouldHaveGlobalNamespace() {
if (ProjectOptions.getPHPVersion(sProject.getProject()).isLessThan(PHPVersion.PHP5_3)) {
return false;
}
IType type = method.getDeclaringType();
boolean isInNamespace = PHPModelUtils.getCurrentNamespaceIfAny(fSourceModule, getReplacementOffset()) != null;
boolean isNotAlias = !(type instanceof AliasType);
boolean isNamespacedType = PHPModelUtils.getCurrentNamespace(type) != null;
try {
boolean globalMethod = (type == null && method.getNamespace() == null);
boolean globalConstructor = type != null && !isNamespacedType && method.isConstructor();
if (((globalMethod && prefixGlobalFunctionCall()) || globalConstructor) && isInNamespace && isNotAlias
&& document.getChar(getReplacementOffset() - 1) != NamespaceReference.NAMESPACE_SEPARATOR) {
return true;
}
} catch (ModelException e) {
PHPUiPlugin.log(e);
} catch (BadLocationException e) {
PHPUiPlugin.log(e);
}
return false;
}
private boolean prefixGlobalFunctionCall() {
return Platform.getPreferencesService().getBoolean(PHPCorePlugin.ID,
PHPCoreConstants.CODEASSIST_PREFIX_GLOBAL_FUNCTION_CALL, false, null);
}
private void dealSuffix(IDocument document, int offset) {
boolean toggleEating = isToggleEating();
boolean insertCompletion = insertCompletion();
String replacement = getReplacementString();
int posReplacementLP = replacement.indexOf(LPAREN);
if (posReplacementLP >= 0 && replacement.endsWith(String.valueOf(RPAREN))) {
int searchOffset;
if (!insertCompletion || toggleEating) {
searchOffset = getReplacementOffset() + getReplacementLength();
} else {
searchOffset = offset;
}
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=459377
int posLP = getRelativePositionOf(document, searchOffset, LPAREN);
if (posLP >= 0) {
int posRP = getRelativePositionOf(document, searchOffset + (posLP + 1), RPAREN);
if (posRP < 0) {
// unset all parameters that have to be written in the
// document, they should not collide with the text
// already written in the document after left parenthesis
fPositions = null;
fChoices = null;
// we truncate the replacement text starting
// from left parenthesis (included)
replacement = replacement.substring(0, posReplacementLP);
setReplacementString(replacement);
// put the cursor before left parenthesis in document,
// it will be put after left parenthesis through
// PHPOverrideCompletionProposal#calculateCursorPosition()
setReplacementLength(getReplacementLength() + posLP);
} else {
// put the cursor after right parenthesis in document
setReplacementLength(getReplacementLength() + (posLP + 1) + (posRP + 1));
}
}
} else {
// unset all existing parameters (if any), they are useless now
fPositions = null;
fChoices = null;
int searchOffset;
if (!insertCompletion || toggleEating) {
searchOffset = getReplacementOffset() + getReplacementLength();
} else {
searchOffset = offset;
}
int posLP = getRelativePositionOf(document, searchOffset, LPAREN);
if (posLP < 0) {
// append missing parentheses in insert and overwrite mode
replacement = replacement + LPAREN + RPAREN;
setReplacementString(replacement);
} else {
// put the cursor before left parenthesis in document,
// it will be put after left parenthesis through
// PHPOverrideCompletionProposal#calculateCursorPosition()
setReplacementLength(getReplacementLength() + posLP);
}
}
}
/**
* Retrieves the position of the first occurrence of a "search" character,
* starting from "offset" position and until end of line.
*
* @param document
* @param offset
* @param search
* character to search
* @return position of "search" character relative to offset, -1 if not
* found or if there are non-whitespace characters between "offset"
* position and the first occurrence of the "search" character
*/
private int getRelativePositionOf(IDocument document, int offset, char search) {
try {
IRegion line = document.getLineInformationOfOffset(offset);
int lineEnd = line.getOffset() + line.getLength();
if (offset >= lineEnd) {
// end of line
return -1;
}
int pos = 0;
while (offset + pos < lineEnd - 1 // 1 = "search" length
&& Character.isWhitespace(document.getChar(offset + pos))) {
pos++;
}
return document.getChar(offset + pos) == search ? pos : -1;
} catch (BadLocationException e) {
}
return -1;
}
/**
* Gets the replacement length.
*
* @return Returns a int
*/
@Override
public final int getReplacementLength() {
if (!fReplacementLengthComputed)
setReplacementLength(fProposal.getReplaceEnd() - fProposal.getReplaceStart());
return super.getReplacementLength();
}
/**
* Sets the replacement length.
*
* @param replacementLength
* The replacementLength to set
*/
@Override
public final void setReplacementLength(int replacementLength) {
fReplacementLengthComputed = true;
super.setReplacementLength(replacementLength);
}
/*
* @seeorg.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal#
* needsLinkedMode()
*/
protected boolean needsLinkedMode() {
return false; // we handle it ourselves
}
private String computeReplacementString(String prefix) {
fReplacementStringComputed = true;
try {
// we should get the real constructor here
method = getProperMethod(guessingMethod);
if (alias != null || (hasParameters() && hasArgumentList())) {
return computeGuessingCompletion(prefix);
}
} catch (ModelException e) {
if (!e.isDoesNotExist()) {
PHPCorePlugin.log(e);
}
}
return prefix + super.getReplacementString();
}
/**
* if modelElement is an instance of FakeConstructor, we need to get the
* real constructor
*
* @param modelElement
* @return
*/
private IMethod getProperMethod(IMethod modelElement) {
if (modelElement instanceof FakeConstructor) {
FakeConstructor fc = (FakeConstructor) modelElement;
if (fc.getParent() instanceof AliasType) {
AliasType aliasType = (AliasType) fc.getParent();
alias = aliasType.getAlias();
if (aliasType.getParent() instanceof IType) {
fc = FakeConstructor.createFakeConstructor(null, (IType) aliasType.getParent(), false);
}
}
IType type = fc.getDeclaringType();
IMethod[] ctors = FakeConstructor.getConstructors(type, fc.isEnclosingClass());
// here we must make sure ctors[1] != null,
// it means there is an available FakeConstructor for ctors[0]
if (ctors != null && ctors.length == 2 && ctors[0] != null && ctors[1] != null) {
return ctors[0];
}
return fc;
}
return modelElement;
}
/**
* Returns <code>true</code> if the argument list should be inserted by the
* proposal, <code>false</code> if not.
*
* @return <code>true</code> when the proposal is not in javadoc nor within
* an import and comprises the parameter list
*/
protected boolean hasArgumentList() {
if (CompletionProposal.METHOD_NAME_REFERENCE == fProposal.getKind())
return false;
String completion = fProposal.getCompletion();
return !isInDoc() && completion.length() > 0;
}
@Override
protected boolean isValidPrefix(String prefix) {
initAlias();
String replacementString = null;
if (alias != null) {
replacementString = alias + LPAREN + RPAREN;
} else {
replacementString = super.getReplacementString();
}
return isPrefix(prefix, replacementString);
}
private void initAlias() {
alias = null;
if (method instanceof FakeConstructor) {
FakeConstructor fc = (FakeConstructor) method;
if (fc.getParent() instanceof AliasType) {
alias = ((AliasType) fc.getParent()).getAlias();
}
} else if (method instanceof AliasMethod) {
alias = ((AliasMethod) method).getAlias();
}
}
private boolean hasParameters() throws ModelException {
return method.getParameters() != null && hasNondefaultValues(method.getParameters());
}
private boolean hasNondefaultValues(IParameter[] parameters) {
for (int i = 0; i < parameters.length; i++) {
IParameter parameter = parameters[i];
if (parameter.getDefaultValue() == null) {
return true;
}
}
return false;
}
/**
* Creates the completion string. Offsets and Lengths are set to the offsets
* and lengths of the parameters.
*
* @param prefix
* completion prefix
* @return the completion string
* @throws ModelException
* if parameter guessing failed
*/
private String computeGuessingCompletion(String prefix) throws ModelException {
StringBuilder buffer = new StringBuilder(prefix);
appendMethodNameReplacement(buffer);
setCursorPosition(buffer.length());
// show method parameter names:
IParameter[] parameters = method.getParameters();
List<String> paramList = new ArrayList<>();
if (parameters != null) {
for (int i = 0; i < parameters.length; i++) {
IParameter parameter = parameters[i];
if (parameter.getDefaultValue() == null) {
paramList.add(parameter.getName());
}
}
}
char[][] parameterNames = new char[paramList.size()][];
for (int i = 0; i < paramList.size(); ++i) {
parameterNames[i] = paramList.get(i).toCharArray();
}
fChoices = guessParameters(parameterNames);
int count = fChoices.length;
int replacementOffset = getReplacementOffset();
for (int i = 0; i < count; i++) {
if (i != 0) {
buffer.append(COMMA);
}
ICompletionProposal proposal = fChoices[i][0];
String argument = proposal.getDisplayString();
Position position = fPositions[i];
position.setOffset(replacementOffset + buffer.length());
position.setLength(argument.length());
buffer.append(argument);
}
buffer.append(RPAREN);
return buffer.toString();
}
/**
* Appends everything up to the method name including the opening
* parenthesis.
* <p>
* In case of {@link CompletionProposal#METHOD_REF_WITH_CASTED_RECEIVER} it
* add cast.
* </p>
*
* @param buffer
* the string buffer
* @since 3.4
*/
protected void appendMethodNameReplacement(StringBuilder buffer) {
if (alias != null) {
buffer.append(alias);
buffer.append(LPAREN);
} else {
buffer.append(fProposal.getName());
buffer.append(LPAREN);
}
}
private ICompletionProposal[][] guessParameters(char[][] parameterNames) throws ModelException {
// find matches in reverse order. Do this because people tend to declare
// the variable meant for the last
// parameter last. That is, local variables for the last parameter in
// the method completion are more
// likely to be closer to the point of code completion. As an example
// consider a "delegation" completion:
//
// public void myMethod(int param1, int param2, int param3) {
// someOtherObject.yourMethod(param1, param2, param3);
// }
//
// The other consideration is giving preference to variables that have
// not previously been used in this
// code completion (which avoids
// "someOtherObject.yourMethod(param1, param1, param1)";
int count = parameterNames.length;
fPositions = new Position[count];
fChoices = new ICompletionProposal[count][];
IParameter[] parameters = method.getParameters();
for (int i = count - 1; i >= 0; i--) {
String paramName = new String(parameterNames[i]);
Position position = new Position(0, 0);
ICompletionProposal[] argumentProposals = parameterProposals(parameters[i].getDefaultValue(), paramName,
position, fFillBestGuess);
fPositions[i] = position;
fChoices[i] = argumentProposals;
}
return fChoices;
}
/*
* @see ICompletionProposal#getSelection(IDocument)
*/
@Override
public Point getSelection(IDocument document) {
if (fSelectedRegion == null)
return new Point(getReplacementOffset(), 0);
return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
}
private void openErrorDialog(Exception e) {
Shell shell = getTextViewer().getTextWidget().getShell();
MessageDialog.openError(shell, Messages.ParameterGuessingProposal_0, e.getMessage());
}
private void ensurePositionCategoryInstalled(final IDocument document, LinkedModeModel model) {
if (!document.containsPositionCategory(getCategory())) {
document.addPositionCategory(getCategory());
fUpdater = new InclusivePositionUpdater(getCategory());
document.addPositionUpdater(fUpdater);
model.addLinkingListener(new ILinkedModeListener() {
/*
* @see
* org.eclipse.jface.text.link.ILinkedModeListener#left(org.
* eclipse.jface.text.link.LinkedModeModel, int)
*/
@Override
public void left(LinkedModeModel environment, int flags) {
ensurePositionCategoryRemoved(document);
}
@Override
public void suspend(LinkedModeModel environment) {
}
@Override
public void resume(LinkedModeModel environment, int flags) {
}
});
}
}
private void ensurePositionCategoryRemoved(IDocument document) {
if (document.containsPositionCategory(getCategory())) {
try {
document.removePositionCategory(getCategory());
} catch (BadPositionCategoryException e) {
// ignore
}
document.removePositionUpdater(fUpdater);
}
}
private String getCategory() {
return "ParameterGuessingProposal_" + toString(); //$NON-NLS-1$
}
/**
* Returns the matches for the type and name argument, ordered by match
* quality.
*
* @param expectedType
* - the qualified type of the parameter we are trying to match
* @param paramName
* - the name of the parameter (used to find similarly named
* matches)
* @param pos
* @param suggestions
* the suggestions or <code>null</code>
* @param fillBestGuess
* @return returns the name of the best match, or <code>null</code> if no
* match found
* @throws JavaModelException
* if it fails
*/
public ICompletionProposal[] parameterProposals(String initialValue, String paramName, Position pos,
boolean fillBestGuess) throws ModelException {
List<String> typeMatches = new ArrayList<>();
if (initialValue != null) {
typeMatches.add(initialValue);
}
ICompletionProposal[] ret = new ICompletionProposal[typeMatches.size()];
int i = 0;
int replacementLength = 0;
for (Iterator<String> it = typeMatches.iterator(); it.hasNext();) {
String name = it.next();
if (i == 0) {
replacementLength = name.length();
}
final char[] triggers = new char[1];
triggers[triggers.length - 1] = ';';
ret[i++] = new PositionBasedCompletionProposal(name, pos, replacementLength, getImage(), name, null, null,
triggers);
}
if (!fillBestGuess) {
// insert a proposal with the argument name
ICompletionProposal[] extended = new ICompletionProposal[ret.length + 1];
System.arraycopy(ret, 0, extended, 1, ret.length);
extended[0] = new PositionBasedCompletionProposal(paramName, pos,
replacementLength/* paramName.length() */, null, paramName, null, null, NO_TRIGGERS);
return extended;
}
return ret;
}
@Override
public void setReplacementOffset(int replacementOffset) {
int oldReplacementOffset = getReplacementOffset();
if (fPositions != null && fPositions.length > 0) {
for (Position position : fPositions) {
position.offset = position.offset + (replacementOffset - oldReplacementOffset);
}
}
super.setReplacementOffset(replacementOffset);
}
@Override
public IModelElement getModelElement() {
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=469377
// be sure to return the "unchanged" method
return guessingMethod;
}
@Override
public Object getExtraInfo() {
return extraInfo;
}
}