/** * Copyright (c) 2013-2016 Angelo ZERR. * 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: * Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation */ package tern.eclipse.ide.ui.contentassist; import java.util.List; 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.ITextViewer; import org.eclipse.jface.text.Region; 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.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.text.link.ProposalPosition; import org.eclipse.swt.SWT; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.texteditor.link.EditorLinkedModeUI; import tern.ITernFile; import tern.ITernProject; import tern.eclipse.ide.core.IIDETernProject; import tern.eclipse.ide.internal.ui.Trace; import tern.eclipse.ide.ui.TernUIPlugin; import tern.eclipse.ide.ui.utils.HTMLTernPrinter; import tern.eclipse.jface.contentassist.TernCompletionProposal; import tern.server.TernPlugin; import tern.server.protocol.completions.FunctionInfo; import tern.server.protocol.completions.Parameter; import tern.server.protocol.completions.TernCompletionProposalRec; import tern.server.protocol.completions.TernTypeHelper; import tern.server.protocol.guesstypes.TernGuessTypesQuery; import tern.utils.StringUtils; /** * JavaScript tern completion proposal. * */ public class JSTernCompletionProposal extends TernCompletionProposal { public static final String TAB = "\t"; public static final String SPACE = " "; private static final String RPAREN = ")"; private static final String LPAREN = "("; private static final String COMMA = ","; private IRegion fSelectedRegion; // initialized by apply() private IPositionUpdater fUpdater; private Arguments arguments; private ITextViewer fTextViewer; private boolean fToggleEating; private boolean generateObjectValue; private boolean generateAnonymousFunction; private String indentChars; private ITernFile ternFile; public JSTernCompletionProposal(TernCompletionProposalRec proposal) { super(proposal); this.indentChars = TAB; } @Override protected Image getDefaultImage() { return TernUIPlugin.getTernDescriptorManager().getImage(this); } @Override public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) { IDocument document = viewer.getDocument(); if (fTextViewer == null) fTextViewer = viewer; // don't eat if not in preferences, XOR with modifier key 1 (Ctrl) // but: if there is a selection, replace it! Point selection = viewer.getSelectedRange(); fToggleEating = (stateMask & SWT.MOD1) != 0; int newLength = selection.x + selection.y - getReplacementOffset(); if ((insertCompletion() ^ fToggleEating) && newLength >= 0) setReplacementLength(newLength); apply(document, trigger, offset); fToggleEating = false; } @Override public void apply(IDocument document, char trigger, int offset) { // compute replacement string String replacement = computeReplacementString(document, offset); setReplacementString(replacement); updateReplacementLengthForString(document, offset, replacement); // apply the replacement. super.apply(document, trigger, offset); int baseOffset = getReplacementOffset(); if (arguments != null && getTextViewer() != null) { try { // adjust offset of the whole arguments arguments.setBaseOffset(baseOffset); // guess parameters if "guess-types" tern plugin is checked. guessParameters(offset); // Create group. Arg arg = null; LinkedModeModel model = new LinkedModeModel(); for (int i = 0; i != arguments.size(); i++) { arg = arguments.get(i); LinkedPositionGroup group = new LinkedPositionGroup(); if (arg.getProposals() == null) { group.addPosition(new LinkedPosition(document, arg .getOffset(), arg.getLength(), LinkedPositionGroup.NO_STOP)); } else { ensurePositionCategoryInstalled(document, model); document.addPosition(getCategory(), arg); group.addPosition(new ProposalPosition(document, arg .getOffset(), arg.getLength(), LinkedPositionGroup.NO_STOP, arg.getProposals())); } 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.setDoContextInfo(true); ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT); ui.enter(); fSelectedRegion = ui.getSelectedRegion(); } catch (BadLocationException e) { ensurePositionCategoryRemoved(document); // JavaScriptPlugin.log(e); // openErrorDialog(e); } catch (BadPositionCategoryException e) { ensurePositionCategoryRemoved(document); // JavaScriptPlugin.log(e); // openErrorDialog(e); } } else { int newOffset = baseOffset + replacement.length(); if (isObjectKey() && TernTypeHelper.isStringType(getType())) { // select cursor inside quote of property value (ex : config: // "") newOffset--; } fSelectedRegion = new Region(newOffset, 0); } } protected void guessParameters(int offset) { ITernProject ternProject = super.getTernProject(); if (ternProject != null && ternProject.hasPlugin(TernPlugin.guess_types)) { String property = super.getName(); TernGuessTypesQuery query = new TernGuessTypesQuery( ternFile.getFileName(), offset, property); try { ternProject.request(query, ternFile, arguments); } catch (Exception e) { Trace.trace(Trace.SEVERE, "Error while guessing type", e); } } } /** * Compute new replacement length for string replacement. * * @param document * @param offset * @param replacement */ protected void updateReplacementLengthForString(IDocument document, int offset, String replacement) { boolean isString = replacement.startsWith("\"") || replacement.startsWith("'"); if (isString) { int length = document.getLength(); int pos = offset; char c; while (pos < length) { try { c = document.getChar(pos); switch (c) { case '\r': case '\n': case '\t': case ' ': return; case '"': case '\'': setReplacementLength(getReplacementLength() + pos - offset + 1); return; } ++pos; } catch (BadLocationException e) { e.printStackTrace(); } } } } @Override public Point getSelection(IDocument document) { if (fSelectedRegion == null) return new Point(getReplacementOffset(), 0); return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength()); } public ITextViewer getTextViewer() { return fTextViewer; } /** * Gets the replacement string. * * @return Returns a String */ @Override public final String getReplacementString() { // if (!fReplacementStringComputed) // setReplacementString(computeReplacementString()); return super.getReplacementString(); } private String computeReplacementString(IDocument document, int offset) { if (isObjectKey()) { StringBuilder replacement = new StringBuilder(super.getName()); replacement.append(": "); if (TernTypeHelper.isStringType(getType())) { replacement.append("\"\""); } // else if ("number".equals(getType())) { // replacement.append("0"); // } else if (TernTypeHelper.isBoolType(getType())) { replacement.append("false"); } return replacement.toString(); } List<Parameter> parameters = super.getParameters(); if (parameters == null) { return super.getReplacementString(); } String indentation = getIndentation(document, offset); arguments = new Arguments(getTernProject()); StringBuilder replacement = new StringBuilder(super.getName()); replacement.append(LPAREN); setCursorPosition(replacement.length()); computeReplacementString(parameters, replacement, arguments, indentation, 1, true); replacement.append(RPAREN); return replacement.toString(); } /** * Compute replacement string for the given function. * * @param parameters * @param replacement * @param arguments * @param indentation * @param nbIndentations * @param initialFunction */ private void computeReplacementString(List<Parameter> parameters, StringBuilder replacement, Arguments arguments, String indentation, int nbIndentations, boolean initialFunction) { int count = parameters.size(); Parameter parameter = null; String paramName = null; for (int i = 0; i != count; i++) { parameter = parameters.get(i); if (i != 0) { // if (prefs.beforeComma) // buffer.append(SPACE); replacement.append(COMMA); // if (prefs.afterComma) replacement.append(SPACE); } if (parameter.isFunction() && isGenerateAnonymousFunction() && initialFunction) { FunctionInfo info = parameter.getInfo(); List<Parameter> parametersOfParam = info.getParameters(); replacement.append("function("); if (parametersOfParam != null) { computeReplacementString(parametersOfParam, replacement, arguments, indentation, nbIndentations + 1, false); } else { // to select focus inside the () of generated inline // function arguments.addArg(replacement.length(), 0); } replacement.append(") {"); replacement.append("\n"); indent(replacement, indentation, nbIndentations); indent(replacement); if (!StringUtils.isEmpty(info.getReturnType())) { if (TernTypeHelper.isStringType(info.getReturnType())) { replacement.append("return \"\";"); } else if (TernTypeHelper.isBoolType(info.getReturnType())) { replacement.append("return true;"); } else if ("{}".equals(info.getReturnType())) { replacement.append("return {"); replacement.append("\n"); indent(replacement, indentation, nbIndentations); indent(replacement); indent(replacement); // to select focus inside the {} of generated return // statement of the function. arguments.addArg(replacement.length(), 0); replacement.append("\n"); indent(replacement, indentation, nbIndentations); indent(replacement); replacement.append("}"); } } else { // to select focus inside the {} of generated inline // function arguments.addArg(replacement.length(), 0); } replacement.append("\n"); indent(replacement, indentation, nbIndentations); replacement.append("}"); } else { if ("{}".equals(parameter.getType()) && isGenerateObjectValue() && initialFunction) { replacement.append("{"); replacement.append("\n"); indent(replacement, indentation, nbIndentations); replacement.append("}"); } else { int offset = replacement.length(); paramName = parameter.getName(); // to select focus for parameter replacement.append(paramName); if (initialFunction) { arguments.addParameter(offset, paramName.length(), paramName, i); } else { arguments.addArg(offset, paramName.length()); } } } } /* * if (!hasParameters() || !hasArgumentList()) return * super.computeReplacementString(); * * char[][] parameterNames = fProposal.findParameterNames(null); int * count = parameterNames.length; fArgumentOffsets = new int[count]; * fArgumentLengths = new int[count]; StringBuilder buffer = new * StringBuilder(String.valueOf(fProposal .getName())); * * FormatterPrefs prefs = getFormatterPrefs(); if * (prefs.beforeOpeningParen) buffer.append(SPACE); * buffer.append(LPAREN); * * setCursorPosition(buffer.length()); * * if (prefs.afterOpeningParen) buffer.append(SPACE); * * for (int i = 0; i != count; i++) { if (i != 0) { if * (prefs.beforeComma) buffer.append(SPACE); buffer.append(COMMA); if * (prefs.afterComma) buffer.append(SPACE); } * * fArgumentOffsets[i] = buffer.length(); * buffer.append(parameterNames[i]); fArgumentLengths[i] = * parameterNames[i].length; } * * if (prefs.beforeClosingParen) buffer.append(SPACE); * * buffer.append(RPAREN); * * return buffer.toString(); */ } protected void indent(StringBuilder replacement) { replacement.append(indentChars); } @Override public String getAdditionalProposalInfo() { return HTMLTernPrinter.getAdditionalProposalInfo(this, null); } protected static final class ExitPolicy implements IExitPolicy { final char fExitCharacter; private final IDocument fDocument; public ExitPolicy(char exitCharacter, IDocument document) { fExitCharacter = exitCharacter; fDocument = document; } public ExitFlags doExit(LinkedModeModel environment, VerifyEvent event, int offset, int length) { if (event.character == fExitCharacter) { if (environment.anyPositionContains(offset)) return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false); else return new ExitFlags(ILinkedModeListener.UPDATE_CARET, true); } switch (event.character) { case ';': return new ExitFlags(ILinkedModeListener.NONE, true); case SWT.CR: // when entering an anonymous class as a parameter, we don't // want // to jump after the parenthesis when return is pressed if (offset > 0) { try { if (fDocument.getChar(offset - 1) == '{') return new ExitFlags(ILinkedModeListener.EXIT_ALL, true); } catch (BadLocationException e) { } } return null; default: return null; } } } @Override protected Shell getActiveWorkbenchShell() { return TernUIPlugin.getActiveWorkbenchShell(); } public boolean isGenerateObjectValue() { return generateObjectValue; } public void setGenerateObjectValue(boolean generateObjectValue) { this.generateObjectValue = generateObjectValue; } /** * Returns true if anonymous function must be generated and false otherwise. * * @return true if anonymous function must be generated and false otherwise. */ public boolean isGenerateAnonymousFunction() { return generateAnonymousFunction; } /** * Set true if anonymous function must be generated and false otherwise. * * @param generateAnonymousFunction */ public void setGenerateAnonymousFunction(boolean generateAnonymousFunction) { this.generateAnonymousFunction = generateAnonymousFunction; } /** * Returns true if completion must be inserted and false otheriwse. * * @return true if completion must be inserted and false otheriwse. */ private boolean insertCompletion() { // TODO : manage that with preferences return true; } /** * Indent the given replacement with indentation * nbIndentations. * * @param replacement * the buffer ton indent. * @param indentation * the indentation composed by \t and spaces. * @param nbIndentations * number of indentation. */ private void indent(StringBuilder replacement, String indentation, int nbIndentations) { for (int j = 0; j < nbIndentations; j++) { replacement.append(indentation); } } /** * Returns the indentation characters from the given line. * * @param document * @param offset * @return the indentation characters from the given line. */ private String getIndentation(IDocument document, int offset) { try { IRegion lineRegion = document.getLineInformationOfOffset(offset); String lineText = document.get(lineRegion.getOffset(), lineRegion.getLength()); StringBuilder indentation = new StringBuilder(); char[] chars = lineText.toCharArray(); char c; for (int i = 0; i < chars.length; i++) { c = chars[i]; if (c == ' ' || c == '\t') { indentation.append(c); } else { break; } } return indentation.toString(); } catch (BadLocationException e1) { } return ""; } public void setIndentChars(String indentChars) { this.indentChars = indentChars; } public String getIndentChars() { return indentChars; } public void setTernFile(ITernFile ternFile) { this.ternFile = ternFile; } public ITernFile getTernFile() { return ternFile; } public void setTernProject(IIDETernProject ternProject) { super.setTernProject(ternProject); } 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 "JSTernCompletionProposal_" + toString(); //$NON-NLS-1$ } }