/*******************************************************************************
* Copyright (c) 2000, 2009 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.wst.jsdt.internal.ui.text.java;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationExtension;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.eclipse.wst.jsdt.core.CompletionProposal;
import org.eclipse.wst.jsdt.core.IType;
import org.eclipse.wst.jsdt.core.JavaScriptModelException;
import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorHighlightingSynchronizer;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.wst.jsdt.ui.text.java.JavaContentAssistInvocationContext;
/**
* Proposal for generic types.
* <p>
* Only used when compliance is set to 5.0 or higher.
* </p>
*/
public final class LazyGenericTypeProposal extends LazyJavaTypeCompletionProposal {
/** Triggers for types. Do not modify. */
private final static char[] GENERIC_TYPE_TRIGGERS= new char[] { '.', '\t', '[', '(', '<', ' ' };
/**
* Short-lived context information object for generic types. Currently, these
* are only created after inserting a type proposal, as core doesn't give us
* the correct type proposal from within SomeType<|>.
*/
private static class ContextInformation implements IContextInformation, IContextInformationExtension {
private final String fInformationDisplayString;
private final String fContextDisplayString;
private final Image fImage;
private final int fPosition;
ContextInformation(LazyGenericTypeProposal proposal) {
// don't cache the proposal as content assistant
// might hang on to the context info
fContextDisplayString= proposal.getDisplayString();
fInformationDisplayString= computeContextString(proposal);
fImage= proposal.getImage();
fPosition= proposal.getReplacementOffset() + proposal.getReplacementString().indexOf('<') + 1;
}
/*
* @see org.eclipse.jface.text.contentassist.IContextInformation#getContextDisplayString()
*/
public String getContextDisplayString() {
return fContextDisplayString;
}
/*
* @see org.eclipse.jface.text.contentassist.IContextInformation#getImage()
*/
public Image getImage() {
return fImage;
}
/*
* @see org.eclipse.jface.text.contentassist.IContextInformation#getInformationDisplayString()
*/
public String getInformationDisplayString() {
return fInformationDisplayString;
}
private String computeContextString(LazyGenericTypeProposal proposal) {
try {
TypeArgumentProposal[] proposals= proposal.computeTypeArgumentProposals();
if (proposals.length == 0)
return null;
StringBuffer buf= new StringBuffer();
for (int i= 0; i < proposals.length; i++) {
buf.append(proposals[i].getDisplayName());
if (i < proposals.length - 1)
buf.append(", "); //$NON-NLS-1$
}
return buf.toString();
} catch (JavaScriptModelException e) {
return null;
}
}
/*
* @see org.eclipse.jface.text.contentassist.IContextInformationExtension#getContextInformationPosition()
*/
public int getContextInformationPosition() {
return fPosition;
}
/*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj) {
if (obj instanceof ContextInformation) {
ContextInformation ci= (ContextInformation) obj;
return getContextInformationPosition() == ci.getContextInformationPosition() && getInformationDisplayString().equals(ci.getInformationDisplayString());
}
return false;
}
}
private static final class TypeArgumentProposal {
private final boolean fIsAmbiguous;
private final String fProposal;
private final String fTypeDisplayName;
TypeArgumentProposal(String proposal, boolean ambiguous, String typeDisplayName) {
fIsAmbiguous= ambiguous;
fProposal= proposal;
fTypeDisplayName= typeDisplayName;
}
public String getDisplayName() {
return fTypeDisplayName;
}
boolean isAmbiguous() {
return fIsAmbiguous;
}
String getProposals() {
return fProposal;
}
public String toString() {
return fProposal;
}
}
private IRegion fSelectedRegion; // initialized by apply()
private TypeArgumentProposal[] fTypeArgumentProposals;
public LazyGenericTypeProposal(CompletionProposal typeProposal, JavaContentAssistInvocationContext context) {
super(typeProposal, context);
}
/*
* @see ICompletionProposalExtension#apply(IDocument, char)
*/
public void apply(IDocument document, char trigger, int offset) {
if (shouldAppendArguments(document, offset, trigger)) {
try {
TypeArgumentProposal[] typeArgumentProposals= computeTypeArgumentProposals();
if (typeArgumentProposals.length > 0) {
int[] offsets= new int[typeArgumentProposals.length];
int[] lengths= new int[typeArgumentProposals.length];
StringBuffer buffer= createParameterList(typeArgumentProposals, offsets, lengths);
// set the generic type as replacement string
boolean insertClosingParenthesis= trigger == '(' && autocloseBrackets();
if (insertClosingParenthesis)
updateReplacementWithParentheses(buffer);
super.setReplacementString(buffer.toString());
// add import & remove package, update replacement offset
super.apply(document, '\0', offset);
if (getTextViewer() != null) {
if (hasAmbiguousProposals(typeArgumentProposals)) {
adaptOffsets(offsets, buffer);
installLinkedMode(document, offsets, lengths, typeArgumentProposals, insertClosingParenthesis);
} else {
if (insertClosingParenthesis)
setUpLinkedMode(document, ')');
else
fSelectedRegion= new Region(getReplacementOffset() + getReplacementString().length(), 0);
}
}
return;
}
} catch (JavaScriptModelException e) {
// log and continue
JavaScriptPlugin.log(e);
}
}
// default is to use the super implementation
// reasons:
// - not a parameterized type,
// - already followed by <type arguments>
// - proposal type does not inherit from expected type
super.apply(document, trigger, offset);
}
/*
* @see org.eclipse.wst.jsdt.internal.ui.text.java.LazyJavaTypeCompletionProposal#computeTriggerCharacters()
*/
protected char[] computeTriggerCharacters() {
return GENERIC_TYPE_TRIGGERS;
}
/**
* Adapt the parameter offsets to any modification of the replacement
* string done by <code>apply</code>. For example, applying the proposal
* may add an import instead of inserting the fully qualified name.
* <p>
* This assumes that modifications happen only at the beginning of the
* replacement string and do not touch the type arguments list.
* </p>
*
* @param offsets the offsets to modify
* @param buffer the original replacement string
*/
private void adaptOffsets(int[] offsets, StringBuffer buffer) {
String replacementString= getReplacementString();
int delta= buffer.length() - replacementString.length(); // due to using an import instead of package
for (int i= 0; i < offsets.length; i++) {
offsets[i]-= delta;
}
}
/**
* Computes the type argument proposals for this type proposals. If there is
* an expected type binding that is a super type of the proposed type, the
* wildcard type arguments of the proposed type that can be mapped through
* to type the arguments of the expected type binding are bound accordingly.
* <p>
* For type arguments that cannot be mapped to arguments in the expected
* type, or if there is no expected type, the upper bound of the type
* argument is proposed.
* </p>
* <p>
* The argument proposals have their <code>isAmbiguos</code> flag set to
* <code>false</code> if the argument can be mapped to a non-wildcard type
* argument in the expected type, otherwise the proposal is ambiguous.
* </p>
*
* @return the type argument proposals for the proposed type
* @throws JavaScriptModelException if accessing the java model fails
*/
private TypeArgumentProposal[] computeTypeArgumentProposals() throws JavaScriptModelException {
if (fTypeArgumentProposals == null) {
IType type= (IType) getJavaElement();
if (type == null)
return new TypeArgumentProposal[0];
return new TypeArgumentProposal[0];
}
return fTypeArgumentProposals;
}
/**
* Returns <code>true</code> if type arguments should be appended when
* applying this proposal, <code>false</code> if not (for example if the
* document already contains a type argument list after the insertion point.
*
* @param document the document
* @param offset the insertion offset
* @param trigger the trigger character
* @return <code>true</code> if arguments should be appended
*/
private boolean shouldAppendArguments(IDocument document, int offset, char trigger) {
/*
* No argument list if there were any special triggers (for example a period to qualify an
* inner type).
*/
if (trigger != '\0' && trigger != '<' && trigger != '(')
return false;
/* No argument list if the completion is empty (already within the argument list). */
char[] completion= fProposal.getCompletion();
if (completion.length == 0)
return false;
/* No argument list if there already is a generic signature behind the name. */
try {
IRegion region= document.getLineInformationOfOffset(offset);
String line= document.get(region.getOffset(), region.getLength());
int index= offset - region.getOffset();
while (index != line.length() && Character.isUnicodeIdentifierPart(line.charAt(index)))
++index;
if (index == line.length())
return true;
char ch= line.charAt(index);
return ch != '<';
} catch (BadLocationException e) {
return true;
}
}
private StringBuffer createParameterList(TypeArgumentProposal[] typeArguments, int[] offsets, int[] lengths) {
StringBuffer buffer= new StringBuffer();
buffer.append(getReplacementString());
FormatterPrefs prefs= getFormatterPrefs();
final char LESS= '<';
final char GREATER= '>';
if (prefs.beforeOpeningBracket)
buffer.append(SPACE);
buffer.append(LESS);
if (prefs.afterOpeningBracket)
buffer.append(SPACE);
StringBuffer separator= new StringBuffer(3);
if (prefs.beforeTypeArgumentComma)
separator.append(SPACE);
separator.append(COMMA);
if (prefs.afterTypeArgumentComma)
separator.append(SPACE);
for (int i= 0; i != typeArguments.length; i++) {
if (i != 0)
buffer.append(separator);
offsets[i]= buffer.length();
buffer.append(typeArguments[i]);
lengths[i]= buffer.length() - offsets[i];
}
if (prefs.beforeClosingBracket)
buffer.append(SPACE);
buffer.append(GREATER);
return buffer;
}
private void installLinkedMode(IDocument document, int[] offsets, int[] lengths, TypeArgumentProposal[] typeArgumentProposals, boolean withParentheses) {
int replacementOffset= getReplacementOffset();
String replacementString= getReplacementString();
try {
LinkedModeModel model= new LinkedModeModel();
for (int i= 0; i != offsets.length; i++) {
if (typeArgumentProposals[i].isAmbiguous()) {
LinkedPositionGroup group= new LinkedPositionGroup();
group.addPosition(new LinkedPosition(document, replacementOffset + offsets[i], lengths[i]));
model.addGroup(group);
}
}
if (withParentheses) {
LinkedPositionGroup group= new LinkedPositionGroup();
group.addPosition(new LinkedPosition(document, replacementOffset + getCursorPosition(), 0));
model.addGroup(group);
}
model.forceInstall();
JavaEditor editor= getJavaEditor();
if (editor != null) {
model.addLinkingListener(new EditorHighlightingSynchronizer(editor));
}
LinkedModeUI ui= new EditorLinkedModeUI(model, getTextViewer());
ui.setExitPolicy(new ExitPolicy(withParentheses ? ')' : '>', document));
ui.setExitPosition(getTextViewer(), replacementOffset + replacementString.length(), 0, Integer.MAX_VALUE);
ui.setDoContextInfo(true);
ui.enter();
fSelectedRegion= ui.getSelectedRegion();
} catch (BadLocationException e) {
JavaScriptPlugin.log(e);
openErrorDialog(e);
}
}
private boolean hasAmbiguousProposals(TypeArgumentProposal[] typeArgumentProposals) {
boolean hasAmbiguousProposals= false;
for (int i= 0; i < typeArgumentProposals.length; i++) {
if (typeArgumentProposals[i].isAmbiguous()) {
hasAmbiguousProposals= true;
break;
}
}
return hasAmbiguousProposals;
}
/**
* Returns the currently active java editor, or <code>null</code> if it
* cannot be determined.
*
* @return the currently active java editor, or <code>null</code>
*/
private JavaEditor getJavaEditor() {
IEditorPart part= JavaScriptPlugin.getActivePage().getActiveEditor();
if (part instanceof JavaEditor)
return (JavaEditor) part;
else
return null;
}
/*
* @see ICompletionProposal#getSelection(IDocument)
*/
public Point getSelection(IDocument document) {
if (fSelectedRegion == null)
return super.getSelection(document);
return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
}
private void openErrorDialog(BadLocationException e) {
Shell shell= getTextViewer().getTextWidget().getShell();
MessageDialog.openError(shell, JavaTextMessages.FilledArgumentNamesMethodProposal_error_msg, e.getMessage());
}
/*
* @see org.eclipse.wst.jsdt.internal.ui.text.java.LazyJavaCompletionProposal#computeContextInformation()
*/
protected IContextInformation computeContextInformation() {
try {
if (hasParameters()) {
TypeArgumentProposal[] proposals= computeTypeArgumentProposals();
if (hasAmbiguousProposals(proposals))
return new ContextInformation(this);
}
} catch (JavaScriptModelException e) {
}
return super.computeContextInformation();
}
protected int computeCursorPosition() {
if (fSelectedRegion != null)
return fSelectedRegion.getOffset() - getReplacementOffset();
return super.computeCursorPosition();
}
private boolean hasParameters() {
IType type= (IType) getJavaElement();
if (type == null)
return false;
return false;
}
}