/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain other free and open source software ("FOSS") code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.ide.editor.js.contentassist; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import org.eclipse.core.runtime.Path; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ContextInformation; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContentAssistProcessor; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationValidator; import org.eclipse.swt.graphics.Image; import com.aptana.ide.core.StringUtils; import com.aptana.ide.core.ui.CoreUIUtils; import com.aptana.ide.editor.js.JSLanguageEnvironment; import com.aptana.ide.editor.js.JSOffsetMapper; import com.aptana.ide.editor.js.environment.JSGuessedObject; import com.aptana.ide.editor.js.lexing.JSTokenTypes; import com.aptana.ide.editor.js.parsing.JSMimeType; import com.aptana.ide.editor.js.runtime.Environment; import com.aptana.ide.editor.js.runtime.IFunction; import com.aptana.ide.editor.js.runtime.IObject; import com.aptana.ide.editor.js.runtime.IScope; import com.aptana.ide.editor.js.runtime.JSScope; import com.aptana.ide.editor.js.runtime.JSUndefined; import com.aptana.ide.editor.js.runtime.ObjectBase; import com.aptana.ide.editor.js.runtime.Property; import com.aptana.ide.editor.scriptdoc.ScriptDocHelper; import com.aptana.ide.editor.scriptdoc.parsing.FunctionDocumentation; import com.aptana.ide.editor.scriptdoc.parsing.PropertyDocumentation; import com.aptana.ide.editor.scriptdoc.parsing.TypedDescription; import com.aptana.ide.editors.UnifiedEditorsPlugin; import com.aptana.ide.editors.managers.FileContextManager; import com.aptana.ide.editors.unified.EditorFileContext; import com.aptana.ide.editors.unified.IFileLanguageService; import com.aptana.ide.editors.unified.IUnifiedViewer; import com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistProcessor; import com.aptana.ide.editors.unified.contentassist.UnifiedContentAssistProcessor; import com.aptana.ide.lexer.Lexeme; import com.aptana.ide.lexer.LexemeList; import com.aptana.ide.lexer.TokenCategories; import com.aptana.ide.metadata.IDocumentation; import com.aptana.ide.parsing.IOffsetMapper; /** * */ public class JSContentAssistProcessor implements IContentAssistProcessor, IUnifiedContentAssistProcessor { private IContextInformationValidator validator; boolean initalPopup = false; private static String[] keywords = new String[] { "break", "case", "catch", "continue", "default", "delete", "do", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "else", "eval", "false", "field", "finally", "for", "function", "if", "in", "instanceof", "new", "null", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$ //$NON-NLS-11$ //$NON-NLS-12$ "return", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "while", "with" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$ //$NON-NLS-11$ // private static String[] futureKeywords = new String[]{"abstract", "boolean", "byte", "char", "class", "const", // "debugger", "double", "enum", "export", "extends", "final", "float", "goto", "implements", "import", "int", // "interface", "long", "native", "package", "private", "protected", "public", "short", "static", "super", // "synchronized", "throws", "transient", "volatile" }; // icons private static Image fIconField = UnifiedEditorsPlugin.getImage("icons/field_public.gif"); //$NON-NLS-1$ private static Image fIconObject = UnifiedEditorsPlugin.getImage("icons/object.gif"); //$NON-NLS-1$ private static Image fIconClass = UnifiedEditorsPlugin.getImage("icons/class.gif"); //$NON-NLS-1$ private static Image fIconFunction = UnifiedEditorsPlugin.getImage("icons/function.gif"); //$NON-NLS-1$ private static Image fIconFieldGuessed = UnifiedEditorsPlugin.getImage("icons/field_public_guess.gif"); //$NON-NLS-1$ private static Image fIconObjectGuessed = UnifiedEditorsPlugin.getImage("icons/object_guess.gif"); //$NON-NLS-1$ private static Image fIconClassGuessed = UnifiedEditorsPlugin.getImage("icons/class_guess.gif"); //$NON-NLS-1$ private static Image fIconFunctionGuessed = UnifiedEditorsPlugin.getImage("icons/function_guess.gif"); //$NON-NLS-1$ private static Image fIconError = UnifiedEditorsPlugin.getImage("icons/error.gif"); //$NON-NLS-1$ private static Image fIconKeyword = UnifiedEditorsPlugin.getImage("icons/keyword.gif"); //$NON-NLS-1$ private static Image fIconConstant = UnifiedEditorsPlugin.getImage("icons/constant.gif"); //$NON-NLS-1$ // private Image fIconDefaultValue = getImageDescriptor("icons/defaultValue.gif").createImage(); private JSCompletionProposalComparator contentAssistComparator; private int _offset; private static final char[] fContextChars = new char[] { '(', ',' }; private EditorFileContext context; private boolean forceActivated = false; // { '(', '.', ' ', '\b' }; // combine fCompletionChars and fContextChars // manually for now /** * Provides code assist information for javascript. * * @param context */ public JSContentAssistProcessor(EditorFileContext context) { this.context = context; validator = new JSContextInformationValidator(this); contentAssistComparator = new JSCompletionProposalComparator(); } private Environment getEnvironment() { return (Environment) JSLanguageEnvironment.getInstance().getRuntimeEnvironment(); } /** * @param viewer * @param offset * @return ICompletionProposal[] * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, * int) */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { return computeCompletionProposals(viewer, offset, UnifiedContentAssistProcessor.DEFAULT_CHARACTER); } /** * @see com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, * int, char) */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset, char activationChar) { return computeCompletionProposals(viewer, offset, activationChar, false); } /** * @see com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, * int, char, boolean) */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset, char activationChar, boolean autoActivated) { char previousCharacter = getActivationChar(viewer, offset); IUnifiedViewer unifiedViewer = null; if (viewer instanceof IUnifiedViewer) { unifiedViewer = (IUnifiedViewer) viewer; } JSOffsetMapper mapper = (JSOffsetMapper) getOffsetMapper(); // test for whitespace to left int lexemeOffset = mapper.getLexemeIndexFromDocumentOffset(offset); Lexeme curLexeme = null; if (lexemeOffset != -1) { curLexeme = mapper.getLexemeAtIndex(lexemeOffset); } else { curLexeme = mapper.getCurrentLexeme(); } String fullName = ""; //$NON-NLS-1$ if (curLexeme != null && curLexeme.getEndingOffset() >= offset) { // lookup current full name fullName = mapper.getNameHash(mapper.getLexemeList().getLexemeIndex(curLexeme)); } // if(activationChar != UnifiedContentAssistProcessor.DEFAULT_CHARACTER && "".equals(fullName)) // return null; // for now just ignore CA inside strings // todo: check if this is an invocation with default values, if so adjust lens etc to be correct if (fullName.equals(JSOffsetMapper.MODE_STRING)) { return null; } if (fullName.endsWith("]") || fullName.endsWith(")")) //$NON-NLS-1$ //$NON-NLS-2$ { return null; } // String argAssistFullName = mapper.getArgAssistNameHash(); _offset = offset; // figure out where we want and dont want code assist to appear // first filter pass is only name based, we later can filter further // based on objects if (fullName.equals(JSOffsetMapper.NOT_AN_IDENTIFIER)) { return null; } // test if we are in an invoke // boolean invoking = !argAssistFullName.equals(JSOffsetMapper.NOT_INVOKING); /* * if ((activationChar == '\0') && invoking) { // show arg assist instead if (viewer instanceof SourceViewer) { * ((SourceViewer) viewer).doOperation(SourceViewer.CONTENTASSIST_CONTEXT_INFORMATION); // If we don't want to * allow both code assist and arg assist at once, return here. // return null; } } */ // test conditions around space activation for 'new' boolean isConstructor = fullName.startsWith(JSOffsetMapper.MODE_NEW); if (isConstructor) { fullName = fullName.substring(JSOffsetMapper.MODE_NEW.length()); } if (previousCharacter == ' ' && !isConstructor) { return null; } // abort if current character isn't space when in 'new' mode // if( isConstructor && jsFileEnvironment.getSource().charAt(offset - 1) != ' ' ) // return null; // now we can continue with regular programming // lookup types of object (function calls must be based on return types // from docs) // until getting a type to find properties from IObject obj; IScope scope = mapper.getGlobal(); // LexemeList ll =getLanguageEnvironment().getLexemeList(); // int lxIndex = ll.getLexemeFloorIndex(offset); // if(lxIndex == -1) // lxIndex = ll.getLexemeCeilingIndex(offset); // Lexeme lex = ll.get(lxIndex); lexemeOffset = this.getJSOffsetMapper().getLexemeIndexFromDocumentOffset(offset); Lexeme lex = null; if (lexemeOffset != -1) { lex = mapper.getLexemeAtIndex(lexemeOffset); } else { lex = mapper.getCurrentLexeme(); } if (lex != null) { if (lex.getCategoryIndex() == TokenCategories.WHITESPACE) { int prevLexeme = mapper.getCurrentLexemeIndex() - 1; // JSLexeme[] lexemes = jsFileEnvironment.getLexemes(); LexemeList lexemeList = mapper.getLexemeList(); while (prevLexeme > 0 && prevLexeme < lexemeList.size()) { lex = lexemeList.get(prevLexeme); if (lex.getCategoryIndex() != TokenCategories.WHITESPACE) { break; } prevLexeme++; } } synchronized (mapper) { scope = mapper.getScope(lex, scope); } } // calculate our prefix and base objects String prefix = ""; //$NON-NLS-1$ if (!fullName.equals("") && fullName.indexOf(".") > -1) //$NON-NLS-1$ //$NON-NLS-2$ { // if there are letters after the last dot, that becomes our prefix // that filters competion options. String lookupName = fullName; if (fullName.indexOf(".") > -1) //$NON-NLS-1$ { lookupName = fullName.substring(0, fullName.lastIndexOf(".") + 1); //$NON-NLS-1$ } // find the final return type // there is a special ca.se here for things like x = new Date().now // in this case we must create the type new Date(), and look up 'now' on that obj = mapper.lookupReturnTypeFromNameHash(lookupName, scope); if (obj == null) { return null; } // set our prefix string to filter with prefix = fullName.substring(fullName.lastIndexOf('.') + 1).toUpperCase(); } else { obj = scope; // jsFileEnvironment.getGlobal(); prefix = fullName.toUpperCase(); } if (prefix.indexOf('(') > -1) { prefix = ""; //$NON-NLS-1$ } ICompletionProposal[] result = null; if (curLexeme != null && curLexeme.typeIndex == JSTokenTypes.STRING) { result = getValidCompletionProposals(obj, curLexeme.getText(), curLexeme.getText(), isConstructor, offset); } else { // calculate all the completion proposals result = getValidCompletionProposals(obj, fullName, prefix, isConstructor, offset); } UnifiedContentAssistProcessor.resetViewerState(unifiedViewer); // we don't need completion proposals when backspacing into a valid object that is // the only valid one, and it is completly typed. // However if we hit ctrlspace in a valid ident, it is nice to get all // the replacement proposals to check things if (result.length == 1 && prefix.equals(result[0].getDisplayString().toUpperCase())) { return getValidCompletionProposals(obj, fullName, "", isConstructor, prefix, offset); //$NON-NLS-1$ } else { return result; } } /** * These are default values that appear in code assist depending on the function inserted. * * @param viewer * @param offset */ private void addDefaultValues(String prefix, Hashtable completionProposals, int beginOffset) { initalPopup = true; // lookup the current full name String fullName = getFullName(); // ...and find the appropriate object based on the return types of the // objects in the sequence IScope scope = getScope(); IObject obj = getObject(fullName, scope); if (obj == null) { return; } IDocumentation doc = obj.getDocumentation(); if (doc instanceof FunctionDocumentation) { FunctionDocumentation fDoc = (FunctionDocumentation) doc; TypedDescription[] params = fDoc.getParams(); if (params.length == 1) { TypedDescription[] defaultValues = params[0].getDefaultValues(); for (int i = 0; i < defaultValues.length; i++) { TypedDescription d = defaultValues[i]; String name = "\"" + d.getName() + "\""; //$NON-NLS-1$ //$NON-NLS-2$ Image defaultImage = fIconConstant; String location = "String Param"; //$NON-NLS-1$ Image[] images = UnifiedContentAssistProcessor.getUserAgentImages(UnifiedContentAssistProcessor .getUserAgents(), fDoc.getUserAgentPlatformNames()); JSCompletionProposal cp = new JSCompletionProposal(name, beginOffset, prefix.length(), name .length(), defaultImage, name, null, d.getDescription(), JSCompletionProposalComparator.OBJECT_TYPE_GLOBAL_OBJECT, location, images); completionProposals.put(name, cp); } } } } /** * @param fullName * @param scope * @return IObject */ private IObject getObject(String fullName, IScope scope) { IObject obj = getJSOffsetMapper().lookupReturnTypeFromNameHash(fullName, scope); return obj; } /** * @return IScope */ private IScope getScope() { JSOffsetMapper mapper = getJSOffsetMapper(); Lexeme curLex = mapper.getCurrentLexeme(); IScope scope = null; if (curLex != null) { scope = mapper.getScope(curLex, mapper.getGlobal()); } // temp guard against ws nodes if (scope == null) { scope = getJSOffsetMapper().getGlobal(); } return scope; } /** * @return String */ private String getFullName() { String fullName = getJSOffsetMapper().getArgAssistNameHash(); if (fullName.indexOf('(') > -1) { fullName = fullName.substring(0, fullName.lastIndexOf('(')); } return fullName; } /** * Determines what 'tooltip' style highlighting to show. This will be argument insight for methods. This is * formatted in JSContextInformationValidator (of all places) by implementing the optional * IContextInformationPresenter interface. * * @param viewer * @param offset * @return Returns an array of relevant context info. */ public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { initalPopup = true; // lookup the current full name String fullName = getFullName(); // ...and find the appropriate object based on the return types of the // objects in the sequence IScope scope = getScope(); IObject obj = getObject(fullName, scope); IContextInformation[] ici = null; ContextInformation ci = computeArgContextInformation(obj, fullName); if (ci != null) { ici = new IContextInformation[] { ci }; } return ici; } /** * The characters that triggers competion proposals (a combination of all activation chars) * * @return Returns the trigger characters for code completion. */ public char[] getCompletionProposalAllActivationCharacters() { char[] allActivationChars = UnifiedContentAssistProcessor.combine( getCompletionProposalAutoActivationCharacters(), getContextInformationAutoActivationCharacters()); Arrays.sort(allActivationChars); return allActivationChars; } /** * The characters that triggers competion proposals (dot for completion, and space for "new XX" in our case) * * @return Returns the trigger characters for code completion. */ public char[] getCompletionProposalAutoActivationCharacters() { return new char[] { '.' }; } /** * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#getContextInformationValidator() */ public IContextInformationValidator getContextInformationValidator() { return validator; } /** * Characters that trigger tooltip popup help * * @return Returns the trigger characters for auto activation. */ public char[] getContextInformationAutoActivationCharacters() { // Make context popup automatically after the following characters return fContextChars; } /** * @see com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistProcessor#getCompletionProposalIdleActivationTokens() */ public int[] getCompletionProposalIdleActivationTokens() { return new int[] { JSTokenTypes.IDENTIFIER }; } /** * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#getErrorMessage() */ public String getErrorMessage() { return null; } /** * Computes argument insight text based on the passed hash name to lookup. * * @param fullName * The hash name of the method to lookup * @param identifier * The "name" of the item * @return A valid ContextInformation object or null. */ private ContextInformation computeArgContextInformation(IObject obj, String identifier) { ContextInformation result = null; StringBuffer docText = new StringBuffer(); // if the object exists, find its documentation if (obj != null) { // int argIndex = getJSOffsetMapper().getArgIndexAndCalculateMode(); IDocumentation idoc = obj.getDocumentation(); if (idoc != null) { // look for params and create a string, if it is a method if (idoc instanceof FunctionDocumentation) { FunctionDocumentation md = (FunctionDocumentation) idoc; TypedDescription[] params = md.getParams(); docText.append(ScriptDocHelper.createMethodSignatureString(identifier, md, obj, false)); if (params.length > 0) { String paramText = ScriptDocHelper.createParameterDocumentationList(params, obj); if (paramText != null && !"".equals(paramText)) //$NON-NLS-1$ { docText.append(ScriptDocHelper.DEFAULT_DELIMITER); docText.append(paramText); } } } // otherwise just add the description (properties, classes, // exceptions) else if (idoc instanceof PropertyDocumentation) { PropertyDocumentation pDoc = (PropertyDocumentation) idoc; docText.append(ScriptDocHelper.createPropertyDocumentationHTML(identifier, pDoc, false)); } } else if (obj instanceof IFunction) { // no docs, just pass arg info if this is a function String[] params = ((IFunction) obj).getParameterNames(); if (params != null && params.length > 0) { docText.append("("); //$NON-NLS-1$ String comma = ""; //$NON-NLS-1$ for (int i = 0; i < params.length; i++) { docText.append(comma + params[i]); comma = ", "; //$NON-NLS-1$ } docText.append(")"); //$NON-NLS-1$ } } String additionalInfo = ((docText.length() == 0) && (obj instanceof IFunction)) ? Messages.JSContentAssistProcessor_NoArgs : docText.toString(); if (additionalInfo.trim().length() > 0) { result = new ContextInformation("argInfo", "" + additionalInfo); //$NON-NLS-1$ //$NON-NLS-2$ } } return result; } private char getActivationChar(ITextViewer viewer, int offset) { if (JSContentAssistant.isHotkeyActivated()) { JSContentAssistant.setHotkeyActivated(false); return '\0'; } String source = viewer.getDocument().get(); if (offset == 0) { return '\0'; } char activationCharacter = source.charAt(offset - 1); // this is a special case as we are using backspace as an activation char // this can't come from the document // possibly we can store the deletion length from // JSPartitionScanner.documentChanged2 if (Arrays.binarySearch(getCompletionProposalAllActivationCharacters(), activationCharacter) < 0) { activationCharacter = UnifiedContentAssistProcessor.DEFAULT_CHARACTER; } return activationCharacter; } /** * Returns a set of valid completion proposals for a given object and prefix. * * @param obj * The object to look for proposals * @param prefix * The valid starting letters to complete with. * @param isConstructor * True if constructors are valid (like when using 'new'). * @return Returns an array of (JS) completion proposals. */ private ICompletionProposal[] getValidCompletionProposals(IObject obj, String fullName, String prefix, boolean isConstructor, int docOffset) { return getValidCompletionProposals(obj, fullName, prefix, isConstructor, "", docOffset); //$NON-NLS-1$ } /** * Returns a set of valid completion proposals for a given object and prefix. * * @param baseObject * The object to look for proposals * @param fullName * The hashName of the object. * @param prefix * The valid starting letters to complete with. * @param isConstructor * True if constructors are valid (like when using 'new'). * @param the * replace string, this gives us full control over what will be replaced in the case where we aren't * matching by ident prefix. * @return Returns an array of (JS) completion proposals. */ private ICompletionProposal[] getValidCompletionProposals(IObject baseObject, String fullName, String prefix, boolean isConstructor, String replaceString, int docOffset) { if (baseObject == null) { return new ICompletionProposal[0]; } boolean isNamedGlobal = baseObject.equals(getJSOffsetMapper().getGlobal()) && fullName.indexOf('.') != -1; int fileIndex = this.getJSOffsetMapper().getFileIndex(); int offset = 0; int lexemeOffset = this.getJSOffsetMapper().getLexemeIndexFromDocumentOffset(docOffset); Lexeme curLexeme = null; if (lexemeOffset != -1) { curLexeme = this.getJSOffsetMapper().getLexemeAtIndex(lexemeOffset); } else { curLexeme = this.getJSOffsetMapper().getCurrentLexeme(); } if (curLexeme != null) { offset = curLexeme.offset; } // boolean isPrefixReplace = false; if (replaceString.equals("")) //$NON-NLS-1$ { replaceString = prefix; // isPrefixReplace = true; } int lastDot = fullName.lastIndexOf('.'); boolean isRootLevelCall = (lastDot > -1) ? false : true; // get all members and then filter Hashtable completionProposals = new Hashtable(); int beginOffset = _offset; if (!prefix.equals("")) //$NON-NLS-1$ { boolean isText = (curLexeme.getCategoryIndex() == TokenCategories.IDENTIFIER || curLexeme .getCategoryIndex() == TokenCategories.KEYWORD); beginOffset = isText ? curLexeme.offset : curLexeme.getEndingOffset() - prefix.length(); } // add all keywords if (isRootLevelCall) { addKeywords(prefix, completionProposals, curLexeme); addDefaultValues(prefix, completionProposals, beginOffset); } // boolean isInFunction = (baseObject instanceof IScope) && !(baseObject == environment.getGlobal()); // compute properties - these are looked up from the proto chain String[] props = getAllPropertyNamesInScope(baseObject, true); String originalCasePrefix = isRootLevelCall ? fullName : fullName.substring(lastDot + 1, fullName.length()); for (int i = 0; i < props.length; i++) { String propName = props[i]; String upperPropName = propName.toUpperCase(); if (completionProposals.containsKey(propName) == false || isRootLevelCall == false) { // note: it seems get instance is not drilling down to the // actual instance but rather the commandNode // eg: f=function(){}; v=f; => v is an IdentifierNode and // getinstance gets a FunctionNode rather than a JSFunction IObject instance = baseObject.getInstance(getEnvironment(), fileIndex, offset); Property p = JSOffsetMapper.getPropertyInScope(instance, propName); if (p == null) { continue; } int sourceFileIndex = p.getSourceFileIndex(fileIndex, beginOffset); // int sourceFileIndex = p.getSourceFileIndex(FileContextManager.CURRENT_FILE_INDEX, Integer.MAX_VALUE); // //fileIndex, beginOffset); if (sourceFileIndex == -1) { sourceFileIndex = p.getSourceFileIndex(FileContextManager.CURRENT_FILE_INDEX, Integer.MAX_VALUE); } // TODO: if we can't find it, we shouldn't set it to the current file // if(sourceFileIndex == -1) // sourceFileIndex = fileIndex; // current file IObject propObject = p.getValue(fileIndex, offset).getInstance(getEnvironment(), fileIndex, offset); // TODO: work this out post beta, the offset here needs to be from the org prop file if (propObject == JSUndefined.getSingletonInstance()) { propObject = p.getValue(FileContextManager.CURRENT_FILE_INDEX, Integer.MAX_VALUE).getInstance( getEnvironment(), FileContextManager.CURRENT_FILE_INDEX, Integer.MAX_VALUE); } // filter out refs to just typed chars int asgnCount = p.getAssignments().size(); boolean wasJustTyped = prefix.equals(upperPropName) && asgnCount <= 1; if (wasJustTyped && asgnCount == 1) { if (p.getAssignment(0).getStartingOffset() != offset) { wasJustTyped = false; } } if (!wasJustTyped) { JSCompletionProposal jscp = computeProposal(p, instance, propObject, propName, prefix, isConstructor, replaceString, isNamedGlobal, beginOffset, sourceFileIndex); if (jscp != null) { completionProposals.put(propName, jscp); // if (!isPrefixReplace && upperPropName.equals(replaceString)) // defaultProposal = jscp; } } } } // testing // Template t = new Template("functionTemplate", " create a function", "all", "function", true); // TemplateContextType tct = new TemplateContextType("xxx", "myName"); // UnifiedTemplateContext utc = new UnifiedTemplateContext(tct); // Region r = new Region(offset, fullName.length()); // TemplateProposal tp = new TemplateProposal(t, utc, r, fIconField); // completionProposals.put("template1", tp); // compute variables - these are looked up from the scope chain if ((isRootLevelCall && baseObject instanceof IScope)) { IScope scope = (IScope) baseObject; String[] vars; if (isRootLevelCall) { vars = scope.getVariableNames(); } else { vars = scope.getLocalVariableNames(); } for (int i = 0; i < vars.length; i++) { String varName = vars[i]; if (!completionProposals.containsKey(varName)) { Property p = scope.getVariable(varName); if (p == null) { continue; } int sourceFileIndex = p.getSourceFileIndex(fileIndex, beginOffset); if (sourceFileIndex == -1) // cur file or not found { sourceFileIndex = fileIndex; } IObject varObject = p.getValue(fileIndex, offset).getInstance(getEnvironment(), fileIndex, offset); if (!(prefix.equals(varName.toUpperCase()) && p.getReferenceCount() < 2)) { JSCompletionProposal jscp = computeProposal(p, baseObject, varObject, varName, prefix, isConstructor, replaceString, isNamedGlobal, beginOffset, sourceFileIndex); if (jscp != null) { completionProposals.put(varName, jscp); // if (!isPrefixReplace && varName.toUpperCase().equals(replaceString)) // defaultProposal = jscp; } } } } } ICompletionProposal[] result = (ICompletionProposal[]) completionProposals.values().toArray( new ICompletionProposal[completionProposals.size()]); Arrays.sort(result, contentAssistComparator); if (originalCasePrefix.length() > 0 || fullName.endsWith(".")) //$NON-NLS-1$ { if (originalCasePrefix.equals("f")) //$NON-NLS-1$ { originalCasePrefix = "function"; //$NON-NLS-1$ } else if (originalCasePrefix.equals("d")) //$NON-NLS-1$ { originalCasePrefix = "document"; //$NON-NLS-1$ } else if (originalCasePrefix.equals("v")) //$NON-NLS-1$ { originalCasePrefix = "var"; //$NON-NLS-1$ } UnifiedContentAssistProcessor.setSelection(originalCasePrefix, result); } return result; } /** * @param prefix * @param completionProposals */ private void addKeywords(String prefix, Hashtable completionProposals, Lexeme curLexeme) { boolean isIdent = false; int beginOffset = 0; if (curLexeme != null) { isIdent = (curLexeme.typeIndex == JSTokenTypes.IDENTIFIER); beginOffset = isIdent ? curLexeme.offset : curLexeme.getEndingOffset(); } for (int i = 0; i < keywords.length; i++) { String keyName = keywords[i]; if (!completionProposals.containsKey(keyName)) { int finOffset = beginOffset; if (curLexeme != null && !isIdent && keyName.equals(prefix.toLowerCase())) { finOffset = curLexeme.offset; } String location = "Keyword"; //$NON-NLS-1$ JSCompletionProposal cp = new JSCompletionProposal(keyName, finOffset, prefix.length(), keyName .length(), fIconKeyword, keyName, new ContextInformation( "keywordInfo", Messages.JSContentAssistProcessor_The + keyName //$NON-NLS-1$ + Messages.JSContentAssistProcessor_Keyword), Messages.JSContentAssistProcessor_The2 + keyName + Messages.JSContentAssistProcessor_Keyword2, JSCompletionProposalComparator.OBJECT_TYPE_GLOBAL_OBJECT, location, UnifiedContentAssistProcessor.getAllUserAgentImages(UnifiedContentAssistProcessor .getUserAgents())); completionProposals.put(keyName, cp); } } } /** * private void setDefaultProposal(ICompletionProposal[] proposals, String prefix) { if (proposals.length == 0) { * return; } // int x = Arrays.binarySearch(proposals, prefix); // if(x < 0) // x = -x - 1; // if(x == * proposals.length) // x = proposals.length - 1; // proposals[x].setDefaultSelection(true); // return; if (prefix == * null || prefix.equals("") ) //$NON-NLS-1$ { for (int i = 0; i < proposals.length; i++) { ICompletionProposal * proposal = proposals[i]; if(proposal instanceof JSCompletionProposal) { * ((JSCompletionProposal)proposal).setDefaultSelection(true); break; } } return; } int defaultIndex = 0; int * defaultIndexNoCase = 0; String pUpper = prefix.toUpperCase(); int prefixIndex = 0; int prefixLen = * prefix.length(); char prefixChar = prefix.charAt(prefixIndex); char prefixUpperChar = pUpper.charAt(prefixIndex); * char nameChar = '\0'; char nameUpperChar = '\0'; // special cases until we get a db for selection history * if(prefix.toLowerCase().equals("f")) //$NON-NLS-1$ { defaultIndex = getProposal(proposals, "function"); * //$NON-NLS-1$ } else if(prefix.toLowerCase().equals("d")) //$NON-NLS-1$ { defaultIndex = getProposal(proposals, * "document"); //$NON-NLS-1$ } else if(prefix.toLowerCase().equals("v")) //$NON-NLS-1$ { defaultIndex = * getProposal(proposals, "var"); //$NON-NLS-1$ } else { for (int i = 0; i < proposals.length; i++) { if(! * (proposals[i] instanceof JSCompletionProposal)) { continue; } String name = proposals[i].getDisplayString(); * String nameUpper = name.toUpperCase(); if (name.length() > prefixIndex) { nameChar = name.charAt(prefixIndex); * nameUpperChar = nameUpper.charAt(prefixIndex); if (nameUpperChar == prefixUpperChar) { defaultIndex = i; if * (nameChar == prefixChar) { defaultIndexNoCase = i; } if (prefixIndex < prefixLen - 1) { i--; prefixIndex++; * prefixChar = prefix.charAt(prefixIndex); prefixUpperChar = pUpper.charAt(prefixIndex); continue; } else { while * (i < proposals.length) { name = proposals[i].getDisplayString(); if (name.startsWith(prefix)) { * defaultIndexNoCase = i; break; } if (name.toUpperCase().charAt(0) > pUpper.charAt(0)) { break; } i++; } break; } } * else if (nameUpperChar < prefixUpperChar || nameUpperChar == '_') { if (prefixIndex > 0 && * !nameUpper.startsWith(pUpper.substring(0, prefixIndex))) { break; } defaultIndex = i; } else if (nameUpperChar > * prefixUpperChar) { break; } } } } if (defaultIndexNoCase > defaultIndex) { defaultIndex = defaultIndexNoCase; } // * special case prototype, just for me : ) String curName = proposals[defaultIndex].getDisplayString(); if * (proposals.length > defaultIndex && pUpper.length() < 4 && curName.startsWith("propertyIsEnumerable") * //$NON-NLS-1$ && proposals[defaultIndex + 1].getDisplayString().startsWith("prototype")) //$NON-NLS-1$ { * defaultIndex++; } // [RD] fix - can't assume all proposals will be JSCompletionProposals * if(proposals[defaultIndex] instanceof JSCompletionProposal) { * ((JSCompletionProposal)proposals[defaultIndex]).setDefaultSelection(true); } // JSCompletionProposal dflt = null; // * JSCompletionProposal dfltNoCase = null; // boolean noCaseMatch = true; // // for (int i = 0; i < * proposals.length; i++) // { // JSCompletionProposal proposal = proposals[i]; // String propName = * proposal.getDisplayString(); // // // may want to use Collator here for � etc // * if(propName.compareToIgnoreCase(prefix) >= 0) // { // // check for at least first letter matches in this special * case // if(noCaseMatch && dfltNoCase != null) // { // char curc = propName.toUpperCase().charAt(0); // char dfltc = * dfltNoCase.getDisplayString().toUpperCase().charAt(0); // char prefixc = prefix.toUpperCase().charAt(0); // * if(dfltc != prefixc && curc == prefixc) // { // dfltNoCase = proposal; // } // } // // noCaseMatch = false; // // * if(dfltNoCase == null) // // dfltNoCase = proposal; // if(proposal.getDisplayString().compareTo(prefix) >= 0) // { // * dflt = proposal; // break; // } // } // if(noCaseMatch) // dfltNoCase = proposal; // } // // make sure there is * at least a default // if(dflt == null) // { // dflt = dfltNoCase; // if(dflt == null) // dflt = proposals[0]; // } // // * now need to pick a winner // String prefixUpper = prefix.toUpperCase(); // String dfltUpper = * dflt.getDisplayString().toUpperCase(); // if(dfltNoCase != null && !dfltUpper.startsWith(prefixUpper)) // { // * String dfltNoCaseUpper = dfltNoCase.getDisplayString().toUpperCase(); // char[] prefixChars = * prefixUpper.toCharArray(); // char[] dfltChars = dfltUpper.toCharArray(); // char[] dfltNoCaseChars = * dfltNoCaseUpper.toCharArray(); // for(int i = 0; i < prefixChars.length; i++) // { // if(prefixChars[i] != * dfltNoCaseChars[i]) // { // break; // }else if(prefixChars[i] != dfltChars[i]) // { // dflt = dfltNoCase; // * break; // } // } // } // dflt.setDefaultSelection(true); } */ // /** // * @param proposals // * @param string // * @return index of proposal // */ // private int getProposal(ICompletionProposal[] proposals, String matchName) // { // int result = 0; // for (int i = 0; i < proposals.length; i++) // { // if(! (proposals[i] instanceof JSCompletionProposal)) // { // continue; // } // String name = proposals[i].getDisplayString(); // if(name.equals(matchName)) // { // result = i; // break; // } // } // return result; // } /** * Filters most invalid proposals * * @param propertyObject * The object to test * @param propertyName * The name of the object to test. * @param prefix * The valid prefix for testing * @return Returns true if this is a valid proposal. */ private boolean isValidProposal(IObject baseObject, IObject propertyObject, String propertyName, String prefix) { if (baseObject == null || propertyName.equals("")) //$NON-NLS-1$ { return false; } if (propertyName.indexOf("[") > -1) //$NON-NLS-1$ { return false; } if (propertyObject == ObjectBase.UNDEFINED || propertyObject == null) { // only abort if there is no actual property (the value may be undefined if the return type is unknown) // note this is a short term fix. Ultimately we need a way to tell the difference between things // that have return values of undefined, and things that haven't been defined (yet), // and things like params, who's type is undefined. This also turns of position info which needs to be // revisited. // TODO: decide proper approach for this after the Alpha (do we want to show undefined properties?) // In the alpha, we now force the actively edited file to have a higher file index than all other files. // if(baseObject.getProperty(propertyName) == null) return false; } // only take members that start with the current prefix // remove duplicates - we can do it this way as the array is sorted // if (propertyName.toUpperCase().startsWith(prefix) == false) // { // return false; // } // we can filter these later if we like, however it is useful to have them for debugging atm. // (note: technically it is correct to show them, but may seem noisy) // if (propertyName.equals("constructor")) // { // return false; // } Property prop = JSOffsetMapper.getPropertyInScope(baseObject, propertyName); if ((prop == null) && (baseObject instanceof IScope)) { prop = ((IScope) baseObject).getVariable(propertyName); } // shouldn't happen if (prop == null) { return false; } if (prop.isVisible() == false) { return false; } // invocation properties like arguments and callee IDocumentation docs = propertyObject.getDocumentation(); if (docs != null && docs instanceof PropertyDocumentation) { PropertyDocumentation pdocs = (PropertyDocumentation) docs; if (pdocs.getIsInvocationOnly()) { if (!(baseObject instanceof JSScope)) { return false; } } else if (pdocs.getIsInternal()) { return false; } } return true; } /** * Computes a single propsal if valid, otherwise returns null. * * @param obj * The object to look for proposals * @param propertyName * The hashName of the object. * @param prefix * The valid starting letters to complete with. * @param isConstructor * True if constructors are valid (like when using 'new'). * @param the * replace string, this gives us full control over what will be replaced in the case where we aren't * matching by ident prefix. * @return Returns a single proposal if valid, otherwise returns null */ // CHECKSTYLE:OFF private JSCompletionProposal computeProposal(Property p, IObject baseObject, IObject propertyObject, String propertyName, String prefix, boolean isConstructor, String replaceString, boolean isNamedGlobal, int beginOffset, int sourceFileIndex) // CHECKSTYLE:ON { if (!isValidProposal(baseObject, propertyObject, propertyName, prefix)) { return null; } ObjectBase global = getJSOffsetMapper().getGlobal(); JSCompletionProposal cp = null; int replaceLength = replaceString.length(); String replacementText = propertyName; Image icon = fIconError; StringBuffer docText = new StringBuffer(); int sortingType = JSCompletionProposalComparator.OBJECT_TYPE_OTHER; String additionalText = ""; //$NON-NLS-1$ int extraInsert = 0; ContextInformation ci = null; int fileIndex = this.getJSOffsetMapper().getFileIndex(); int offset = 0; Lexeme curLexeme = this.getOffsetMapper().getCurrentLexeme(); if (curLexeme != null) { offset = curLexeme.offset; } IObject finalObject = propertyObject.getInstance(getEnvironment(), fileIndex, offset); // guard for objects in error state if (finalObject == null) { return cp; } boolean isGuessedObject = false; IDocumentation doc = finalObject.getDocumentation(); // handle possible reassignments (the docs would have been hidden in this case) if (doc == null) { doc = p.getAnyValidDocumentation(sourceFileIndex, beginOffset); } if (finalObject instanceof JSGuessedObject && doc == null) { isGuessedObject = true; } Image[] images = null; // Note: taking this out, because it doesn't allow namespaced CTORs like 'new YAHOO.widget();' // // "new XX" only uses constructors - for now things are only treated as // // ctors if documented as such // if (isConstructor && !(finalObject instanceof IFunction))//(doc instanceof FunctionDocumentation) ) // { // return null; // } if (doc != null) { // todo: use the type desc here, and make all docs just "Documentation" if (doc instanceof PropertyDocumentation && !(doc instanceof FunctionDocumentation)) { PropertyDocumentation pDoc = (PropertyDocumentation) doc; if (pDoc.getUserAgentPlatformNames().length > 0) { images = UnifiedContentAssistProcessor.getUserAgentImages(UnifiedContentAssistProcessor .getUserAgents(), pDoc.getUserAgentPlatformNames()); } // turn off 'window' (or _root for example) from seeing internal classes (but not instnaces) // boolean isMember = pDoc.getMemberOf().getTypes().length == 0; if (isNamedGlobal && pDoc.getIsInternal()) { return null; } docText.append(ScriptDocHelper.createPropertyDocumentationHTML(propertyName, pDoc, true)); if (baseObject == global) { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_GLOBAL_OBJECT; if (isGuessedObject) { icon = fIconObjectGuessed; } else { icon = fIconObject; } } else { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_PROPERTY; if (isGuessedObject) { icon = fIconFieldGuessed; } else { icon = fIconField; } } } else if (doc instanceof FunctionDocumentation) { // taking these out until they can talk better to the bracket inserter logic // if(isConstructor || finalObject.getPropertyNames().length == 0) // additionalText = "()"; // extraInsert = 1; // Don't need arg assist if we are not inserting the "()" parens // ci = computeArgContextInformation(finalObject, propertyName); FunctionDocumentation fDoc = (FunctionDocumentation) doc; if (fDoc.getUserAgentPlatformNames().length > 0) { images = UnifiedContentAssistProcessor.getUserAgentImages(UnifiedContentAssistProcessor .getUserAgents(), fDoc.getUserAgentPlatformNames()); } // turn off 'window' (or _root for example) from seeing internal properties // boolean isMember = fDoc.getMemberOf().getTypes().length == 0; if (isNamedGlobal && fDoc.getIsInternal() && fDoc.getIsConstructor()) { return null; } docText.append(ScriptDocHelper.createMethodDocumentationHTML(propertyName, fDoc, finalObject)); boolean isCtor = fDoc.getIsConstructor(); boolean isMethod = fDoc.getIsMethod(); // if they haven't set it specifically, guess if it is a class by checking the prototype if (!isCtor && !isMethod) { // find out if there are properties on the prototype if (hasPropertiesOnPrototype(finalObject, fileIndex, offset)) { isCtor = true; } } if (isCtor) { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_CLASS; if (isGuessedObject) { icon = fIconClassGuessed; } else { icon = fIconClass; } } else { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_METHOD; if (isGuessedObject) { icon = fIconFunctionGuessed; } else { icon = fIconFunction; } } } else { docText.append(ScriptDocHelper.createGenericDocumentationHTML(propertyName, doc, false)); docText.append(doc.getDescription()); } } else { // no docs, so we'll have to guess if (propertyObject instanceof IFunction) { // taking these out until they can talk better to the bracket inserter logic // if(isConstructor || finalObject.getPropertyNames().length == 0) // additionalText = "()"; // extraInsert = 1; ci = computeArgContextInformation(finalObject, propertyName); docText.append("<b>" + propertyName + "</b> " + ci.getInformationDisplayString()); //$NON-NLS-1$ //$NON-NLS-2$ ci = null; // find out if there are properties on the prototype if (hasPropertiesOnPrototype(finalObject, fileIndex, offset)) { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_CLASS; if (isGuessedObject) { icon = fIconClassGuessed; } else { icon = fIconClass; } } else { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_METHOD; if (isGuessedObject) { icon = fIconFunctionGuessed; } else { icon = fIconFunction; } } } else if (baseObject == global) { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_GLOBAL_OBJECT; if (isGuessedObject) { icon = fIconObjectGuessed; } else { icon = fIconObject; } docText.append("<b>" + propertyName + Messages.JSContentAssistProcessor_NoDocs); //$NON-NLS-1$ } else { sortingType = JSCompletionProposalComparator.OBJECT_TYPE_PROPERTY; if (isGuessedObject) { icon = fIconFieldGuessed; } else { icon = fIconField; } docText.append("<b>" + propertyName + Messages.JSContentAssistProcessor_NoDocs2); //$NON-NLS-1$ } } // find source file name String name = ""; //$NON-NLS-1$ if (sourceFileIndex == FileContextManager.BUILT_IN_FILE_INDEX) { if (doc != null) { name = doc.getUserAgent(); } else { name = "JS Core"; //$NON-NLS-1$ } } else if (sourceFileIndex == FileContextManager.CURRENT_FILE_INDEX) { if (doc != null && doc.getUserAgent() != null && !doc.getUserAgent().equals("")) //$NON-NLS-1$ { name = doc.getUserAgent(); } else { // TODO: safely cache this value at some point name = CoreUIUtils.getActiveEditorURI(); if (name != null && "".equals(name) == false) //$NON-NLS-1$ { name = new Path(name).lastSegment(); } } } else if (sourceFileIndex > FileContextManager.DEFAULT_FILE_INDEX) { name = FileContextManager.getURIFromFileIndex(sourceFileIndex); if (name != null && name.length() > 0) { name = new Path(name).lastSegment(); } else if (doc != null) { name = doc.getUserAgent(); } // [IM] now decoding file names to remove %20's } else { name = Messages.JSContentAssistProcessor_Inferred; // shouldn't happen - this is the -1 case which doesn't get added to the environment // //$NON-NLS-1$ } String location = (name == null) ? "" : name; // files not in profile show up in CA //$NON-NLS-1$ location = StringUtils.urlDecodeFilename(location.toCharArray()); cp = new JSCompletionProposal(propertyName + additionalText, beginOffset,// offset - prefixLen, replaceLength, propertyName.length() + extraInsert, icon, replacementText, ci, docText.toString(), sortingType, location, images); return cp; } private boolean hasPropertiesOnPrototype(IObject obj, int fileIndex, int offset) { // find out if there are properties directly on the prototype Property prot = obj.getProperty("prototype"); //$NON-NLS-1$ if (prot != null) { String[] protProps = prot.getValue(fileIndex, offset).getLocalPropertyNames(); // functions always have a 'constructor' property return (protProps != null && protProps.length > 1); } return false; } /** * getAllPropertyNamesInScope * * @param object * @param includeHiddenProps * @return String[] */ public static String[] getAllPropertyNamesInScope(IObject object, boolean includeHiddenProps) { if (object instanceof IScope) { IScope scope = (IScope) object; if (scope.getParentScope() == null) { return object.getPropertyNames(includeHiddenProps); } ArrayList al = new ArrayList(); String[] curProps = object.getPropertyNames(includeHiddenProps); int totalProps = curProps.length; al.add(curProps); scope = scope.getParentScope(); while (scope != null) { curProps = scope.getPropertyNames(includeHiddenProps); totalProps += curProps.length; al.add(curProps); scope = scope.getParentScope(); } String[] result = new String[totalProps]; int count = 0; for (int i = 0; i < al.size(); i++) { String[] props = (String[]) al.get(i); for (int k = 0; k < props.length; k++) { result[count++] = props[k]; } } return result; } else { return object.getPropertyNames(includeHiddenProps); } } /** * getOffsetMapper * * @return IOffsetMapper */ public IOffsetMapper getOffsetMapper() { IFileLanguageService ls = context.getLanguageService(JSMimeType.MimeType); if (ls != null) { return (JSOffsetMapper) ls.getOffsetMapper(); } else { return null; } } /** * getJSOffsetMapper * * @return JSOffsetMapper */ public JSOffsetMapper getJSOffsetMapper() { return (JSOffsetMapper) getOffsetMapper(); } /** * setHotkeyActivated * * @param value */ public void setHotkeyActivated(boolean value) { JSContentAssistant.setHotkeyActivated(value); } /** * setNextIdleActivated * * @param value */ public void setNextIdleActivated(boolean value) { forceActivated = value; } /** * isNextIdleActivated * * @return boolean */ public boolean isNextIdleActivated() { return forceActivated; } /** * @see com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistProcessor#isValidIdleActivationLocation(org.eclipse.jface.text.ITextViewer, * int) */ public boolean isValidIdleActivationLocation(ITextViewer viewer, int offset) { Lexeme currentLexeme = this.getOffsetMapper().getCurrentLexeme(); return UnifiedContentAssistProcessor.isValidIdleActivationToken(currentLexeme, getCompletionProposalIdleActivationTokens()); } }