/* * Copyright 2009-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.eclipse.codeassist.completions; import org.codehaus.groovy.eclipse.codeassist.GroovyContentAssist; import org.codehaus.groovy.eclipse.codeassist.ProposalUtils; import org.codehaus.groovy.eclipse.codeassist.processors.GroovyCompletionProposal; import org.codehaus.groovy.eclipse.codeassist.proposals.ProposalFormattingOptions; import org.eclipse.jdt.core.CompletionContext; import org.eclipse.jdt.core.CompletionProposal; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.core.ImportContainer; import org.eclipse.jdt.internal.corext.template.java.SignatureUtil; import org.eclipse.jdt.internal.ui.JavaPlugin; import org.eclipse.jdt.internal.ui.javaeditor.EditorHighlightingSynchronizer; import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor; import org.eclipse.jdt.internal.ui.text.java.JavaCompletionProposal; import org.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal; import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IPositionUpdater; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.link.ILinkedModeListener; import org.eclipse.jface.text.link.InclusivePositionUpdater; 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.jface.text.link.ProposalPosition; import org.eclipse.jface.viewers.StyledString; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.texteditor.link.EditorLinkedModeUI; /** * An adaptation of {@link ParameterGuessingProposal} for Groovy code. */ public class GroovyJavaGuessingCompletionProposal extends JavaMethodCompletionProposal { /** * Creates a {@link ParameterGuessingProposal} or <code>null</code> if the * core context isn't available or extended. * * @param proposal the original completion proposal * @param context the current context * @param fillBestGuess if set, the best guess will be filled in * * @return a proposal or <code>null</code> */ public static GroovyJavaGuessingCompletionProposal createProposal(CompletionProposal proposal, JavaContentAssistInvocationContext context, boolean fillBestGuess, String contributor, ProposalFormattingOptions options) { CompletionContext coreContext = context.getCoreContext(); if (coreContext != null && coreContext.isExtended()) { return new GroovyJavaGuessingCompletionProposal(proposal, context, coreContext, fillBestGuess, contributor, options); } return null; } private ICompletionProposal[][] fChoices; // initialized by guessParameters() private Position[] fPositions; // initialized by guessParameters() private IRegion fSelectedRegion; // initialized by apply() private IPositionUpdater fUpdater; private final boolean fFillBestGuess; private final CompletionContext fCoreContext; private final ProposalFormattingOptions options; private final String contributor; private boolean methodPointer; private GroovyJavaGuessingCompletionProposal(CompletionProposal proposal, JavaContentAssistInvocationContext context, CompletionContext coreContext, boolean fillBestGuess, String contributor, ProposalFormattingOptions proposalOptions) { super(proposal, context); fCoreContext = coreContext; fFillBestGuess = fillBestGuess; this.contributor = contributor; this.options = proposalOptions; } @Override protected int computeRelevance() { // precomputed return fProposal.getRelevance(); } private IJavaElement getEnclosingElement() { return fCoreContext.getEnclosingElement(); } // includes the regular and the named parameters private String[] cachedVisibleParameterTypes = null; private String[] getParameterTypes() { if (cachedVisibleParameterTypes == null) { char[] signature = SignatureUtil.fix83600(fProposal.getSignature()); char[][] types = Signature.getParameterTypes(signature); String[] ret = new String[types.length]; for (int i = 0; i < types.length; i++) { ret[i] = new String(Signature.toCharArray(types[i])); } cachedVisibleParameterTypes = ret; } return cachedVisibleParameterTypes; } private IJavaElement[][] getAssignableElements() { // get the visible parameters (ie- the regular and named params // together) String[] parameterTypes = getParameterTypes(); IJavaElement[][] assignableElements = new IJavaElement[parameterTypes.length][]; for (int i = 0; i < parameterTypes.length; i++) { // hmmmm...I don't like all this back and forth between type names // and signatures String typeName = new String(parameterTypes[i]); assignableElements[i] = fCoreContext.getVisibleElements(Signature.createTypeSignature(typeName, true)); } return assignableElements; } /* * @see ICompletionProposalExtension#apply(IDocument, char) */ @Override public void apply(IDocument document, char trigger, int offset) { methodPointer = ProposalUtils.isMethodPointerCompletion(document, getReplacementOffset()); try { super.apply(document, trigger, offset); int baseOffset = getReplacementOffset(); String replacement = getReplacementString(); if (fPositions != null && getTextViewer() != null) { LinkedModeModel model = new LinkedModeModel(); for (int i = 0; i < fPositions.length; i++) { LinkedPositionGroup group = new LinkedPositionGroup(); int positionOffset = fPositions[i].getOffset(); 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); } model.forceInstall(); JavaEditor editor = getJavaEditor(); if (editor != null) { model.addLinkingListener(new EditorHighlightingSynchronizer(editor)); } LinkedModeUI ui = new EditorLinkedModeUI(model, getTextViewer()); ui.setExitPosition(getTextViewer(), baseOffset + replacement.length(), 0, Integer.MAX_VALUE); ui.setExitPolicy(new ExitPolicy(')', document)); ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT); ui.setDoContextInfo(true); ui.enter(); fSelectedRegion = ui.getSelectedRegion(); } else { fSelectedRegion = new Region(baseOffset + replacement.length(), 0); } } catch (BadLocationException e) { ensurePositionCategoryRemoved(document); GroovyContentAssist.logError(e); openErrorDialog(e); } catch (BadPositionCategoryException e) { ensurePositionCategoryRemoved(document); GroovyContentAssist.logError(e); openErrorDialog(e); } } /** * @see org.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal#needsLinkedMode() */ @Override protected boolean needsLinkedMode() { return false; // we handle it ourselves } @Override protected StyledString computeDisplayString() { return super.computeDisplayString().append(getStyledGroovy()); } private StyledString getStyledGroovy() { return new StyledString(" (" + contributor + ")", StyledString.DECORATIONS_STYLER); } /** * @see org.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal#computeReplacementString() */ @Override protected String computeReplacementString() { char[] proposalName = fProposal.getName(); boolean hasWhitespace = ProposalUtils.hasWhitespace(proposalName); if (methodPointer) { // complete the name only for a method pointer expression return String.valueOf(!hasWhitespace ? proposalName : CharOperation.concat('"', proposalName, '"')); } if (!hasParameters() || !hasArgumentList()) { if (options.noParens) { return String.valueOf(!hasWhitespace ? proposalName : CharOperation.concat('"', proposalName, '"')); } else { String replacementString = super.computeReplacementString(); if (replacementString.endsWith(");")) { replacementString = replacementString.substring(0, replacementString.length() - 1); } return replacementString; } } try { return computeGuessingCompletion(); } catch (JavaModelException e) { fPositions = null; fChoices = null; GroovyContentAssist.logError(e); openErrorDialog(e); } return super.computeReplacementString(); } /** * Creates the completion string. Offsets and Lengths are set to the offsets * and lengths of the * parameters. * * @return the completion string * @throws JavaModelException if parameter guessing failed */ private String computeGuessingCompletion() throws JavaModelException { StringBuffer buffer = new StringBuffer(); char[] proposalName = fProposal.getName(); boolean hasWhitespace = ProposalUtils.hasWhitespace(proposalName); char[] newProposalName = !hasWhitespace ? proposalName : CharOperation.concat('"', proposalName, '"'); fProposal.setName(newProposalName); appendMethodNameReplacement(buffer); fProposal.setName(proposalName); FormatterPrefs prefs = getFormatterPrefs(); if (options.noParens) { // eat the opening paren replace with a space if there isn't one already buffer.replace(buffer.length() - 1, buffer.length(), prefs.beforeOpeningParen ? "" : " "); } setCursorPosition(buffer.length()); // groovy doesn't require parens around closures if it is the last argument // If the option is set, then we follow that heuristic char[][] regularParameterTypes = ((GroovyCompletionProposal) fProposal).getRegularParameterTypeNames(); // check if the last regular parameter is a closure. If so, it must be // moved to the end boolean lastArgIsClosure = lastArgIsClosure(regularParameterTypes); int indexOfLastClosure = lastArgIsClosure ? regularParameterTypes.length - 1 : -1; char[][] namedParameterNames = ((GroovyCompletionProposal) fProposal).getNamedParameterNames(); char[][] regularParameterNames = ((GroovyCompletionProposal) fProposal).getRegularParameterNames(); int namedCount = namedParameterNames.length; int argCount = regularParameterNames.length; int allCount = argCount + namedCount; if (options.noParensAroundClosures) { // remove the opening paren only if there is a single closure parameter if (indexOfLastClosure == 0 && namedCount == 0) { buffer.replace(buffer.length() - 1, buffer.length(), ""); // add space if not already there // would be added by call to appendMethodNameReplacement if (!prefs.beforeOpeningParen) { buffer.append(SPACE); } } else { if (prefs.afterOpeningParen) buffer.append(SPACE); } } else { if (prefs.afterOpeningParen) buffer.append(SPACE); } // we don't want parameters for static import declarations if (fCoreContext.getEnclosingElement() != null && !(fCoreContext.getEnclosingElement().getParent() instanceof ImportContainer)) { // now add the parameters int replacementOffset = getReplacementOffset(); fChoices = guessParameters(namedParameterNames, regularParameterNames); for (int i = 0; i < allCount; i++) { if (i == indexOfLastClosure) { // handle the last closure separately continue; } char[] nextName; if (i < argCount) { nextName = regularParameterNames[i]; } else { nextName = namedParameterNames[i - argCount]; } if (i >= argCount || options.useNamedArguments) { buffer.append(nextName).append(":"); } // handle the value ICompletionProposal proposal = fChoices[i][0]; String argument = proposal.getDisplayString(); Position position = fPositions[i]; position.setOffset(replacementOffset + buffer.length()); position.setLength(argument.length()); // handle the "unknown" case where we only insert a proposal. if (proposal instanceof JavaCompletionProposal) { ((JavaCompletionProposal) proposal).setReplacementOffset(replacementOffset + buffer.length()); } buffer.append(argument); // check what to add after argument if (i == allCount - 1 || (i == allCount - 2 && i == indexOfLastClosure - 1)) { if (prefs.beforeClosingParen || options.noParens) { buffer.append(SPACE); } if (!options.noParens) { buffer.append(RPAREN); } } else if (i < allCount - 1) { if (prefs.beforeComma) buffer.append(SPACE); buffer.append(COMMA); if (prefs.afterComma) buffer.append(SPACE); } } // closures at the end are added in an idiomatic groovy way if (indexOfLastClosure >= 0) { if (allCount > 1) { if (!options.noParensAroundClosures) { buffer.append(COMMA); } buffer.append(SPACE); } Position position = fPositions[indexOfLastClosure]; position.setOffset(replacementOffset + buffer.length()); position.setLength(1); buffer.append("{"); if (!options.noParensAroundClosures) { buffer.append(" }"); buffer.append(RPAREN); } } } return buffer.toString(); } private boolean lastArgIsClosure(char[][] regularParameterTypes) { if (regularParameterTypes != null && regularParameterTypes.length > 0) { char[] lastArgType = regularParameterTypes[regularParameterTypes.length - 1]; // we should be comparing against a fully qualified type name, but // it is not always available // so a simple name is close enough return CharOperation.equals("Closure".toCharArray(), lastArgType); } else { // no args return false; } } /** * 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 = JavaPlugin.getActivePage().getActiveEditor(); if (part instanceof JavaEditor) return (JavaEditor) part; else return null; } private ICompletionProposal[][] guessParameters(char[][] firstParameterNames, char[][] secondParameterNames) throws JavaModelException { // 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)"; char[][] parameterNames = new char[firstParameterNames.length + secondParameterNames.length][]; System.arraycopy(firstParameterNames, 0, parameterNames, 0, firstParameterNames.length); System.arraycopy(secondParameterNames, 0, parameterNames, firstParameterNames.length, secondParameterNames.length); int count = parameterNames.length; fPositions = new Position[count]; fChoices = new ICompletionProposal[count][]; String[] parameterTypes = getParameterTypes(); IJavaElement[][] assignableElements = getAssignableElements(); for (int i = count - 1; i >= 0; i--) { String paramName = new String(parameterNames[i]); Position position = new Position(0, 0); ICompletionProposal[] argumentProposals = new ParameterGuesserDelegate(getEnclosingElement()).parameterProposals( parameterTypes[i], paramName, position, assignableElements[i], fFillBestGuess); if (argumentProposals.length == 0) argumentProposals = new ICompletionProposal[] { new JavaCompletionProposal(paramName, 0, paramName.length(), null, paramName, 0) }; 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, "Error guessing parameters for content assist proposal", 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) */ public void left(LinkedModeModel environment, int flags) { ensurePositionCategoryRemoved(document); } public void suspend(LinkedModeModel environment) {} 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$ } /** * Not API, for testing only. * * @return the guessed parameter proposals */ public ICompletionProposal[][] getChoices() { return fChoices; } }