/******************************************************************************* * Copyright (c) 2005, 2016 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 * *******************************************************************************/ package org.eclipse.dltk.ruby.internal.core.codeassist; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.dltk.ast.ASTNode; import org.eclipse.dltk.ast.Modifiers; import org.eclipse.dltk.ast.declarations.MethodDeclaration; import org.eclipse.dltk.ast.declarations.ModuleDeclaration; import org.eclipse.dltk.ast.declarations.TypeDeclaration; import org.eclipse.dltk.ast.expressions.CallArgumentsList; import org.eclipse.dltk.ast.expressions.CallExpression; import org.eclipse.dltk.ast.expressions.NumericLiteral; import org.eclipse.dltk.ast.expressions.StringLiteral; import org.eclipse.dltk.ast.parser.ISourceParser; import org.eclipse.dltk.ast.references.ConstantReference; import org.eclipse.dltk.ast.references.SimpleReference; import org.eclipse.dltk.codeassist.ScriptCompletionEngine; import org.eclipse.dltk.compiler.env.IModuleSource; import org.eclipse.dltk.compiler.util.Util; import org.eclipse.dltk.core.CompletionProposal; import org.eclipse.dltk.core.DLTKLanguageManager; import org.eclipse.dltk.core.Flags; import org.eclipse.dltk.core.IField; import org.eclipse.dltk.core.IMethod; import org.eclipse.dltk.core.IModelElement; import org.eclipse.dltk.core.ISourceModule; import org.eclipse.dltk.core.IType; import org.eclipse.dltk.core.ModelException; import org.eclipse.dltk.core.mixin.IMixinElement; import org.eclipse.dltk.core.mixin.MixinModel; import org.eclipse.dltk.evaluation.types.AmbiguousType; import org.eclipse.dltk.evaluation.types.IClassType; import org.eclipse.dltk.internal.core.util.WeakHashSet; import org.eclipse.dltk.ruby.ast.RubyBlock; import org.eclipse.dltk.ruby.ast.RubyColonExpression; import org.eclipse.dltk.ruby.ast.RubyDAssgnExpression; import org.eclipse.dltk.ruby.ast.RubyDVarExpression; import org.eclipse.dltk.ruby.ast.RubySelfReference; import org.eclipse.dltk.ruby.core.RubyNature; import org.eclipse.dltk.ruby.core.RubyPlugin; import org.eclipse.dltk.ruby.core.model.FakeField; import org.eclipse.dltk.ruby.core.utils.RubySyntaxUtils; import org.eclipse.dltk.ruby.internal.parser.mixin.IMixinSearchRequestor; import org.eclipse.dltk.ruby.internal.parser.mixin.IRubyMixinElement; import org.eclipse.dltk.ruby.internal.parser.mixin.PrefixNoCaseMixinSearchPattern; import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinClass; import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinElementInfo; import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinMethod; import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinModel; import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinUtils; import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinVariable; import org.eclipse.dltk.ruby.internal.parsers.jruby.ASTUtils; import org.eclipse.dltk.ruby.typeinference.RubyClassType; import org.eclipse.dltk.ruby.typeinference.RubyModelUtils; import org.eclipse.dltk.ruby.typeinference.RubyTypeInferencingUtils; import org.eclipse.dltk.ti.BasicContext; import org.eclipse.dltk.ti.DLTKTypeInferenceEngine; import org.eclipse.dltk.ti.goals.ExpressionTypeGoal; import org.eclipse.dltk.ti.types.IEvaluatedType; public class RubyCompletionEngine extends ScriptCompletionEngine { /** * Type inferencer timeout */ private static final int TI_TIMEOUT = 2000; private final static int RELEVANCE_FREE_SPACE = 100000; /** * Relevance for keywords. Should be used only for keywords - the distinct * value is required to sort templates (their relevance is much lower by * default) before the matching keywords - see * ScriptCompletionProposalComputer#updateTemplateProposalRelevance() */ private final static int RELEVANCE_KEYWORD = 1000000; private final static int RELEVANCE_TYPE = 2000000; private final static int RELEVANCE_METHODS = 10000000; private final static int RELEVANCE_VARIABLES = 100000000; private final static String[] globalVars = { "$DEBUG", "$$", "$-i", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ "$deferr", "$/", "$'", "$stdout", "$-l", "$-I", "$.", "$KCODE", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ "$binding", "$-w", "$FILENAME", "$defout", "$,", "$`", "$-F", "$*", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ "$LOADED_FEATURES", "$stdin", "$-p", "$:", "$\\", "$=", "$!", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ "$-v", "$>", "$&", "$;", "$SAFE", "$PROGRAM_NAME", "$\"", "$-d", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ "$?", "$-0", "$+", "$@", "$-a", "$VERBOSE", "$stderr", "$~", "$0", //$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$ "$LOAD_PATH", "$<", "$_", "$-K" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ private DLTKTypeInferenceEngine inferencer; private ISourceParser parser = null; private RubyMixinModel mixinModel; private HashSet<String> completedNames = new HashSet<String>(); private WeakHashSet intresting = new WeakHashSet(); private ASTNode completionNode; private ISourceModule currentModule; public RubyCompletionEngine() { this.inferencer = new DLTKTypeInferenceEngine(); this.parser = DLTKLanguageManager.getSourceParser(RubyNature.NATURE_ID); } @Override protected int getEndOfEmptyToken() { return 0; } @Override protected String processMethodName(IMethod method, String token) { return null; } @Override protected String processTypeName(IType method, String token) { return null; } private boolean afterColons(String content, int position) { if (position < 2) return false; if (content.charAt(position - 1) == ':' && content.charAt(position - 2) == ':') return true; return false; } private boolean afterDollar(String content, int position) { if (position < 1) return false; if (content.charAt(position - 1) == '$') return true; return false; } private boolean afterAt(String content, int position) { if (position < 1) return false; if (content.charAt(position - 1) == '@') return true; return false; } private boolean afterAt2(String content, int position) { if (position < 2) return false; if (content.charAt(position - 1) == '@' && content.charAt(position - 2) == '@') return true; return false; } private boolean afterDot(String content, int position) { return position >= 1 && content.charAt(position - 1) == '.'; } private String getWordStarting(String content, int position, int maxLen) { if (position <= 0 || position > content.length()) return Util.EMPTY_STRING; final int original = position; while (position > 0 && maxLen > 0 && ((content.charAt(position - 1) == ':') || (content.charAt(position - 1) == '\'') || (content.charAt(position - 1) == '"') || RubySyntaxUtils .isLessStrictIdentifierCharacter(content .charAt(position - 1)))) { --position; --maxLen; } return content.substring(position, original); } @Override public void complete(IModuleSource module, int position, int i) { this.currentModule = (ISourceModule) module; this.mixinModel = RubyMixinModel.getInstance(currentModule .getScriptProject()); completedNames.clear(); this.actualCompletionPosition = position; this.requestor.beginReporting(); try { final String content = module.getSourceContents(); String wordStarting = getWordStarting(content, position, 10); if (wordStarting.length() != 0) { this.setSourceRange(position - wordStarting.length(), position); String[] keywords = RubyKeyword.findByPrefix(wordStarting); for (int j = 0; j < keywords.length; j++) { reportKeyword(keywords[j]); } } ModuleDeclaration moduleDeclaration = (ModuleDeclaration) parser .parse(module, null); if (afterDollar(content, position)) { completeGlobalVar(moduleDeclaration, "$", position); //$NON-NLS-1$ } else if (afterAt2(content, position)) { completeSimpleRef(moduleDeclaration, "@@", position); //$NON-NLS-1$ } else if (afterAt(content, position)) { completeSimpleRef(moduleDeclaration, "@", position); //$NON-NLS-1$ } else if (afterColons(content, position)) { ASTNode node = ASTUtils.findMaximalNodeEndingAt( moduleDeclaration, position - 2); this.setSourceRange(position, position); if (node != null) { BasicContext basicContext = new BasicContext(currentModule, moduleDeclaration); ExpressionTypeGoal goal = new ExpressionTypeGoal( basicContext, node); IEvaluatedType type = inferencer.evaluateType(goal, TI_TIMEOUT * 3 / 2); reportSubElements(type, Util.EMPTY_STRING); } else { completeConstant(moduleDeclaration, Util.EMPTY_STRING, position, true); } } else { ASTNode minimalNode = ASTUtils.findMinimalNode( moduleDeclaration, position, position); if (minimalNode != null) { if (minimalNode instanceof CallExpression) { CallExpression callExp = (CallExpression) minimalNode; if ((position > 0) && (content.charAt(position - 1) == ' ')) { minimalNode = callExp.getArgs(); } } this.completionNode = minimalNode; if (minimalNode instanceof CallExpression) { completeCall(moduleDeclaration, (CallExpression) minimalNode, position); } else if (minimalNode instanceof CallArgumentsList) { completeSimpleRef(moduleDeclaration, wordStarting, position); IEvaluatedType self = RubyTypeInferencingUtils .determineSelfClass(mixinModel, currentModule, moduleDeclaration, position); if ((self != null) && "Object".equals(self.getTypeName())) { //$NON-NLS-1$ ExpressionTypeGoal goal = new ExpressionTypeGoal( new BasicContext(currentModule, moduleDeclaration), minimalNode); IEvaluatedType self2 = inferencer.evaluateType( goal, TI_TIMEOUT); if (self2 != null) { self = self2; } } completeClassMethods(moduleDeclaration, self, Util.EMPTY_STRING, true); } else if (minimalNode instanceof ConstantReference) { completeConstant(moduleDeclaration, (ConstantReference) minimalNode, position); } else if (minimalNode instanceof RubyColonExpression) { completeColonExpression(moduleDeclaration, (RubyColonExpression) minimalNode, position); } else if (minimalNode instanceof SimpleReference) { completeSimpleRef(moduleDeclaration, wordStarting, position); } else if (minimalNode instanceof RubyDVarExpression) { if (afterDot(content, position)) { completeClassMethods(moduleDeclaration, minimalNode, wordStarting); } else { completeSimpleRef(moduleDeclaration, wordStarting, position); } } else if (minimalNode instanceof MethodDeclaration || minimalNode instanceof TypeDeclaration || minimalNode instanceof StringLiteral) { completeSimpleRef(moduleDeclaration, wordStarting, position); IEvaluatedType self = RubyTypeInferencingUtils .determineSelfClass(mixinModel, currentModule, moduleDeclaration, position); if ((self != null) && "Object".equals(self.getTypeName())) { //$NON-NLS-1$ ExpressionTypeGoal goal = new ExpressionTypeGoal( new BasicContext(currentModule, moduleDeclaration), minimalNode); IEvaluatedType self2 = inferencer.evaluateType( goal, TI_TIMEOUT); if (self2 != null) { self = self2; } } completeClassMethods(moduleDeclaration, self, wordStarting, true); } else if (minimalNode instanceof ModuleDeclaration) { ExpressionTypeGoal goal = new ExpressionTypeGoal( new BasicContext(currentModule, moduleDeclaration), minimalNode); IEvaluatedType self = inferencer.evaluateType(goal, TI_TIMEOUT); completeClassMethods(moduleDeclaration, self, wordStarting, true); } else if (minimalNode instanceof NumericLiteral && position > 0 && position == minimalNode.sourceEnd() && position > minimalNode.sourceStart() && content.charAt(position - 1) == '.') { setSourceRange(position, position); completeClassMethods(moduleDeclaration, minimalNode, Util.EMPTY_STRING); } else { // worst case completeSimpleRef(moduleDeclaration, wordStarting, position); if (wordStarting.length() == 0 && !requestor.isContextInformationMode()) { if (!afterContentAndSpace(moduleDeclaration, content, position)) { reportCurrentElements(moduleDeclaration, position); } } } } } } finally { this.requestor.endReporting(); } } /** * @param content * @param position * @return */ private boolean afterContentAndSpace(ModuleDeclaration moduleDeclaration, String content, int position) { while (position > 0) { final char c = content.charAt(position - 1); if (c == ' ' || c == '\t') { --position; } else { break; } } if (position > 0 && RubySyntaxUtils.isIdentifierCharacter(content .charAt(position - 1))) { ASTNode node = ASTUtils.findMinimalNode(moduleDeclaration, position, position); if (node instanceof CallExpression) { int begin = position; while (begin > 0 && content.charAt(begin - 1) != '\r' && content.charAt(begin - 1) != '\n') { --begin; } ASTNode[] way = ASTUtils.restoreWayToNode(moduleDeclaration, node); for (int i = way.length - 1; --i >= 0;) { if (way[i] instanceof CallExpression) { // if multiple method calls in the same line return way[i].sourceStart() >= begin; } } return false; } return true; } return false; } private void reportCurrentElements(ModuleDeclaration moduleDeclaration, int position) { setSourceRange(position, position); completeSimpleRef(moduleDeclaration, Util.EMPTY_STRING, position); IClassType self = RubyTypeInferencingUtils.determineSelfClass( mixinModel, currentModule, moduleDeclaration, position); if (self == null) { return; } completeClassMethods(moduleDeclaration, self, Util.EMPTY_STRING, true); if ("Object".equals(self.getTypeName())) { //$NON-NLS-1$ try { final IModelElement[] children = currentModule.getChildren(); if (children != null) { for (int i = 0; i < children.length; ++i) { IModelElement element = children[i]; if (element instanceof IField) { reportField((IField) element, RELEVANCE_FREE_SPACE); } else if (element instanceof IMethod) { IMethod method = (IMethod) element; if ((method.getFlags() & Modifiers.AccStatic) == 0) { reportMethod(method, RELEVANCE_FREE_SPACE); } } else if (element instanceof IType) { if (!element.getElementName().trim().startsWith( "<<")) //$NON-NLS-1$ reportType((IType) element, RELEVANCE_FREE_SPACE); } } } } catch (ModelException e) { RubyPlugin.log(e); } } } private class CompletionMixinMethodRequestor implements IMixinSearchRequestor { private String lastParent = null; private boolean lastParentIsSuperClass = true; private final List<RubyMixinMethod> group = new ArrayList<RubyMixinMethod>(); private final RubyMixinClass klass; private final boolean isSelf; public CompletionMixinMethodRequestor(RubyMixinClass klass, boolean isSelf) { this.klass = klass; this.isSelf = isSelf; } /** * Tests that the specified key identifies superclass of the * {@link #klass} or the same class. * * @param key * @return */ private boolean isSuperClass(String key) { if (!RubyMixinUtils.isObjectOrKernel(key)) { if (key.equals(klass.getKey())) { return true; } RubyMixinClass superclass = klass.getSuperclass(); while (superclass != null) { final String superClassKey = superclass.getKey(); if (RubyMixinUtils.isObjectOrKernel(superClassKey)) { break; } if (key.equals(superClassKey)) { return true; } superclass = superclass.getSuperclass(); } } return false; } @Override public void acceptResult(IRubyMixinElement element) { if (element instanceof RubyMixinMethod) { final RubyMixinMethod method = (RubyMixinMethod) element; final RubyMixinClass selfClass = method.getSelfType(); if (lastParent == null || !lastParent.equals(selfClass.getKey())) { if (lastParent != null) { this.flush(); } lastParent = selfClass.getKey(); lastParentIsSuperClass = isSuperClass(lastParent); } int flags = 0; final IMethod[] methods = method.getSourceMethods(); for (int cnt = 0, max = methods.length; cnt < max; cnt++) { final IMethod m = methods[cnt]; if (m != null) { try { flags |= m.getFlags(); } catch (ModelException mxcn) { // ignore } } } final boolean shouldAdd = Flags.isPublic(flags) || lastParentIsSuperClass && Flags.isProtected(flags) || Flags.isPrivate(flags) && isSelf || RubyMixinUtils.isKernel(selfClass.getKey()); if (shouldAdd) group.add(method); } } public void flush() { if (group.size() > 0) { RubyMixinMethod[] mixinMethods = group.toArray(new RubyMixinMethod[group.size()]); final List<IMethod> methods = RubyModelUtils.getAllSourceMethods( mixinMethods, klass); int skew = 0; if (klass.getKey().equals(lastParent)) { skew = 2; } else if (lastParentIsSuperClass) { // TODO calculate the distance in the hierarchy skew = 1; } for (int j = 0, size = methods.size(); j < size; j++) { reportMethod(methods.get(j), RELEVANCE_METHODS + skew); } group.clear(); } } } private void completeClassMethods(ModuleDeclaration moduleDeclaration, RubyMixinClass rubyClass, String prefix, boolean isSelf) { CompletionMixinMethodRequestor mixinSearchRequestor = new CompletionMixinMethodRequestor( rubyClass, isSelf); rubyClass.findMethods(new PrefixNoCaseMixinSearchPattern(prefix), mixinSearchRequestor); mixinSearchRequestor.flush(); } private void completeClassMethods(ModuleDeclaration moduleDeclaration, IEvaluatedType type, String prefix, boolean isSelf) { if (type instanceof RubyClassType) { RubyClassType rubyClassType = (RubyClassType) type; RubyMixinClass rubyClass = mixinModel .createRubyClass(rubyClassType); if (rubyClass != null) { completeClassMethods(moduleDeclaration, rubyClass, prefix, isSelf); } } else if (type instanceof AmbiguousType) { AmbiguousType type2 = (AmbiguousType) type; IEvaluatedType[] possibleTypes = type2.getPossibleTypes(); for (int i = 0; i < possibleTypes.length; i++) { completeClassMethods(moduleDeclaration, possibleTypes[i], prefix, isSelf); } } } private void completeClassMethods(ModuleDeclaration moduleDeclaration, ASTNode receiver, String pattern) { ExpressionTypeGoal goal = new ExpressionTypeGoal(new BasicContext( currentModule, moduleDeclaration), receiver); IEvaluatedType type = inferencer.evaluateType(goal, TI_TIMEOUT); completeClassMethods(moduleDeclaration, type, pattern, receiver instanceof RubySelfReference); } private void completeGlobalVar(ModuleDeclaration moduleDeclaration, String prefix, int position) { this.setSourceRange(position - (prefix != null ? prefix.length() : 0), position); IMixinElement[] elements = mixinModel.getRawModel().find( (prefix != null ? prefix : Util.EMPTY_STRING) + "*", new NullProgressMonitor()); //$NON-NLS-1$ // String[] findKeys = RubyMixinModel.getRawInstance().findKeys( // prefix + "*"); for (int i = 0; i < elements.length; i++) { IRubyMixinElement rubyElement = mixinModel .createRubyElement(elements[i]); if (rubyElement instanceof RubyMixinVariable) { RubyMixinVariable variable = (RubyMixinVariable) rubyElement; IField[] sourceFields = variable.getSourceFields(); for (int j = 0; j < sourceFields.length; j++) { if (sourceFields[j] != null) { reportField(sourceFields[j], RELEVANCE_VARIABLES); break; } } } } for (int i = 0; i < globalVars.length; i++) { if (prefix == null || globalVars[i].startsWith(prefix)) reportField(new FakeField(currentModule, globalVars[i], 0, 0), RELEVANCE_VARIABLES); } } private void completeSimpleRef(ModuleDeclaration moduleDeclaration, String prefix, int position) { this.setSourceRange(position - prefix.length(), position); ASTNode[] wayToNode = ASTUtils.restoreWayToNode(moduleDeclaration, this.completionNode); for (int i = wayToNode.length - 1; i > 0; i--) { if (wayToNode[i] instanceof RubyBlock) { RubyBlock rubyBlock = (RubyBlock) wayToNode[i]; Set<ASTNode> vars = rubyBlock.getVars(); for (Iterator<ASTNode> iterator = vars.iterator(); iterator.hasNext();) { ASTNode n = iterator.next(); if (n instanceof RubyDAssgnExpression) { RubyDAssgnExpression rd = (RubyDAssgnExpression) n; if (rd.getName().startsWith(prefix)) { reportField(new FakeField(currentModule, rd .getName(), 0, 0), RELEVANCE_VARIABLES); } } } } } if (prefix.startsWith("$")) { // globals //$NON-NLS-1$ completeGlobalVar(moduleDeclaration, prefix, position); } else { // class & instance & locals IField[] fields = RubyModelUtils.findFields(mixinModel, currentModule, moduleDeclaration, prefix, position); for (int i = 0; i < fields.length; i++) { reportField(fields[i], RELEVANCE_VARIABLES); } } } private void reportSubElements(IEvaluatedType type, String prefix) { if (!(type instanceof RubyClassType)) { return; } RubyClassType rubyClassType = (RubyClassType) type; IMixinElement mixinElement = mixinModel.getRawModel().get( rubyClassType.getModelKey()); if (mixinElement == null) { return; } List<IType> types = new ArrayList<IType>(); List<IMethod> methods = new ArrayList<IMethod>(); List<IField> fields = new ArrayList<IField>(); IMixinElement[] children = mixinElement.getChildren(); for (int i = 0; i < children.length; i++) { Object[] infos = children[i].getAllObjects(); for (int j = 0; j < infos.length; j++) { RubyMixinElementInfo obj = (RubyMixinElementInfo) infos[j]; if (obj.getObject() == null) continue; if (obj.getKind() == RubyMixinElementInfo.K_CLASS || obj.getKind() == RubyMixinElementInfo.K_MODULE) { IType type2 = (IType) obj.getObject(); if (type2 != null && (prefix == null || type2.getElementName() .startsWith(prefix))) { types.add(type2); } } else if (obj.getKind() == RubyMixinElementInfo.K_METHOD) { IMethod method2 = (IMethod) obj.getObject(); if (method2 != null && (prefix == null || method2.getElementName() .startsWith(prefix))) { methods.add(method2); } } if (obj.getKind() == RubyMixinElementInfo.K_VARIABLE) { IField fff = (IField) obj.getObject(); if (fff != null && (prefix == null || fff.getElementName() .startsWith(prefix))) { fields.add(fff); } } break; } } for (Iterator<IField> iterator = fields.iterator(); iterator.hasNext();) { IField t = iterator.next(); reportField(t, RELEVANCE_VARIABLES); } for (Iterator<IType> iterator = types.iterator(); iterator.hasNext();) { IType t = iterator.next(); reportType(t, RELEVANCE_TYPE); } for (Iterator<IMethod> iterator = methods.iterator(); iterator.hasNext();) { IMethod t = iterator.next(); reportMethod(t, RELEVANCE_METHODS); } } private void completeColonExpression(ModuleDeclaration moduleDeclaration, RubyColonExpression node, int position) { String content; try { content = currentModule.getSource(); } catch (ModelException e) { return; } int pos = (node.getLeft() != null) ? (node.getLeft().sourceEnd() + 2) : (node.sourceStart()); String starting = null; try { starting = content.substring(pos, position).trim(); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); return; } if (starting.startsWith("::")) { //$NON-NLS-1$ this.setSourceRange(position - starting.length() + 2, position); completeConstant(moduleDeclaration, starting.substring(2), position, true); return; } this.setSourceRange(position - starting.length(), position); ExpressionTypeGoal goal = new ExpressionTypeGoal(new BasicContext( currentModule, moduleDeclaration), node.getLeft()); IEvaluatedType type = inferencer.evaluateType(goal, TI_TIMEOUT * 3 / 2); reportSubElements(type, starting); } private void completeConstant(ModuleDeclaration moduleDeclaration, String prefix, int position, boolean topLevelOnly) { IType[] types = RubyTypeInferencingUtils.getAllTypes(currentModule, prefix); Arrays.sort(types, new ProjectTypeComparator(currentModule)); final Set<String> names = new HashSet<String>(); for (int i = 0; i < types.length; i++) { final String elementName = types[i].getElementName(); if (names.add(elementName)) { reportType(types[i], RELEVANCE_TYPE); } } if (!topLevelOnly) { IMixinElement[] modelStaticScopes = RubyTypeInferencingUtils .getModelStaticScopes(mixinModel.getRawModel(), moduleDeclaration, position); for (int i = modelStaticScopes.length - 1; i >= 0; i--) { IMixinElement scope = modelStaticScopes[i]; if (scope == null) continue; reportSubElements(new RubyClassType(scope.getKey()), prefix); } } if (prefix != null && prefix.length() > 0) { String varkey = "Object" + MixinModel.SEPARATOR + prefix; //$NON-NLS-1$ String[] keys2 = mixinModel.getRawModel().findKeys(varkey + "*", new NullProgressMonitor()); //$NON-NLS-1$ for (int i = 0; i < keys2.length; i++) { IRubyMixinElement element = mixinModel .createRubyElement(keys2[i]); if (element instanceof RubyMixinVariable) { RubyMixinVariable variable = (RubyMixinVariable) element; IField[] sourceFields = variable.getSourceFields(); for (int j = 0; j < sourceFields.length; j++) { if (sourceFields[j] != null) { reportField(sourceFields[j], RELEVANCE_VARIABLES); break; } } } } } } private void completeConstant(ModuleDeclaration moduleDeclaration, ConstantReference node, int position) { String content; try { content = currentModule.getSource(); } catch (ModelException e) { return; } String prefix = content.substring(node.sourceStart(), position); this.setSourceRange(position - prefix.length(), position); completeConstant(moduleDeclaration, prefix, position, false); } private void completeCall(ModuleDeclaration moduleDeclaration, CallExpression node, int position) { ASTNode receiver = node.getReceiver(); String content; try { content = currentModule.getSource(); } catch (ModelException e) { return; } int pos = (receiver != null) ? (receiver.sourceEnd() == node .sourceStart()) ? receiver.sourceStart() : (receiver .sourceEnd() + 1) : (node.sourceStart()); for (int t = 0; t < 2; t++) { // correct not more 2 chars if (pos < position && !RubySyntaxUtils.isStrictIdentifierCharacter(content .charAt(pos))) // for (...).name and Foo::name // calls pos++; } String starting = content.substring(pos, position).trim(); if ((receiver == null) || (receiver.sourceEnd() == node.sourceStart())) { completeSimpleRef(moduleDeclaration, starting, position); completeConstant(moduleDeclaration, starting, position, false); } this.setSourceRange(position - starting.length(), position); if (starting.startsWith("__")) { //$NON-NLS-1$ String[] keywords = RubyKeyword.findByPrefix("__"); //$NON-NLS-1$ for (int j = 0; j < keywords.length; j++) { reportKeyword(keywords[j]); } } if ((receiver != null) && (receiver.sourceEnd() != node.sourceStart())) { completeClassMethods(moduleDeclaration, receiver, starting); } else { IEvaluatedType self = RubyTypeInferencingUtils.determineSelfClass( mixinModel, currentModule, moduleDeclaration, position); if ((self != null) && "Object".equals(self.getTypeName())) { //$NON-NLS-1$ ASTNode minNode = node; ASTNode[] wayToNode = ASTUtils.restoreWayToNode( moduleDeclaration, node); for (int cnt = (wayToNode.length - 2); cnt >= 0; cnt--) { if ((wayToNode[cnt] instanceof TypeDeclaration) || (wayToNode[cnt] instanceof ModuleDeclaration)) { minNode = wayToNode[cnt]; break; } } ExpressionTypeGoal goal = new ExpressionTypeGoal( new BasicContext(currentModule, moduleDeclaration), minNode); IEvaluatedType self2 = inferencer .evaluateType(goal, TI_TIMEOUT); if (self2 != null) { self = self2; } } completeClassMethods(moduleDeclaration, self, starting, true); } } private void reportMethod(IMethod method, int rel) { this.intresting.add(method); String elementName = method.getElementName(); if (completedNames.contains(elementName)) { return; } completedNames.add(elementName); if (elementName.indexOf('.') != -1) { elementName = elementName.substring(elementName.indexOf('.') + 1); } String name = elementName; String compl = name; // accept result noProposal = false; if (!requestor.isIgnored(CompletionProposal.METHOD_REF)) { CompletionProposal proposal = createProposal( CompletionProposal.METHOD_REF, actualCompletionPosition); String[] params = null; try { params = method.getParameterNames(); } catch (ModelException e) { // ssanders: Ignore } if (params != null && params.length > 0) { proposal.setParameterNames(params); } proposal.setModelElement(method); proposal.setName(name); proposal.setCompletion(compl); try { proposal.setFlags(method.getFlags()); } catch (ModelException e) { // ssanders: Ignore } proposal.setReplaceRange(this.startPosition - this.offset, this.endPosition - this.offset); proposal.setRelevance(rel); this.requestor.accept(proposal); if (DEBUG) { this.printDebug(proposal); } } } private void reportType(IType type, int rel) { this.intresting.add(type); String elementName = type.getElementName(); if (completedNames.contains(elementName)) { return; } completedNames.add(elementName); String name = elementName; if (name.length() == 0) return; // accept result noProposal = false; if (!requestor.isIgnored(CompletionProposal.TYPE_REF)) { CompletionProposal proposal = createProposal( CompletionProposal.TYPE_REF, actualCompletionPosition); proposal.setModelElement(type); proposal.setName(name); proposal.setCompletion(elementName); // proposal.setFlags(Flags.AccDefault); try { proposal.setFlags(type.getFlags()); } catch (ModelException e) { } proposal.setReplaceRange(this.startPosition - this.offset, this.endPosition - this.offset); proposal.setRelevance(rel); this.requestor.accept(proposal); if (DEBUG) { this.printDebug(proposal); } } } private void reportField(IField field, int rel) { this.intresting.add(field); String elementName = field.getElementName(); if (completedNames.contains(elementName)) { return; } completedNames.add(elementName); String name = elementName; if (name.length() == 0) return; // accept result noProposal = false; if (!requestor.isIgnored(CompletionProposal.FIELD_REF)) { CompletionProposal proposal = createProposal( CompletionProposal.FIELD_REF, actualCompletionPosition); proposal.setModelElement(field); proposal.setName(name); proposal.setCompletion(elementName); // proposal.setFlags(Flags.AccDefault); proposal.setReplaceRange(this.startPosition - this.offset, this.endPosition - this.offset); proposal.setRelevance(rel); this.requestor.accept(proposal); if (DEBUG) { this.printDebug(proposal); } } } private void reportKeyword(String name) { // accept result noProposal = false; if (!requestor.isIgnored(CompletionProposal.KEYWORD)) { CompletionProposal proposal = createProposal( CompletionProposal.KEYWORD, actualCompletionPosition); proposal.setName(name); proposal.setCompletion(name); // proposal.setFlags(Flags.AccDefault); proposal.setReplaceRange(this.startPosition - this.offset, this.endPosition - this.offset); proposal.setRelevance(RELEVANCE_KEYWORD); this.requestor.accept(proposal); if (DEBUG) { this.printDebug(proposal); } } } }