ruby/src/org/netbeans/modules/ruby/RubyCodeCompleter.java
author enebo@netbeans.org
Sat, 19 Apr 2014 16:26:21 -0500
changeset 4557 123da219e4f8
parent 4549 a8ced3d20fca
permissions -rw-r--r--
zero-sum cleanup
     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
    31  * Microsystems, Inc. All Rights Reserved.
    32  *
    33  * If you wish your version of this file to be governed by only the CDDL
    34  * or only the GPL Version 2, indicate your decision by adding
    35  * "[Contributor] elects to include this software in this distribution
    36  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    37  * single choice of license, a recipient has the option to distribute
    38  * your version of this file under either the CDDL, the GPL Version 2 or
    39  * to extend the choice of license to its licensees as provided above.
    40  * However, if you add GPL Version 2 code and therefore, elected the GPL
    41  * Version 2 license, then the option applies only if the new code is
    42  * made subject to such option by the copyright holder.
    43  */
    44 package org.netbeans.modules.ruby;
    45 
    46 import java.io.BufferedInputStream;
    47 import java.io.IOException;
    48 import java.io.InputStream;
    49 import java.util.ArrayList;
    50 import java.util.Collections;
    51 import java.util.HashMap;
    52 import java.util.HashSet;
    53 import java.util.List;
    54 import java.util.Map;
    55 import java.util.Set;
    56 import javax.swing.text.BadLocationException;
    57 import javax.swing.text.Document;
    58 import javax.swing.text.JTextComponent;
    59 import org.jrubyparser.SourcePosition;
    60 import org.jrubyparser.ast.ArgsNode;
    61 import org.jrubyparser.ast.ArgumentNode;
    62 import org.jrubyparser.ast.ClassNode;
    63 import org.jrubyparser.ast.ILocalScope;
    64 import org.jrubyparser.ast.ListNode;
    65 import org.jrubyparser.ast.LocalAsgnNode;
    66 import org.jrubyparser.ast.MethodDefNode;
    67 import org.jrubyparser.ast.Node;
    68 import org.jrubyparser.ast.NodeType;
    69 import org.jrubyparser.ast.INameNode;
    70 import org.netbeans.api.lexer.Token;
    71 import org.netbeans.api.lexer.TokenHierarchy;
    72 import org.netbeans.api.lexer.TokenId;
    73 import org.netbeans.api.lexer.TokenSequence;
    74 import org.netbeans.api.ruby.platform.RubyInstallation;
    75 import org.netbeans.editor.BaseDocument;
    76 import org.netbeans.editor.Utilities;
    77 import org.netbeans.modules.csl.api.CodeCompletionContext;
    78 import org.netbeans.modules.csl.api.CodeCompletionHandler;
    79 import org.netbeans.modules.csl.api.CodeCompletionHandler.QueryType;
    80 import org.netbeans.modules.csl.api.CodeCompletionResult;
    81 import org.netbeans.modules.csl.api.CompletionProposal;
    82 import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
    83 import org.netbeans.modules.csl.api.ElementHandle;
    84 import org.netbeans.modules.csl.api.ElementKind;
    85 import org.netbeans.modules.csl.api.ParameterInfo;
    86 import org.netbeans.modules.csl.spi.DefaultCompletionResult;
    87 import org.netbeans.modules.csl.spi.ParserResult;
    88 import org.netbeans.modules.parsing.api.Snapshot;
    89 import org.netbeans.modules.parsing.api.Source;
    90 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
    91 import org.netbeans.modules.ruby.RubyCompletionItem.CallItem;
    92 import org.netbeans.modules.ruby.RubyCompletionItem.ClassItem;
    93 import org.netbeans.modules.ruby.RubyCompletionItem.FieldItem;
    94 import org.netbeans.modules.ruby.RubyCompletionItem.MethodItem;
    95 import org.netbeans.modules.ruby.RubyCompletionItem.ParameterItem;
    96 import org.netbeans.modules.ruby.elements.AstElement;
    97 import org.netbeans.modules.ruby.elements.AstFieldElement;
    98 import org.netbeans.modules.ruby.elements.AstNameElement;
    99 import org.netbeans.modules.ruby.elements.CommentElement;
   100 import org.netbeans.modules.ruby.elements.Element;
   101 import org.netbeans.modules.ruby.elements.IndexedClass;
   102 import org.netbeans.modules.ruby.elements.IndexedElement;
   103 import org.netbeans.modules.ruby.elements.IndexedField;
   104 import org.netbeans.modules.ruby.elements.IndexedMethod;
   105 import org.netbeans.modules.ruby.elements.IndexedVariable;
   106 import org.netbeans.modules.ruby.elements.KeywordElement;
   107 import org.netbeans.modules.ruby.elements.RubyElement;
   108 import org.netbeans.modules.ruby.lexer.Call;
   109 import org.netbeans.modules.ruby.lexer.LexUtilities;
   110 import org.netbeans.modules.ruby.lexer.RubyStringTokenId;
   111 import org.netbeans.modules.ruby.lexer.RubyTokenId;
   112 import org.openide.filesystems.FileObject;
   113 import org.openide.filesystems.FileUtil;
   114 import org.openide.util.Exceptions;
   115 import org.openide.util.NbBundle;
   116 
   117 import static org.netbeans.modules.ruby.RubyUtils.*;
   118 
   119 /**
   120  * Code completion handler for Ruby.
   121  * 
   122  * Bug: I add lists of fields etc. But if these -overlap- the current line,
   123  *  I throw them away. The problem is that there may be other references
   124  *  to the field that I should -not- throw away, elsewhere!
   125  * @todo Ensure that I prefer assignment over reference such that javadoc is
   126  *   more likely to be there!
   127  *   
   128  * 
   129  * @todo Handle this case:  {@code class HTTPBadResponse < StandardError; end}
   130  * @todo Code completion should automatically suggest "initialize()" for def completion! (if I'm in a class)
   131  * @todo It would be nice if you select a method that takes a block, such as Array.each, if we could
   132  *   insert a { ^ } suffix
   133  * @todo Use lexical tokens to avoid attempting code completion within comments,
   134  *   literal strings and regexps
   135  * @todo Percent-completion doesn't work if you at this to the end of the
   136  *   document:  x = %    and try to complete.
   137  * @todo Handle more completion scenarios: Classes (no keywords) after "class Foo <",
   138  *   classes after "::", parameter completion (!!!), .new() completion (initialize), etc.
   139  * @todo Make sure completion works during a "::"
   140  * @todo I need to move the smart-determination from just checking in=Object/Class/Module
   141  *   to the code which computes matches, since we have for example ObjectMixin in pretty printer
   142  *   which adds mixin methods to Object.
   143  * @todo Handle Rails methods that deal with hashes:
   144  *    - Try figuring out whether the method should take parameters by looking for examples;
   145  *      lines that start with the method name and looks like it might have arguments
   146  *    - Try to figure out what the different parameters are if there are hashes
   147  *    - &lt;tt&gt;: looks like a parameter, e.g. "<tt>:filename</tt>"
   148  *      and to see which parameter it might correspond to, see the
   149  *      label; see if any of the parameter names are listed there (possibly in the args list)
   150  *      A fallback is to look for args that look like they may be hashes, e.g.  
   151  *      def(foo1, foo2, foo3={}) - the third one is obviously a hash
   152  * @todo Make code completion when we're in a parameter list include the parameters as well!
   153  * @todo For .rjs files, insert an object named "page" of type 
   154  *    ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods
   155  *    (#105088)
   156  * @todo For .builder files, insert an object named "xml" of type
   157  *    Builder::XmlMarkup
   158  * @todo For .rhtml/.html.erb files, insert fields etc. as documented in actionpack's lib/action_view/base.rb
   159  *    (#105095)
   160  * @todo For test files in Rails, get testing context (#105043). In particular, actionpack's
   161  *    ActionController::Assertions needs to be pulled in. This happens in action_controller/assertions.rb.
   162  * @todo Require-completion should handle ruby gems; it should provide the "preferred" (entry-point) files for
   163  *    all the ruby gems, and it should hide all the files that are inside the gem
   164  * @todo Rakefiles files should inherit Rakefile context
   165  * @todo See http://blog.diegodoval.com/2007/09/ruby_on_os_x_some_useful_links.html
   166  * @todo Documentation completion in a rdoc should preview that rdoc section
   167  * @todo Make a dedicated completion item which I return on documentation completion if I want  to
   168  *    complete the CURRENT element; it basically just wraps the desired comment so we can pull it
   169  *    out in the document() method
   170  * @todo Provide code completion for "3|" or "3 |" - show available overloaded operators! This
   171  *    shouldn't just apply to numbers - any class you've overridden
   172  * @todo Digest http://blogs.sun.com/coolstuff/entry/using_java_classes_in_jruby
   173  *    to fix require'java' etc.
   174  * @todo http://www.innovationontherun.com/scraping-dynamic-websites-using-jruby-and-htmlunit/
   175  *    Idea: Use a quicktip to require all the jars in the project?
   176  * @todo The "h" method in <%= %> doesn't show up in RHTML files... where is it?
   177  * @todo Completion AFTER a method which takes a block (optional or required) should offer
   178  *    { } and do/end !!
   179  * @author Tor Norbye
   180  */
   181 public class RubyCodeCompleter implements CodeCompletionHandler {
   182 
   183     // Another good logical parameter would be SINGLE_WHITESPACE which would
   184     // insert a whitespace separator IF NEEDED
   185 
   186     /** Live code template parameter: require the given file, if not already done so */
   187     private static final String KEY_REQUIRE = "require"; // NOI18N
   188 
   189     /** Live code template parameter: find a name in scope that is known to be of the given type */
   190     private static final String KEY_INSTANCEOF = "instanceof"; // NOI18N
   191 
   192     /** Live code template parameter: compute an unused local variable name */
   193     private static final String ATTR_UNUSEDLOCAL = "unusedlocal"; // NOI18N
   194 
   195     /** Live code template parameter: pipe variable, since | is a bit mishandled in the UI for editing abbrevs */
   196     private static final String KEY_PIPE = "pipe"; // NOI18N
   197 
   198     /** Live code template parameter: compute the method name */
   199     private static final String KEY_METHOD = "method"; // NOI18N
   200 
   201     /** Live code template parameter: compute the method signature */
   202     private static final String KEY_METHOD_FQN = "methodfqn"; // NOI18N
   203 
   204     /** Live code template parameter: compute the class name (not including the module prefix) */
   205     private static final String KEY_CLASS = "class"; // NOI18N
   206 
   207     /** Live code template parameter: compute the class fully qualified name */
   208     private static final String KEY_CLASS_FQN = "classfqn"; // NOI18N
   209 
   210     /** Live code template parameter: compute the superclass of the current class */
   211     private static final String KEY_SUPERCLASS = "superclass"; // NOI18N
   212 
   213     /** Live code template parameter: compute the filename (not including the path) of the file */
   214     private static final String KEY_FILE = "file"; // NOI18N
   215 
   216     /** Live code template parameter: compute the full path of the source directory */
   217     private static final String KEY_PATH = "path"; // NOI18N
   218 
   219     /** Default name values for ATTR_UNUSEDLOCAL and friends */
   220     private static final String ATTR_DEFAULTS = "defaults"; // NOI18N
   221 
   222     private static final Set<String> selectionTemplates = new HashSet<String>();
   223 
   224     static {
   225         selectionTemplates.add("begin"); // NOI18N
   226         selectionTemplates.add("do"); // NOI18N
   227         selectionTemplates.add("doc"); // NOI18N
   228         //selectionTemplates.add("dop"); // NOI18N
   229         selectionTemplates.add("if"); // NOI18N
   230         selectionTemplates.add("ife"); // NOI18N
   231     }
   232 
   233     private boolean caseSensitive;
   234     private int anchor;
   235 
   236     public RubyCodeCompleter() {
   237     }
   238 
   239     static boolean startsWith(String theString, String prefix, boolean caseSensitive) {
   240         if (prefix.length() == 0) return true;
   241 
   242         return caseSensitive ? theString.startsWith(prefix)
   243                              : theString.toLowerCase().startsWith(prefix.toLowerCase());
   244     }
   245 
   246     private boolean startsWith(String theString, String prefix) {
   247         return RubyCodeCompleter.startsWith(theString, prefix, caseSensitive);
   248     }
   249 
   250     /**
   251      * Compute an appropriate prefix to use for code completion.
   252      * In Strings, we want to return the -whole- string if you're in a
   253      * require-statement string, otherwise we want to return simply "" or the previous "\"
   254      * for quoted strings, and ditto for regular expressions.
   255      * For non-string contexts, just return null to let the default identifier-computation
   256      * kick in.
   257      */
   258     @SuppressWarnings("unchecked")
   259     @Override
   260     public String getPrefix(ParserResult info, int lexOffset, boolean upToOffset) {
   261         try {
   262             BaseDocument doc = RubyUtils.getDocument(info);
   263             if (doc == null) return null;
   264 
   265             TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
   266             doc.readLock(); // Read-lock due to token hierarchy use
   267             try {
   268             int requireStart = LexUtilities.getRequireStringOffset(lexOffset, th);
   269 
   270             // XXX todo - do upToOffset
   271             if (requireStart != -1) return doc.getText(requireStart, lexOffset - requireStart);
   272 
   273             TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
   274             if (ts == null) return null;
   275 
   276             ts.move(lexOffset);
   277 
   278             if (!ts.moveNext() && !ts.movePrevious()) return null;
   279 
   280             // We're looking at the offset to the RIGHT of the caret and here I care about what's on the left
   281             if (ts.offset() == lexOffset) ts.movePrevious();
   282 
   283             Token<? extends RubyTokenId> token = ts.token();
   284 
   285             if (token != null) {
   286                 TokenId id = token.id();
   287 
   288                 // We're within a String that has embedded Ruby. Drop into the
   289                 // embedded language and see if we're within a literal string there.
   290                 if (id == RubyTokenId.EMBEDDED_RUBY) {
   291                     ts = (TokenSequence) ts.embedded();
   292                     assert ts != null;
   293                     ts.move(lexOffset);
   294 
   295                     if (!ts.moveNext() && !ts.movePrevious()) return null;
   296 
   297                     token = ts.token();
   298                     id = token.id();
   299                 }
   300 
   301                 String tokenText = token.text().toString();
   302 
   303                 if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
   304                         ((id == RubyTokenId.ERROR) && tokenText.equals("%"))) {
   305                     int currOffset = ts.offset();
   306 
   307                     // Percent completion
   308                     if ((currOffset == (lexOffset - 1)) && (tokenText.length() > 0) &&
   309                             (tokenText.charAt(0) == '%')) {
   310                         return "%";
   311                     }
   312                 }
   313             }
   314 
   315             int doubleQuotedOffset = LexUtilities.getDoubleQuotedStringOffset(lexOffset, th);
   316 
   317             if (doubleQuotedOffset != -1) {
   318                 // Tokenize the string and offer the current token portion as the text
   319                 if (doubleQuotedOffset == lexOffset) {
   320                     return "";
   321                 } else if (doubleQuotedOffset < lexOffset) {
   322                     String text = doc.getText(doubleQuotedOffset, lexOffset - doubleQuotedOffset);
   323                     TokenHierarchy hi =
   324                             TokenHierarchy.create(text, RubyStringTokenId.languageDouble());
   325 
   326                     TokenSequence seq = hi.tokenSequence();
   327 
   328                     seq.move(lexOffset - doubleQuotedOffset);
   329 
   330                     if (!seq.moveNext() && !seq.movePrevious()) {
   331                         return "";
   332                     }
   333 
   334                     TokenId id = seq.token().id();
   335                     String s = seq.token().text().toString();
   336 
   337                     if ((id == RubyStringTokenId.STRING_ESCAPE) ||
   338                             (id == RubyStringTokenId.STRING_INVALID)) {
   339                         return s;
   340                     } else if (s.startsWith("\\")) {
   341                         return s;
   342                     } else {
   343                         return "";
   344                     }
   345                 } else {
   346                     // The String offset is greater than the caret position.
   347                     // This means that we're inside the string-begin section,
   348                     // for example here: %q|(
   349                     // In this case, report no prefix
   350                     return "";
   351                 }
   352             }
   353 
   354             int singleQuotedOffset = LexUtilities.getSingleQuotedStringOffset(lexOffset, th);
   355 
   356             if (singleQuotedOffset != -1) {
   357                 if (singleQuotedOffset == lexOffset) {
   358                     return "";
   359                 } else if (singleQuotedOffset < lexOffset) {
   360                     String text = doc.getText(singleQuotedOffset, lexOffset - singleQuotedOffset);
   361                     TokenHierarchy hi =
   362                             TokenHierarchy.create(text, RubyStringTokenId.languageSingle());
   363 
   364                     TokenSequence seq = hi.tokenSequence();
   365 
   366                     seq.move(lexOffset - singleQuotedOffset);
   367 
   368                     if (!seq.moveNext() && !seq.movePrevious()) {
   369                         return "";
   370                     }
   371 
   372                     TokenId id = seq.token().id();
   373                     String s = seq.token().text().toString();
   374 
   375                     if ((id == RubyStringTokenId.STRING_ESCAPE) ||
   376                             (id == RubyStringTokenId.STRING_INVALID)) {
   377                         return s;
   378                     } else if (s.startsWith("\\")) {
   379                         return s;
   380                     } else {
   381                         return "";
   382                     }
   383                 } else {
   384                     // The String offset is greater than the caret position.
   385                     // This means that we're inside the string-begin section,
   386                     // for example here: %q|(
   387                     // In this case, report no prefix
   388                     return "";
   389                 }
   390             }
   391 
   392             // Regular expression
   393             int regexpOffset = LexUtilities.getRegexpOffset(lexOffset, th);
   394 
   395             if ((regexpOffset != -1) && (regexpOffset <= lexOffset)) {
   396                 // This is not right... I need to actually parse the regexp
   397                 // (I should use my Regexp lexer tokens which will be embedded here)
   398                 // such that escaping sequences (/\\\\\/) will work right, or
   399                 // character classes (/[foo\]). In both cases the \ may not mean escape.
   400                 String tokenText = token.text().toString();
   401                 int index = lexOffset - ts.offset();
   402 
   403                 if ((index > 0) && (index <= tokenText.length()) &&
   404                         (tokenText.charAt(index - 1) == '\\')) {
   405                     return "\\";
   406                 } else {
   407                     // No prefix for regexps unless it's \
   408                     return "";
   409                 }
   410 
   411                 //return doc.getText(regexpOffset, offset-regexpOffset);
   412             }
   413 
   414             int lineBegin = Utilities.getRowStart(doc, lexOffset);
   415             if (lineBegin != -1) {
   416                 int lineEnd = Utilities.getRowEnd(doc, lexOffset);
   417                 String line = doc.getText(lineBegin, lineEnd - lineBegin);
   418                 int lineOffset = lexOffset - lineBegin;
   419                 int start = lineOffset;
   420                 if (lineOffset > 0) {
   421                     for (int i = lineOffset - 1; i >= 0; i--) {
   422                         char c = line.charAt(i);
   423                         if (!RubyUtils.isIdentifierChar(c)) {
   424                             break;
   425                         } else {
   426                             start = i;
   427                         }
   428                     }
   429                 }
   430 
   431                 // Find identifier end
   432                 String prefix;
   433                 if (upToOffset) {
   434                     prefix = line.substring(start, lineOffset);
   435                 } else {
   436                     if (lineOffset == line.length()) {
   437                         prefix = line.substring(start);
   438                     } else {
   439                         int n = line.length();
   440                         int end = lineOffset;
   441                         for (int j = lineOffset; j < n; j++) {
   442                             char d = line.charAt(j);
   443                             // Try to accept Foo::Bar as well
   444                             if (!RubyUtils.isStrictIdentifierChar(d)) {
   445                                 break;
   446                             } else {
   447                                 end = j + 1;
   448                             }
   449                         }
   450                         prefix = line.substring(start, end);
   451                     }
   452                 }
   453 
   454                 if (prefix.length() > 0) {
   455                     if (prefix.endsWith("::")) {
   456                         return "";
   457                     }
   458 
   459                     if (prefix.endsWith(":") && prefix.length() > 1) {
   460                         return null;
   461                     }
   462 
   463                     // Strip out LHS if it's a qualified method, e.g.  Benchmark::measure -> measure
   464                     int q = prefix.lastIndexOf("::");
   465 
   466                     if (q != -1) {
   467                         prefix = prefix.substring(q + 2);
   468                     }
   469 
   470                     // The identifier chars identified by RubyLanguage are a bit too permissive;
   471                     // they include things like "=", "!" and even "&" such that double-clicks will
   472                     // pick up the whole "token" the user is after. But "=" is only allowed at the
   473                     // end of identifiers for example.
   474                     if (prefix.length() == 1) {
   475                         char c = prefix.charAt(0);
   476                         if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$' || c == ':')) {
   477                             return null;
   478                         }
   479                     } else {
   480                         for (int i = prefix.length() - 2; i >= 0; i--) { // -2: the last position (-1) can legally be =, ! or ?
   481                             char c = prefix.charAt(i);
   482                             if (i == 0 && c == ':') {
   483                                 // : is okay at the begining of prefixes
   484                             } else if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$')) {
   485                                 prefix = prefix.substring(i + 1);
   486                                 break;
   487                             }
   488                         }
   489                     }
   490 
   491                     return prefix;
   492                 }
   493             }
   494             } finally {
   495                 doc.readUnlock();
   496             }
   497             // Else: normal identifier: just return null and let the machinery do the rest
   498         } catch (BadLocationException ble) {
   499             // do nothing - see #154991;
   500         }
   501 
   502         // Default behavior
   503         return null;
   504     }
   505 
   506     /** Determine if we're trying to complete the name for a "def" (in which case
   507      * we'd show the inherited methods).
   508      * This needs to be enhanced to handle "Foo." prefixes, e.g. def self.foo
   509      */
   510     private boolean completeDefOrInclude(List<CompletionProposal> proposals, CompletionRequest request, String fqn) {
   511         RubyIndex index = request.index;
   512         String prefix = request.prefix;
   513         int lexOffset = request.lexOffset;
   514         TokenHierarchy<Document> th = request.th;
   515         QuerySupport.Kind kind = request.kind;
   516         
   517         TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
   518 
   519         if ((index != null) && (ts != null)) {
   520             ts.move(lexOffset);
   521 
   522             if (!ts.moveNext() && !ts.movePrevious()) {
   523                 return false;
   524             }
   525 
   526             if (ts.offset() == lexOffset) {
   527                 // We're looking at the offset to the RIGHT of the caret
   528                 // position, which could be whitespace, e.g.
   529                 //  "def fo| " <-- looking at the whitespace
   530                 ts.movePrevious();
   531             }
   532 
   533             Token<?extends RubyTokenId> token = ts.token();
   534 
   535             if (token != null) {
   536                 TokenId id = token.id();
   537 
   538                 // See if we're in the identifier - "foo" in "def foo"
   539                 // I could also be a keyword in case the prefix happens to currently
   540                 // match a keyword, such as "next"
   541                 if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CONSTANT) || id.primaryCategory().equals("keyword")) {
   542                     if (!ts.movePrevious()) {
   543                         return false;
   544                     }
   545 
   546                     token = ts.token();
   547                     id = token.id();
   548                 }
   549 
   550                 // If we're not in the identifier we need to be in the whitespace after "def"
   551                 if (id != RubyTokenId.WHITESPACE) {
   552                     // Do something about http://www.netbeans.org/issues/show_bug.cgi?id=100452 here
   553                     // In addition to checking for whitespace I should look for "Foo." here
   554                     return false;
   555                 }
   556 
   557                 // There may be more than one whitespace; skip them
   558                 while (ts.movePrevious()) {
   559                     token = ts.token();
   560 
   561                     if (token.id() != RubyTokenId.WHITESPACE) {
   562                         break;
   563                     }
   564                 }
   565 
   566                 if (token.id() == RubyTokenId.DEF) {
   567                     Set<IndexedMethod> methods = index.getInheritedMethods(fqn, prefix, kind);
   568 
   569                     for (IndexedMethod method : methods) {
   570                         // Hmmm, is this necessary? Filtering should happen in the getInheritedMEthods call
   571                         if ((prefix.length() > 0) && !method.getName().startsWith(prefix)) {
   572                             continue;
   573                         }
   574 
   575                         // For def completion, skip local methods, only include superclass and included
   576                         if ((fqn != null) && fqn.equals(method.getClz())) {
   577                             continue;
   578                         }
   579                         
   580                         if (method.isNoDoc()) {
   581                             continue;
   582                         }
   583 
   584                         // If a method is an "initialize" method I should do something special so that
   585                         // it shows up as a "constructor" (in a new() statement) but not as a directly
   586                         // callable initialize method (it should already be culled because it's private)
   587                         MethodItem item = new MethodItem(method, anchor, request);
   588                         // Exact matches
   589                         item.setSmart(method.isSmart());
   590                         proposals.add(item);
   591                     }
   592 
   593                     return true;
   594                 } else if (token.id() == RubyTokenId.IDENTIFIER && "include".equals(token.text().toString())) {
   595                     // Module completion
   596                     Set<IndexedClass> classes = index.getClasses(prefix, kind, false, true, false);
   597                     for (IndexedClass clz : classes) {
   598                         if (clz.isNoDoc()) {
   599                             continue;
   600                         }
   601                         
   602                         ClassItem item = new ClassItem(clz, anchor, request);
   603                         item.setSmart(true);
   604                         proposals.add(item);
   605                     }     
   606                     
   607                     return true;
   608                 }
   609             }
   610         }
   611 
   612         return false;
   613     }
   614     
   615     private void completeGlobals(List<CompletionProposal> proposals, CompletionRequest request, boolean showSymbols) {
   616         RubyIndex index = request.index;
   617         String prefix = request.prefix;
   618         QuerySupport.Kind kind = request.kind;
   619         
   620         Set<IndexedVariable> globals = index.getGlobals(prefix, kind);
   621         for (IndexedVariable global : globals) {
   622             RubyCompletionItem item = new RubyCompletionItem(global, anchor, request);
   623             item.setSmart(true);
   624 
   625             if (showSymbols) {
   626                 item.setSymbol(true);
   627             }
   628             
   629             proposals.add(item);
   630         }
   631     }
   632 
   633     private boolean addParameters(List<CompletionProposal> proposals, CompletionRequest request) {
   634         IndexedMethod[] methodHolder = new IndexedMethod[1];
   635         @SuppressWarnings("unchecked")
   636         Set<IndexedMethod>[] alternatesHolder = new Set[1];
   637         int[] paramIndexHolder = new int[1];
   638         int[] anchorOffsetHolder = new int[1];
   639         ParserResult info = request.parserResult;
   640         int lexOffset = request.lexOffset;
   641         int astOffset = request.astOffset;
   642         if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
   643                 methodHolder, paramIndexHolder, anchorOffsetHolder, alternatesHolder, request.kind)) {
   644 
   645             return false;
   646         }
   647 
   648         IndexedMethod targetMethod = methodHolder[0];
   649         int index = paramIndexHolder[0];
   650         
   651         CallItem callItem = new CallItem(targetMethod, index, anchor, request);
   652         proposals.add(callItem);
   653         // Also show other documented, not nodoc'ed items (except for those
   654         // with identical signatures, such as overrides of the same method)
   655         if (alternatesHolder[0] != null) {
   656             Set<String> signatures = new HashSet<String>();
   657             signatures.add(targetMethod.getSignature().substring(targetMethod.getSignature().indexOf('#')+1));
   658             for (IndexedMethod m : alternatesHolder[0]) {
   659                 if (m != targetMethod && m.isDocumented() && !m.isNoDoc()) {
   660                     String sig = m.getSignature().substring(m.getSignature().indexOf('#')+1);
   661                     if (!signatures.contains(sig)) {
   662                         CallItem item = new CallItem(m, index, anchor, request);
   663                         proposals.add(item);
   664                         signatures.add(sig);
   665                     }
   666                 }
   667             }
   668         }
   669         
   670         List<String> params = targetMethod.getParameters();
   671         if (params == null || params.isEmpty()) {
   672             return false;
   673         }
   674 
   675         if  (params.size() <= index) {
   676             // Just use the last parameter in these cases
   677             // See for example the TableDefinition.binary dynamic method where
   678             // you can add a number of parameter names and the options parameter
   679             // is always the last one
   680             index = params.size()-1;
   681         }
   682 
   683         boolean isLastArg = index < params.size()-1;
   684         
   685         String attrs = targetMethod.getEncodedAttributes();
   686         if (attrs != null && attrs.length() > 0) {
   687             int offset = -1;
   688             for (int i = 0; i < 3; i++) {
   689                 offset = attrs.indexOf(';', offset+1);
   690                 if (offset == -1) {
   691                     break;
   692                 }
   693             }
   694             if (offset == -1) {
   695                 Node root = null;
   696                 if (info != null) {
   697                     root = AstUtilities.getRoot(info);
   698                 }
   699 
   700                 IndexedElement match = findDocumentationEntry(root, targetMethod);
   701                 if (match == targetMethod || !(match instanceof IndexedMethod)) {
   702                     return false;
   703                 }
   704                 targetMethod = (IndexedMethod)match;
   705                 attrs = targetMethod.getEncodedAttributes();
   706                 if (attrs != null && attrs.length() > 0) {
   707                     offset = -1;
   708                     for (int i = 0; i < 3; i++) {
   709                         offset = attrs.indexOf(';', offset+1);
   710                         if (offset == -1) {
   711                             break;
   712                         }
   713                     }
   714                 }
   715             }
   716             String currentName = params.get(index);
   717             if (currentName.startsWith("*")) {
   718                 // * and & are part of the sig
   719                 currentName = currentName.substring(1);
   720             } else if (currentName.startsWith("&")) {
   721                 currentName = currentName.substring(1);
   722             }
   723             if (offset != -1) {
   724                 // Pick apart
   725                 attrs = attrs.substring(offset+1);
   726                 if (attrs.length() == 0) {
   727                     return false;
   728                 }
   729                 String[] argEntries = attrs.split(",");
   730                 for (String entry : argEntries) {
   731                     int parenIndex = entry.indexOf('(');
   732                     assert parenIndex != -1 : attrs;
   733                     String name = entry.substring(0, parenIndex);
   734                     if  (currentName.equals(name)) {
   735                         // Found a special parameter desc entry for this
   736                         // parameter - decode it and create completion items
   737                         // Decode
   738                         int endIndex = entry.indexOf(')', parenIndex);
   739                         assert endIndex != -1;
   740                         String data = entry.substring(parenIndex+1, endIndex);
   741                         if (data.length() > 0 && data.charAt(0) == '-') {
   742                             // It's a plain item (e.g. not a hash etc) where
   743                             // we have some logical types to complete
   744                             if ("-table".equals(data)) {
   745                                 completeDbTables(proposals, targetMethod, request, isLastArg);
   746                                 // Not exiting - I may have other entries here too
   747                             } else if ("-column".equals(data)) {
   748                                 completeDbColumns(proposals, targetMethod, request, isLastArg);
   749                                 // Not exiting - I may have other entries here too
   750                             } else if ("-model".equals(data)) {
   751                                 completeModels(proposals, targetMethod, request, isLastArg);
   752                             }
   753                         } else if (data.startsWith("=>")) {
   754                             // It's a hash; show the given keys
   755                             // TODO: Determine if the caret is in the
   756                             // value part, and if so, show the values instead
   757                             // Uhm... what about fields and such?
   758                             completeHash(proposals, request, targetMethod, data, isLastArg);
   759                             // Not exiting - I may have a non-hash entry here too!
   760                         } else {
   761                             // Just show a fixed set of values
   762                             completeFixed(proposals, request, targetMethod, data, isLastArg);
   763                             // Not exiting - I may have other entries here too
   764                         }
   765                     }
   766                 }
   767             }
   768         }
   769         
   770         return true;
   771     }
   772 
   773 //    /** Handle insertion of :action, :controller, etc. even for methods without
   774 //     * actual method signatures. Operate at the lexical level.
   775 //     */
   776 //    private void handleRailsKeys(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
   777 //        TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
   778 //        if (ts == null) {
   779 //            return;
   780 //        }
   781 //        boolean inValue = false;
   782 //        ts.move(anchor);
   783 //        String line = null;
   784 //        while (ts.movePrevious()) {
   785 //            final Token token = ts.token();
   786 //            if (token.id() == RubyTokenId.WHITESPACE) {
   787 //                continue;
   788 //            } else if (token.id() == RubyTokenId.NONUNARY_OP &&
   789 //                    (token.text().toString().equals("=>"))) { // NOI18N
   790 //                inValue = true;
   791 //                // TODO - continue on to find out what the key is
   792 //                try {
   793 //                    BaseDocument doc = request.doc;
   794 //                    int lineStart = Utilities.getRowStart(doc, ts.offset());
   795 //                    line = doc.getText(lineStart, ts.offset()-lineStart).trim();
   796 //                } catch (BadLocationException ble) {
   797 //                    Exceptions.printStackTrace(ble);
   798 //                    return;
   799 //                }
   800 //            } else {
   801 //                break;
   802 //            }
   803 //        }
   804 //
   805 //        if (inValue) {
   806 //            if (line.endsWith(":action")) {
   807 //                // TODO
   808 //            } else if (line.endsWith(":controller")) {
   809 //                // Dynamically produce controllers
   810 //                List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
   811 //                String prefix = request.prefix;
   812 //                for (String n : controllers) {
   813 //                    n = "'" + n + "'";
   814 //                    if (startsWith(n, prefix)) {
   815 //                        String insert = n;
   816 //                        if (!isLastArg) {
   817 //                            insert = insert + ", ";
   818 //                        }
   819 //                        ParameterItem item = new ParameterItem(target, n, null, insert,  anchor, request);
   820 //                        item.setSymbol(true);
   821 //                        item.setSmart(true);
   822 //                        proposals.add(item);
   823 //                    }
   824 //                }
   825 //            } else if (line.endsWith(":partial")) {
   826 //                // TODO
   827 //            }
   828 //        }
   829 //    }
   830 
   831     private boolean completeHash(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
   832         assert data.startsWith("=>");
   833         data = data.substring(2);
   834         String prefix = request.prefix;
   835         
   836         // Determine if we're in the key part or the value part when completing
   837         boolean inValue = false;
   838         TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
   839         if (ts == null) {
   840             return false;
   841         }
   842         ts.move(anchor);
   843         String line = null;
   844         
   845         while (ts.movePrevious()) {
   846             final Token token = ts.token();
   847             if (token.id() == RubyTokenId.WHITESPACE) {
   848                 continue;
   849             } else if (token.id() == RubyTokenId.NONUNARY_OP &&
   850                     (token.text().toString().equals("=>"))) { // NOI18N
   851                 inValue = true;
   852                 // TODO - continue on to find out what the key is
   853                 try {
   854                     BaseDocument doc = request.doc;
   855                     int lineStart = Utilities.getRowStart(doc, ts.offset());
   856                     line = doc.getText(lineStart, ts.offset()-lineStart).trim();
   857                 } catch (BadLocationException ble) {
   858                     return false;
   859                 }
   860             } else {
   861                 break;
   862             }
   863         }
   864 
   865         List<String> suggestions = new ArrayList<String>();
   866         
   867         String key = null;
   868         String[] values = data.split("\\|");
   869         if (inValue) {
   870             // Find the key and see if we have a type to offer for it
   871             for (String value : values) {
   872                 int typeIndex = value.indexOf(':');
   873                 if (typeIndex != -1) {
   874                     String name = value.substring(0, typeIndex);
   875                     if (line.endsWith(name)) {
   876                         key = name;
   877                         // Score - it appears we're using the
   878                         // key for this item
   879                         String type = value.substring(typeIndex+1);
   880                         if ("nil".equals(type)) { // NOI18N
   881                             suggestions.add("nil"); // NOI18N
   882                         } else if ("bool".equals(type)) { // NOI18N
   883                             suggestions.add("true"); // NOI18N
   884                             suggestions.add("false"); // NOI18N
   885                         } else if ("submitmethod".equals(type)) { // NOI18N
   886                             suggestions.add("post"); // NOI18N
   887                             suggestions.add("get"); // NOI18N
   888                         } else if ("validationactive".equals(type)) { // NOI18N
   889                             suggestions.add(":save"); // NOI18N
   890                             suggestions.add(":create"); // NOI18N
   891                             suggestions.add(":update"); // NOI18N
   892                         } else if ("string".equals(type)) { // NOI18N
   893                             suggestions.add("\""); // NOI18N
   894                         } else if ("hash".equals(type)) { // NOI18N
   895                             suggestions.add("{"); // NOI18N
   896                         } else if ("controller".equals(type)) {
   897                             // Dynamically produce controllers
   898                             List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
   899                             for (String n : controllers) {
   900                                 suggestions.add("'" + n + "'");
   901                             }
   902                         } else if ("action".equals(type)) {
   903                             // Dynamically produce actions
   904                             // This would need to be scoped by the current
   905                             // context - look at the hash, find the specified
   906                             // controller and limit it to that
   907                             List<String> actions = getActionNames(request);
   908                             for (String n : actions) {
   909                                 suggestions.add("'" + n + "'");
   910                             }
   911                         } else if ("status".equals(type)) {
   912                             return RubyHttpStatusCodeCompleter.complete(proposals, request, anchor, caseSensitive, target);
   913                         }
   914                     }
   915                 }
   916             }
   917         } else {
   918             for (String value : values) {
   919                 int typeIndex = value.indexOf(':');
   920                 if (typeIndex != -1) {
   921                     value = value.substring(0, typeIndex);
   922                 }
   923                 value = ":" + value + " => ";
   924                 suggestions.add(value);
   925             }
   926         }
   927 
   928         // I've gotta clean up the colon handling in complete()
   929         // I originally stripped ":" to make direct (INameNode)getName()
   930         // comparisons on symbols work directly but it's becoming a liability now
   931         String colonPrefix = ":" + prefix;
   932         for (String suggestion : suggestions) {
   933             if (startsWith(suggestion, prefix) || startsWith(suggestion, colonPrefix)) {
   934                 String insert = suggestion;
   935                 String desc = null;
   936                 if (inValue) {
   937                     if (!isLastArg) {
   938                         insert = insert + ", ";
   939                     }
   940                     if (key != null) {
   941                         desc = ":" + key + " = " + suggestion;
   942                     }
   943                 }
   944                 ParameterItem item = new ParameterItem(target, suggestion, desc, insert,  anchor, request);
   945                 item.setSymbol(true);
   946                 item.setSmart(true);
   947                 proposals.add(item);
   948             }
   949         }
   950         
   951         return true;
   952     }
   953 
   954     /** Get the actions for the given file. If the file is a controller, list the actions within it,
   955      * otherwise, if the file is a view, list the actions for the corresponding controller.
   956      * 
   957      * @param fileInProject the file we're looking up
   958      * @return A List of action names
   959      */
   960     private List<String> getActionNames(CompletionRequest request) {
   961         FileObject file = request.fileObject;
   962         FileObject controllerFile = null;
   963         if (file.getNameExt().endsWith("_controller.rb")) {
   964             controllerFile = file;
   965         } else {
   966             controllerFile = RubyUtils.getRailsControllerFor(file);
   967         }
   968         // TODO - check for other :controller-> settings in the hashmap and if present, use it
   969         if (controllerFile == null) {
   970             return Collections.emptyList();
   971         }
   972         
   973         String controllerClass = RubyUtils.getControllerClass(controllerFile);
   974         if (controllerClass != null) {
   975             String prefix = request.prefix;
   976             Set<IndexedMethod> methods = request.index.getMethods(prefix, controllerClass, request.kind);
   977             List<String> actions = new ArrayList<String>();
   978             for (IndexedMethod method : methods) {
   979                 if (method.isPublic() && method.getArgs() == null || method.getArgs().length == 0) {
   980                     actions.add(method.getName());
   981                 }
   982             }
   983             
   984             return actions;
   985         }
   986         
   987         // TODO - pull out the methods or this class
   988         
   989         return Collections.emptyList();
   990     }
   991     
   992     private void completeFixed(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
   993         String[] values = data.split("\\|");
   994         String prefix = request.prefix;
   995         // I originally stripped ":" to make direct (INameNode)getName()
   996         // comparisons on symbols work directly but it's becoming a liability now
   997         String colonPrefix = ":" + prefix;
   998         for (String value : values) {
   999             if (startsWith(value, prefix) || startsWith(value, colonPrefix)) {
  1000                 String insert = isLastArg ? value : (value + ", ");
  1001                 ParameterItem item = new ParameterItem(target, value, null, insert, anchor, request);
  1002                 item.setSymbol(true);
  1003                 item.setSmart(true);
  1004                 proposals.add(item);
  1005             }
  1006         }
  1007     }
  1008     
  1009     private void completeDbTables(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
  1010         // Add in the eligible database tables found in this project
  1011         // Assumes this is a Rails project
  1012         String p = request.prefix;
  1013         String colonPrefix = p;
  1014         if (":".equals(p)) { // NOI18N
  1015             p = "";
  1016         } else {
  1017             colonPrefix = ":" + p; // NOI18N
  1018         }
  1019         Set<String> tables = request.index.getDatabaseTables(p, request.kind);
  1020         
  1021         // I originally stripped ":" to make direct (INameNode)getName()
  1022         // comparisons on symbols work directly but it's becoming a liability now
  1023         String prefix = request.prefix;
  1024         for (String table : tables) {
  1025             // PENDING: Should I insert :tablename or 'tablename' or "tablename" ?
  1026             String tableName = ":" + table;
  1027             if (startsWith(tableName, prefix) || startsWith(tableName, colonPrefix)) {
  1028                 String insert = isLastArg ? tableName : (tableName + ", ");
  1029                 ParameterItem item = new ParameterItem(target, tableName, null, insert, anchor, request);
  1030                 item.setSymbol(true);
  1031                 item.setSmart(true);
  1032                 proposals.add(item);
  1033             }
  1034         }
  1035     }
  1036 
  1037     private void completeModels(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
  1038         Set<IndexedClass> clz = request.index.getSubClasses(request.prefix, RubyIndex.ACTIVE_RECORD_BASE, request.kind);
  1039         
  1040         String prefix = request.prefix;
  1041         // I originally stripped ":" to make direct (INameNode)getName()
  1042         // comparisons on symbols work directly but it's becoming a liability now
  1043         String colonPrefix = ":" + prefix;
  1044         for (IndexedClass c : clz) {
  1045             String name = c.getName();
  1046             String symbol = ":"+RubyUtils.camelToUnderlinedName(name);
  1047             if (startsWith(symbol, prefix) || startsWith(symbol, colonPrefix)) {
  1048                 String insert = isLastArg ? symbol : (symbol + ", ");
  1049                 ParameterItem item = new ParameterItem(target, symbol, name, insert, anchor, request);
  1050                 item.setSymbol(true);
  1051                 item.setSmart(true);
  1052                 proposals.add(item);
  1053             }
  1054         }
  1055     }
  1056     
  1057     private void completeDbColumns(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
  1058         // Add in the eligible database tables found in this project
  1059         // Assumes this is a Rails project
  1060 //        Set<String> tables = request.index.getDatabaseTables(request.prefix, request.kind);
  1061         
  1062         // TODO
  1063 //        for (String table : tables) {
  1064 //            if (startsWith(table, prefix)) {
  1065 //                SymbolHashItem item = new SymbolHashItem(target, ":" + table, null, anchor, request);
  1066 //                item.setSymbol(true);
  1067 //                proposals.add(item);
  1068 //            }
  1069 //        }
  1070     }
  1071     
  1072     private boolean isEmpty(String value) {
  1073         return value == null || value.length() == 0;
  1074     }
  1075 
  1076     // TODO: Move to the top
  1077     @Override
  1078     public CodeCompletionResult complete(final CodeCompletionContext context) {
  1079         ParserResult ir = context.getParserResult();
  1080         int lexOffset = context.getCaretOffset();
  1081         String prefix = context.getPrefix();
  1082         QuerySupport.Kind kind = context.isPrefixMatch() ? QuerySupport.Kind.PREFIX : QuerySupport.Kind.EXACT;
  1083         QueryType queryType = context.getQueryType();
  1084         this.caseSensitive = context.isCaseSensitive();
  1085 
  1086         final int astOffset = AstUtilities.getAstOffset(ir, lexOffset);
  1087         if (astOffset == -1) return null;
  1088         
  1089         // Avoid all those annoying null checks
  1090         if (prefix == null) prefix = "";
  1091 
  1092         List<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
  1093         DefaultCompletionResult completionResult = new DefaultCompletionResult(proposals, false);
  1094 
  1095         anchor = lexOffset - prefix.length();
  1096 
  1097         final RubyIndex index = RubyIndex.get(ir);
  1098 
  1099         final Document document = RubyUtils.getDocument(ir);
  1100         if (document == null) return CodeCompletionResult.NONE;
  1101 
  1102         // TODO - move to LexUtilities now that this applies to the lexing offset?
  1103         lexOffset = AstUtilities.boundCaretOffset(ir, lexOffset);
  1104 
  1105         // Discover whether we're in a require statement, and if so, use special completion
  1106         final TokenHierarchy<Document> th = TokenHierarchy.get(document);
  1107         final BaseDocument doc = (BaseDocument)document;
  1108         final FileObject fileObject = RubyUtils.getFileObject(ir);
  1109         
  1110         boolean showLower = true;
  1111         boolean showUpper = true;
  1112         boolean showSymbols = false;
  1113         char first = 0;
  1114 
  1115         doc.readLock(); // Read-lock due to Token hierarchy use
  1116         try {
  1117         if (prefix.length() > 0) {
  1118             first = prefix.charAt(0);
  1119 
  1120             // Foo::bar --> first char is "b" - we're looking for a method
  1121             int qualifier = prefix.lastIndexOf("::");
  1122             if (qualifier != -1 && qualifier < prefix.length() - 2) first = prefix.charAt(qualifier + 2);
  1123 
  1124             showLower = Character.isLowerCase(first);
  1125             // showLower is not necessarily !showUpper - prefix can be ":foo" for example
  1126             showUpper = Character.isUpperCase(first);
  1127 
  1128             if (first == ':') {
  1129                 showSymbols = true;
  1130 
  1131                 if (prefix.length() > 1) {
  1132                     char second = prefix.charAt(1);
  1133                     prefix = prefix.substring(1);
  1134                     showLower = Character.isLowerCase(second);
  1135                     showUpper = Character.isUpperCase(second);
  1136                 }
  1137             }
  1138         }
  1139 
  1140         // Carry completion context around since this logic is split across lots of methods
  1141         // and I don't want to pass dozens of parameters from method to method; just pass
  1142         // a request context with supporting parserResult needed by the various completion helpers.
  1143         CompletionRequest request = new CompletionRequest(
  1144                 completionResult, th, ir, lexOffset, astOffset,
  1145                 doc, prefix, index, kind, queryType, fileObject);
  1146 
  1147         // See if we're inside a string or regular expression and if so,
  1148         // do completions applicable to strings - require-completion,
  1149         // escape codes for quoted strings and regular expressions, etc.
  1150         if (RubyStringCompleter.complete(proposals, request, anchor, caseSensitive)) {
  1151             completionResult.setFilterable(false);
  1152             return completionResult;
  1153         }
  1154 
  1155         Call call = Call.getCallType(doc, th, lexOffset);
  1156 
  1157         // Fields
  1158         // This is a bit stupid at the moment, not looking at the current typing context etc.
  1159         Node root = AstUtilities.getRoot(ir);
  1160 
  1161         if (root == null) {
  1162             RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
  1163             return completionResult;
  1164         }
  1165 
  1166         // Compute the bounds of the line that the caret is on, and suppress nodes overlapping the line.
  1167         // This will hide not only paritally typed identifiers, but surrounding contents like the current class and module
  1168         final int astLineBegin;
  1169         final int astLineEnd;
  1170 
  1171         try {
  1172             astLineBegin = AstUtilities.getAstOffset(ir, Utilities.getRowStart(doc, lexOffset));
  1173             astLineEnd = AstUtilities.getAstOffset(ir, Utilities.getRowEnd(doc, lexOffset));
  1174         } catch (BadLocationException ble) {
  1175             return CodeCompletionResult.NONE;
  1176         }
  1177 
  1178         final AstPath path = new AstPath(root, astOffset);
  1179         request.path = path;
  1180 
  1181         Map<String, Node> variables = new HashMap<String, Node>();
  1182         Map<String, Node> fields = new HashMap<String, Node>();
  1183         Map<String, Node> globals = new HashMap<String, Node>();
  1184         Map<String, Node> constants = new HashMap<String, Node>();
  1185 
  1186         final Node closest = path.leaf();
  1187         request.target = closest;
  1188 
  1189         // Don't try to add local vars, globals etc. as part of calls or class fqns
  1190         if (call.getLhs() == null) {
  1191             if (showLower && closest != null) {
  1192                 for (Node block : AstUtilities.getApplicableBlocks(path, false)) {
  1193                     addDynamic(block, variables);
  1194                 }
  1195 
  1196                 for (Node child : AstUtilities.findLocalScope(closest, path).childNodes()) {
  1197                     addLocals(child, variables);
  1198                 }
  1199             }
  1200 
  1201             boolean inAttrCall = isInAttr(closest, path);
  1202 
  1203             if (prefix.length() == 0 || first == '@' || showSymbols || inAttrCall) {
  1204                 String fqn = AstUtilities.getFqnName(path);
  1205 
  1206                 if (isEmpty(fqn)) {
  1207                     String fileName = RubyUtils.getFileObject(context.getParserResult()).getName();
  1208                     if (fileName.endsWith("_spec")) { //NOI18N
  1209                         // use the virtual class created for the spec file
  1210                         fqn = RubyUtils.underlinedNameToCamel(fileName);
  1211                     } else {
  1212                         fqn = "Object"; // NOI18N
  1213                     }
  1214                 }
  1215 
  1216                 // TODO - if fqn has multiple ::'s, try various combinations? or is 
  1217                 // add inherited already doing that?
  1218 
  1219                 Set<IndexedField> f;
  1220                 if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
  1221                     f = new HashSet<IndexedField>();
  1222                     addActionViewFields(f, fileObject, index, prefix, kind);
  1223                 } else {
  1224                     //strip out ':' when querying fields for cases like 'attr_reader :^'
  1225                     if (inAttrCall && first == ':' && prefix.length() == 1) {
  1226                         f = index.getInheritedFields(fqn, "", kind, false);
  1227                     } else {
  1228                         f = index.getInheritedFields(fqn, prefix, kind, false);
  1229                     }
  1230                 }
  1231 
  1232                 for (IndexedField field : f) {
  1233                     String insertPrefix = inAttrCall ? ":" : null;
  1234                     FieldItem item = new FieldItem(field, anchor, request, insertPrefix);
  1235 
  1236                     item.setSmart(field.isSmart());
  1237 
  1238                     if (showSymbols) item.setSymbol(true);
  1239 
  1240                     proposals.add(item);
  1241                 }
  1242 
  1243                 // return just the fields for attr_
  1244                 if (inAttrCall) return completionResult;
  1245             }
  1246 
  1247             // $ is neither upper nor lower 
  1248             if (prefix.length() == 0 || first == '$' || showSymbols) {
  1249                 if (prefix.startsWith("$") || showSymbols) {
  1250                     completeGlobals(proposals, request, showSymbols);
  1251                     // Dollar variables too
  1252                     RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
  1253 
  1254                     if (!showSymbols) return completionResult;
  1255                 }
  1256             }
  1257         }
  1258 
  1259         // TODO: should only include fields etc. down to caret location??? Decide. (Depends on language semantics. Can I have forward referemces?
  1260         if (call.isConstantExpected()) {
  1261             RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
  1262             RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
  1263             RubyType type = call.getType();
  1264             if (type.isKnown() && type.isSingleton()) {
  1265                 RubyMethodCompleter.complete(proposals, request, type.first(), call, anchor, caseSensitive);
  1266             }
  1267             return completionResult;
  1268         }
  1269 
  1270         // If we're in a call, add in some parserResult and help for the code completion call
  1271         boolean inCall = addParameters(proposals, request);
  1272 
  1273         // Code completion from the index.
  1274         if (index != null) {
  1275             if (showLower || showSymbols) {
  1276                 String fqn = AstUtilities.getFqnName(path);
  1277                 if (isEmpty(fqn)) fqn = "Object"; // NOI18N
  1278 
  1279                 // doesn't apply to (or work with) documentation/tooltip help
  1280                 if (queryType == QueryType.COMPLETION && completeDefOrInclude(proposals, request, fqn)) {
  1281                     return completionResult;
  1282                 }
  1283 
  1284                 if (RubyMethodCompleter.complete(proposals, request, fqn, call, anchor, caseSensitive)) {
  1285                     return completionResult;
  1286                 }
  1287 
  1288                 // Only call local and inherited methods if we don't have an LHS, such as Foo::
  1289                 if (call.getLhs() == null) {
  1290                     // TODO - pull this into a completeInheritedMethod call
  1291                     // Complete inherited methods or local methods only (plus keywords) since there
  1292                     // is no receiver so it must be a local or inherited method call
  1293                     Set<IndexedMethod> inheritedMethods = index.getInheritedMethods(fqn, prefix, kind);
  1294 
  1295                     inheritedMethods = RubyDynamicFindersCompleter.proposeDynamicMethods(inheritedMethods, proposals, request, anchor);
  1296                     // Handle action view completion for RHTML and Markaby files
  1297                     if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
  1298                         addActionViewMethods(inheritedMethods, fileObject, index, prefix, kind);
  1299                     } else if (fileObject.getName().endsWith("_spec")) { // NOI18N
  1300                         // RSpec
  1301 
  1302                         /* My spec object had the following extras methods over a plain Object:
  1303                         x = self.class.methods
  1304                         x.each {|c|
  1305                         puts c
  1306                         }
  1307                         > args_and_options
  1308                         > context
  1309                         > copy_instance_variables_from
  1310                         > describe
  1311                         > gem
  1312                         > metaclass
  1313                         > require
  1314                         > require_gem
  1315                         > respond_to
  1316                         > should
  1317                         > should_not
  1318                          */
  1319                         String includes[] = {
  1320                             // "describe" should be in Kernel already, from spec/runner/extensions/kernel.rb
  1321                             "Spec::Matchers",
  1322                             // This one shouldn't be necessary since there's a
  1323                             // "class Object; include xxx::ObjectExpectations; end" in rspec's object.rb
  1324                             "Spec::Expectations::ObjectExpectations",
  1325                             "Spec::DSL::BehaviourEval::InstanceMethods"}; // NOI18N
  1326                         for (String fqns : includes) {
  1327                             inheritedMethods.addAll(index.getInheritedMethods(fqns, prefix, kind));
  1328                         }
  1329                     }
  1330 
  1331                     for (IndexedMethod method : inheritedMethods) {
  1332                         // This should not be necessary - filtering happens in getInheritedMethods right?
  1333                         if (prefix.length() > 0 && !method.getName().startsWith(prefix)) continue;
  1334                         if (method.isNoDoc()) continue;
  1335 
  1336                         // If a method is an "initialize" method I should do something special so that
  1337                         // it shows up as a "constructor" (in a new() statement) but not as a directly
  1338                         // callable initialize method (it should already be culled because it's private)
  1339                         MethodItem item = new MethodItem(method, anchor, request);
  1340 
  1341                         item.setSmart(method.isSmart());
  1342 
  1343                         if (showSymbols) item.setSymbol(true);
  1344 
  1345                         proposals.add(item);
  1346                     }
  1347                 }
  1348             }
  1349 
  1350             if (showUpper) {
  1351                 // doesn't apply to (or work with) documentation/tooltip help
  1352                 if (queryType == QueryType.COMPLETION && completeDefOrInclude(proposals, request, "")) {
  1353                     return completionResult;
  1354                 }
  1355             }
  1356             if ((showUpper && ((prefix != null && prefix.length() > 0) ||
  1357                     (!call.isMethodExpected() && call.getLhs() != null && call.getLhs().length() > 0))) || (showSymbols && !inCall)) {
  1358                 // TODO - allow method calls if you're already entered the first char!
  1359                 RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
  1360                 RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
  1361             }
  1362         }
  1363         assert (kind == QuerySupport.Kind.PREFIX) || (kind == QuerySupport.Kind.CASE_INSENSITIVE_PREFIX) ||
  1364                 (kind == QuerySupport.Kind.EXACT);
  1365 
  1366         // TODO
  1367         // Remove fields and variables whose names are already taken, e.g. do a fields.removeAll(variables) etc.
  1368         for (String variable : variables.keySet()) {
  1369             if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
  1370                     ((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
  1371                 Node node = variables.get(variable);
  1372 
  1373                 if (!overlapsLine(node, astLineBegin, astLineEnd)) {
  1374                     AstElement co = new AstNameElement(ir, node, variable,
  1375                             ElementKind.VARIABLE);
  1376                     RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
  1377                     item.setSmart(true);
  1378 
  1379                     if (showSymbols) {
  1380                         item.setSymbol(true);
  1381                     }
  1382 
  1383                     proposals.add(item);
  1384                 }
  1385             }
  1386         }
  1387 
  1388         for (String field : fields.keySet()) {
  1389             if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(field)) ||
  1390                     ((kind != QuerySupport.Kind.EXACT) && startsWith(field, prefix))) {
  1391                 Node node = fields.get(field);
  1392 
  1393                 if (overlapsLine(node, astLineBegin, astLineEnd)) {
  1394                     continue;
  1395                 }
  1396 
  1397                 Element co = new AstFieldElement(ir, node);
  1398                 FieldItem item = new FieldItem(co, anchor, request);
  1399                 item.setSmart(true);
  1400 
  1401                 if (showSymbols) {
  1402                     item.setSymbol(true);
  1403                 }
  1404 
  1405                 proposals.add(item);
  1406             }
  1407         }
  1408 
  1409         // TODO - model globals and constants using different icons / etc.
  1410         for (String variable : globals.keySet()) {
  1411             // TODO - kind.EXACT
  1412             if (startsWith(variable, prefix) ||
  1413                     (showSymbols && startsWith(variable.substring(1), prefix))) {
  1414                 Node node = globals.get(variable);
  1415 
  1416                 if (overlapsLine(node, astLineBegin, astLineEnd)) {
  1417                     continue;
  1418                 }
  1419 
  1420                 AstElement co = new AstNameElement(ir, node, variable,
  1421                         ElementKind.VARIABLE);
  1422                 RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
  1423                 item.setSmart(true);
  1424 
  1425                 if (showSymbols) {
  1426                     item.setSymbol(true);
  1427                 }
  1428 
  1429                 proposals.add(item);
  1430             }
  1431         }
  1432 
  1433         // TODO - model globals and constants using different icons / etc.
  1434         for (String variable : constants.keySet()) {
  1435             if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
  1436                     ((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
  1437                 // Skip constants that are known to be classes
  1438                 Node node = constants.get(variable);
  1439 
  1440                 if (overlapsLine(node, astLineBegin, astLineEnd)) continue;
  1441 
  1442                 //                ComObject co;
  1443                 //                if (isClassName(variable)) {
  1444                 //                    co = JRubyNode.create(target, null);
  1445                 //                    if (co == null) {
  1446                 //                        continue;
  1447                 //                    }
  1448                 //                } else {
  1449                 //                    co = new DefaultComVariable(variable, false, -1, -1);
  1450                 //                    ((DefaultComVariable)co).setNode(target);
  1451                 AstElement co = new AstNameElement(ir, node, variable, ElementKind.VARIABLE);
  1452 
  1453                 RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
  1454                 item.setSmart(true);
  1455 
  1456                 if (showSymbols) {
  1457                     item.setSymbol(true);
  1458                 }
  1459 
  1460                 proposals.add(item);
  1461             }
  1462         }
  1463 
  1464         if (RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols)) {
  1465             return completionResult;
  1466         }
  1467 
  1468         if (queryType == QueryType.DOCUMENTATION) {
  1469             proposals = filterDocumentation(proposals, root, doc, ir, astOffset, lexOffset, prefix, path,
  1470                     index);
  1471         }
  1472         } finally {
  1473             doc.readUnlock();
  1474         }
  1475 
  1476         return completionResult;
  1477     }
  1478         
  1479 
  1480     private boolean isInAttr(Node closest, AstPath path) {
  1481         if (closest != null) {
  1482             // first argument in attr_*
  1483             for (Node child : closest.childNodes()) {
  1484                 if (AstUtilities.isAttr(child)) {
  1485                     return true;
  1486                 }
  1487             }
  1488             // others, e.g. attr_reader :foo, :ba^r
  1489             if (AstUtilities.isAttr(path.leafParent()) || AstUtilities.isAttr(path.leafGrandParent())) {
  1490                 return true;
  1491             }
  1492         }
  1493         return false;
  1494     }
  1495 
  1496     private void addActionViewMethods(Set<IndexedMethod> inheritedMethods, FileObject fileObject, RubyIndex index, String prefix,
  1497             QuerySupport.Kind kind) { 
  1498         // RHTML and Markaby: Add in the helper methods etc. from the associated files
  1499         boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
  1500         if (isMarkaby) {
  1501             Set<IndexedMethod> actionView = index.getInheritedMethods("ActionView::Base", prefix, kind); // NOI18N
  1502             inheritedMethods.addAll(actionView);
  1503         }
  1504 
  1505         if (isRhtmlFile(fileObject) || isMarkaby) {
  1506             // Hack - include controller and helper files as well
  1507             FileObject f = fileObject.getParent();
  1508             // name of the controller w/o the "controller" suffix
  1509             String controllerName = null;
  1510             // XXX Will this work for .mab files? Where do they go?
  1511             while (f != null && !f.getName().equals("views")) { // todo - make sure grandparent is app
  1512                 String n = underlinedNameToCamel(f.getName());
  1513                 if (controllerName == null) {
  1514                     controllerName = n;
  1515                 } else {
  1516                     controllerName = n + "::" + controllerName;
  1517                 }
  1518                 f = f.getParent();
  1519             }
  1520 
  1521 //           // add in all methods from the associated helper and inherited helpers. this will
  1522             // add also ApplicationHelper, which is global
  1523             Set<String> helperNames = new HashSet<String>();
  1524             helperNames.add(helperName(controllerName));
  1525             for (IndexedClass superClass : index.getSuperClasses(controllerName(controllerName))) {
  1526                 if ("ActionController::Base".equals(superClass.getFqn())) { //NOI18N
  1527                     break;
  1528                 }
  1529                 helperNames.add(helperName(superClass.getFqn()));
  1530             }
  1531             for (String helper : helperNames) {
  1532                 inheritedMethods.addAll(index.getInheritedMethods(helper, prefix, kind));
  1533             }
  1534 
  1535             index.getSuperClasses(controllerName);
  1536             // TODO - pull in the fields (NOT THE METHODS) from the controller
  1537             //Set<IndexedMethod> controller = index.getInheritedMethods(controllerName+"Controller", prefix, kind);
  1538             //inheritedMethods.addAll(controller);
  1539         }
  1540     }
  1541 
  1542     private void addActionViewFields(Set<IndexedField> inheritedFields, FileObject fileObject, RubyIndex index, String prefix, 
  1543             QuerySupport.Kind kind) { 
  1544         // RHTML and Markaby: Add in the helper methods etc. from the associated files
  1545         boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
  1546         if (isMarkaby) {
  1547             Set<IndexedField> actionView = index.getInheritedFields("ActionView::Base", prefix, kind, true); // NOI18N
  1548             inheritedFields.addAll(actionView);
  1549         }
  1550 
  1551         if (RubyUtils.isRhtmlFile(fileObject) || isMarkaby) {
  1552             // Hack - include controller and helper files as well
  1553             FileObject f = fileObject.getParent();
  1554             String controllerName = null;
  1555             // XXX Will this work for .mab files? Where do they go?
  1556             while (f != null && !f.getName().equals("views")) { // NOI18N // todo - make sure grandparent is app
  1557                 String n = RubyUtils.underlinedNameToCamel(f.getName());
  1558                 if (controllerName == null) {
  1559                     controllerName = n;
  1560                 } else {
  1561                     controllerName = n + "::" + controllerName; // NOI18N
  1562                 }
  1563                 f = f.getParent();
  1564             }
  1565 
  1566             String fqn = controllerName+"Controller"; // NOI18N
  1567             Set<IndexedField> controllerFields = index.getInheritedFields(fqn, prefix, kind, true);
  1568             for (IndexedField field : controllerFields) {
  1569                 if ("ActionController::Base".equals(field.getIn())) { // NOI18N
  1570                     continue;
  1571                 }
  1572                 inheritedFields.add(field);
  1573             }
  1574         }
  1575     }       
  1576     
  1577     /** If we're doing documentation completion, try to drop the list down to a single alternative
  1578      * (since the framework will just use the first produced result), and in particular, the -best-
  1579      * alternative
  1580      */
  1581     // TODO - pass in request object here!
  1582     private List<CompletionProposal> filterDocumentation(List<CompletionProposal> proposals,
  1583         Node root, BaseDocument doc, ParserResult parserResult, int astOffset, int lexOffset, String name,
  1584         AstPath path, RubyIndex index) {
  1585         // Look to see if this symbol is either a "class Foo" or a "def foo", and if we invoke
  1586         // completion on it, prefer this element provided it has documentation
  1587         List<CompletionProposal> candidates = new ArrayList<CompletionProposal>();
  1588         FileObject fo = RubyUtils.getFileObject(parserResult);
  1589         Map<IndexedElement, CompletionProposal> elementMap =
  1590             new HashMap<IndexedElement, CompletionProposal>();
  1591         Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
  1592         Set<IndexedClass> classes = new HashSet<IndexedClass>();
  1593 
  1594         for (CompletionProposal proposal : proposals) {
  1595             RubyElement e = (RubyElement) proposal.getElement();
  1596 
  1597             if (e instanceof IndexedElement) {
  1598                 IndexedElement ie = (IndexedElement)e;
  1599 
  1600                 if (ie instanceof IndexedClass) {
  1601                     classes.add((IndexedClass)ie);
  1602                     elementMap.put(ie, proposal);
  1603                 } else if (ie instanceof IndexedMethod) {
  1604                     methods.add((IndexedMethod)ie);
  1605                     elementMap.put(ie, proposal);
  1606                 }
  1607 
  1608                 if (ie.getFileObject() == fo) {
  1609                     // The class is in this file - if it has documentation, prefer it
  1610                     candidates.add(proposal);
  1611                 }
  1612             }
  1613         }
  1614 
  1615         // Check the candidates to see if one of them is actually -defined-
  1616         // under the caret; e.g. if you have "class File" with documentation,
  1617         // and you ctrl-space on it, you always want to show THIS documentation
  1618         // for File, not the standard one defined elsewhere.
  1619         for (CompletionProposal candidate : candidates) {
  1620             // See if the candidate corresponds to the caret position
  1621             RubyElement re = (RubyElement) candidate.getElement();
  1622             if (!(re instanceof IndexedElement)) {
  1623                 continue;
  1624             }
  1625             IndexedElement e = (IndexedElement)re;
  1626             String signature = e.getSignature();
  1627             Node node = AstUtilities.findBySignature(root, signature);
  1628 
  1629             if (node != null) {
  1630                 SourcePosition pos = node.getPosition();
  1631                 int startPos = LexUtilities.getLexerOffset(parserResult, pos.getStartOffset());
  1632 
  1633                 try {
  1634                     int lineBegin = AstUtilities.getAstOffset(parserResult, Utilities.getRowFirstNonWhite(doc, startPos));
  1635                     int lineEnd = AstUtilities.getAstOffset(parserResult, Utilities.getRowEnd(doc, startPos));
  1636 
  1637                     if ((astOffset >= lineBegin) && (astOffset <= lineEnd)) {
  1638                         // Look for documentation
  1639                         List<String> rdoc = AstUtilities.gatherDocumentation(parserResult.getSnapshot(), node);
  1640 
  1641                         if (rdoc != null && !rdoc.isEmpty()) {
  1642                             return Collections.singletonList(candidate);
  1643                         }
  1644                     }
  1645                 } catch (BadLocationException ble) {
  1646                     // The parse information is too old - the document has shrunk. Do nothing, the
  1647                     // AST nodes are pointing into the old contents.
  1648                 }
  1649             }
  1650         }
  1651 
  1652         // Try to pick the best match among many documentation entries: Heuristic time.
  1653         // Similar to heuristics used for Go To Declaration: Prefer long documentation,
  1654         // prefer documentation related to the require-statements in this file, etc.
  1655         IndexedElement candidate = null;
  1656 
  1657         if (!classes.isEmpty()) {
  1658             RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(parserResult, null, path, index, path.leaf());
  1659             candidate = cdf.findBestElementMatch(classes);
  1660         } else if (!methods.isEmpty()) {
  1661             RubyDeclarationFinder finder = new RubyDeclarationFinder();
  1662             candidate = finder.findBestMethodMatch(name, methods, doc, astOffset, lexOffset, path,
  1663                     path.leaf(), index);
  1664         }
  1665 
  1666         if (candidate != null) {
  1667             CompletionProposal proposal = elementMap.get(candidate);
  1668 
  1669             if (proposal != null) {
  1670                 return Collections.singletonList(proposal);
  1671             }
  1672         }
  1673 
  1674         return proposals;
  1675     }
  1676 
  1677     //    private boolean isClassName(String s) {
  1678     //        // Initial capital letter, second letter is not
  1679     //        if (s.length() == 1) {
  1680     //            return Character.isUpperCase(s.charAt(0));
  1681     //        }
  1682     //        
  1683     //        if (Character.isLowerCase(s.charAt(0))) {
  1684     //            return false;
  1685     //        }
  1686     //        
  1687     //        return Character.isLowerCase(s.charAt(1));
  1688     //    }
  1689     private boolean overlapsLine(Node node, int lineBegin, int lineEnd) {
  1690         SourcePosition pos = node.getPosition();
  1691 
  1692         //return (((pos.getStartOffset() <= lineEnd) && (pos.getEndOffset() >= lineBegin)));
  1693         // Don't look to see if the line is within the target. See if the target is started on this line (where
  1694         // the declaration is, e.g. it might be an incomplete line.
  1695         return ((pos.getStartOffset() >= lineBegin) && (pos.getStartOffset() <= lineEnd));
  1696     }
  1697 
  1698     //    /** Return true iff the name looks like an operator name */
  1699     //    private boolean isOperator(String name) {
  1700     //        // If a name contains not a single letter, it is probably an operator - especially
  1701     //        // if it is a short name
  1702     //        int n = name.length();
  1703     //
  1704     //        if (n > 2) {
  1705     //            return false;
  1706     //        }
  1707     //
  1708     //        for (int i = 0; i < n; i++) {
  1709     //            if (Character.isLetter(name.charAt(i))) {
  1710     //                return false;
  1711     //            }
  1712     //        }
  1713     //
  1714     //        return true;
  1715     //    }
  1716 
  1717     static void addLocals(Node node, Map<String, Node> variables) {
  1718         switch (node.getNodeType()) {
  1719         case LOCALASGNNODE: {
  1720             String name = ((INameNode)node).getName();
  1721 
  1722             if (!variables.containsKey(name)) {
  1723                 variables.put(name, node);
  1724             }
  1725             break;
  1726         }
  1727         case ARGSNODE: {
  1728             // TODO - use AstUtilities.getDefArgs here - but avoid hitting them twice!
  1729             //List<String> parameters = AstUtilities.getDefArgs(def, true);
  1730             // However, I've gotta find the parameter nodes themselves too!
  1731             ArgsNode an = (ArgsNode)node;
  1732 
  1733             if (an.getRequiredCount() > 0) {
  1734                 for (Node arg : an.childNodes()) {
  1735                     if (arg instanceof ListNode) {
  1736                         for (Node arg2 : arg.childNodes()) {
  1737                             if (arg2 instanceof ArgumentNode) {
  1738                                 variables.put(((ArgumentNode)arg2).getName(), arg2);
  1739                             } else if (arg2 instanceof LocalAsgnNode) {
  1740                                 variables.put(((INameNode)arg2).getName(), arg2);
  1741                             }
  1742                         }
  1743                     }
  1744                 }
  1745             }
  1746 
  1747             // Rest args
  1748             if (an.getRest() != null) {
  1749                 String name = an.getRest().getName();
  1750                 variables.put(name, an.getRest());
  1751             }
  1752 
  1753             // Block args
  1754             if (an.getBlock() != null) {
  1755                 String name = an.getBlock().getName();
  1756                 variables.put(name, an.getBlock());
  1757             }
  1758             
  1759             break;
  1760         }
  1761 
  1762         //        } else if (target instanceof AliasNode) {
  1763         //            AliasNode an = (AliasNode)target;
  1764         // Tricky -- which NODE do we add here? Completion creator needs to be aware of new name etc. Do later.
  1765         // Besides, do we show it as a field or a method or what?
  1766 
  1767         //            variab
  1768         //            if (an.getNewName().equals(name)) {
  1769         //                OffsetRange range = AstUtilities.getAliasNewRange(an);
  1770         //                highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
  1771         //            } else if (an.getOldName().equals(name)) {
  1772         //                OffsetRange range = AstUtilities.getAliasOldRange(an);
  1773         //                highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
  1774         //            }
  1775         //          break;
  1776         }
  1777 
  1778         for (Node child : node.childNodes()) {
  1779             if (child instanceof ILocalScope) continue; // ignore nested local scopes
  1780 
  1781             addLocals(child, variables);
  1782         }
  1783     }
  1784 
  1785     static void addDynamic(Node node, Map<String, Node> variables) {
  1786         if (node.getNodeType() == NodeType.DASGNNODE) {
  1787             String name = ((INameNode)node).getName();
  1788 
  1789             if (!variables.containsKey(name)) {
  1790                 variables.put(name, node);
  1791             }
  1792 
  1793             //} else if (target instanceof ArgsNode) {
  1794             //    ArgsNode an = (ArgsNode)target;
  1795             //
  1796             //    if (an.getArgsCount() > 0) {
  1797             //        List<Node> args = an.childNodes();
  1798             //        List<String> parameters = null;
  1799             //
  1800             //        for (Node arg : args) {
  1801             //            if (arg instanceof ListNode) {
  1802             //                List<Node> args2 = arg.childNodes();
  1803             //                parameters = new ArrayList<String>(args2.size());
  1804             //
  1805             //                for (Node arg2 : args2) {
  1806             //                    if (arg2 instanceof ArgumentNode) {
  1807             //                        OffsetRange range = AstUtilities.getRange(arg2);
  1808             //                        highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
  1809             //                    } else if (arg2 instanceof LocalAsgnNode) {
  1810             //                        OffsetRange range = AstUtilities.getRange(arg2);
  1811             //                        highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
  1812             //                    }
  1813             //                }
  1814             //            }
  1815             //        }
  1816             //    }
  1817             //        } else if (!ignoreAlias && target instanceof AliasNode) {
  1818             //            AliasNode an = (AliasNode)target;
  1819             //
  1820             //            if (an.getNewName().equals(name)) {
  1821             //                OffsetRange range = AstUtilities.getAliasNewRange(an);
  1822             //                highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
  1823             //            } else if (an.getOldName().equals(name)) {
  1824             //                OffsetRange range = AstUtilities.getAliasOldRange(an);
  1825             //                highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
  1826             //            }
  1827         }
  1828 
  1829         for (Node child : node.childNodes()) {
  1830             if (child instanceof ILocalScope || child.getNodeType() == NodeType.ITERNODE) continue;
  1831 
  1832             addDynamic(child, variables);
  1833         }
  1834     }
  1835 
  1836     private String loadResource(String basename) {
  1837         // TODO: I18N
  1838         InputStream is = null;
  1839         
  1840         try {
  1841             StringBuilder sb = new StringBuilder();
  1842             is = new BufferedInputStream(RubyCodeCompleter.class.getResourceAsStream("resources/" +
  1843                     basename));
  1844             //while (is)
  1845             while (true) {
  1846                 int c = is.read();
  1847 
  1848                 if (c == -1) break;
  1849 
  1850                 sb.append((char)c);
  1851             }
  1852 
  1853             if (sb.length() > 0) return sb.toString();
  1854         } catch (IOException ie) {
  1855             Exceptions.printStackTrace(ie);
  1856         } finally {
  1857             try {
  1858                 if (is != null) is.close();
  1859             } catch (IOException ie) {
  1860                 Exceptions.printStackTrace(ie);
  1861             }
  1862         }
  1863 
  1864         return null;
  1865     }
  1866 
  1867     private String getKeywordHelp(String keyword) {
  1868         // Difficulty here with context; "else" is used for both the ifelse.html and case.html both define it.
  1869         // End is even more used.
  1870         if (keyword.equals("if") || keyword.equals("elsif") || keyword.equals("else") ||
  1871                 keyword.equals("then") || keyword.equals("unless")) { // NOI18N
  1872 
  1873             return loadResource("ifelse.html"); // NOI18N
  1874         } else if (keyword.equals("case") || keyword.equals("when") || keyword.equals("else")) { // NOI18N
  1875 
  1876             return loadResource("case.html"); // NOI18N
  1877         } else if (keyword.equals("rescue") || keyword.equals("ensure")) { // NOI18N
  1878 
  1879             return loadResource("rescue.html"); // NOI18N
  1880         } else if (keyword.equals("yield")) { // NOI18N
  1881 
  1882             return loadResource("yield.html"); // NOI18N
  1883         }
  1884 
  1885         return null;
  1886     }
  1887 
  1888     /**
  1889      * Find the best possible documentation match for the given IndexedClass or IndexedMethod.
  1890      * This involves looking at index to see which instances of this class or method
  1891      * definition have associated rdoc, as well as choosing between them based on the
  1892      * require statements in the file.
  1893      */
  1894     static IndexedElement findDocumentationEntry(Node root, IndexedElement obj) {
  1895         // 1. Find entries known to have documentation
  1896         String fqn = obj.getSignature();
  1897         Set<?extends IndexedElement> result = obj.getIndex().getDocumented(fqn);
  1898 
  1899         if ((result == null) || (result.isEmpty())) {
  1900             return null;
  1901         } else if (result.size() == 1) {
  1902             return result.iterator().next();
  1903         }
  1904 
  1905         // 2. There are multiple matches so try to disambiguate them by the imports in this file.
  1906         // For example, for "File" we usually show the standard (builtin) documentation,
  1907         // unless you have required "ftools", which redefines File with new docs.
  1908         Set<IndexedElement> candidates;
  1909         if (root != null) {
  1910             candidates = new HashSet<IndexedElement>();
  1911             Set<String> requires = AstUtilities.getRequires(root);
  1912 
  1913             for (IndexedElement o : result) {
  1914                 String require = o.getRequire();
  1915 
  1916                 if (requires.contains(require)) {
  1917                     candidates.add(o);
  1918                 }
  1919             }
  1920 
  1921             if (candidates.size() == 1) {
  1922                 return candidates.iterator().next();
  1923             } else if (!candidates.isEmpty()) {
  1924                 result = candidates;
  1925             }
  1926         }
  1927 
  1928         // 3. Prefer builtin (kernel) docs over other docs.
  1929         candidates = new HashSet<IndexedElement>();
  1930 
  1931         for (IndexedElement o : result) {
  1932             String url = o.getFileUrl();
  1933 
  1934             if (RubyUtils.isRubyStubsURL(url)) {
  1935                 candidates.add(o);
  1936             }
  1937         }
  1938 
  1939         if (candidates.size() == 1) {
  1940             return candidates.iterator().next();
  1941         } else if (!candidates.isEmpty()) {
  1942             result = candidates;
  1943         }
  1944 
  1945         // 4. Consider other heuristics, like picking the "larger" documentation
  1946         // (more lines)
  1947 
  1948         // 5. Just pick an arbitrary one.
  1949         return result.iterator().next();
  1950     }
  1951     
  1952     /**
  1953      * @todo If you invoke this on top of a symbol, I should really just show
  1954      *   the documentation for that symbol!
  1955      * 
  1956      * @param element The element we want to look up comments for
  1957      * @param parserResult The (optional) compilation parserResult for a document referencing the element.
  1958      *   This is used to consult require-statements in the given compilation context etc.
  1959      *   to choose among many alternatives. May be null, in which case the element had
  1960      *   better be an IndexedElement.
  1961      */
  1962     static List<String> getComments(ParserResult info, Element element) {
  1963         assert info != null || element instanceof IndexedElement;
  1964         
  1965         if (element == null) {
  1966             return null;
  1967         }
  1968 
  1969         Node node;
  1970 
  1971         if (element instanceof AstElement) {
  1972             node = ((AstElement)element).getNode();
  1973         } else if (element instanceof IndexedElement) {
  1974             IndexedElement com = (IndexedElement)element;
  1975             Node root = null;
  1976             if (info != null) {
  1977                 root = AstUtilities.getRoot(info);
  1978             }
  1979 
  1980             IndexedElement match = findDocumentationEntry(root, com);
  1981 
  1982             if (match != null) {
  1983                 com = match;
  1984                 element = com;
  1985             }
  1986 
  1987             node = AstUtilities.getForeignNode(com);
  1988 
  1989             if (node == null) {
  1990                 return null;
  1991             }
  1992         } else {
  1993             assert false : element;
  1994 
  1995             return null;
  1996         }
  1997 
  1998         // Initially, I implemented this by using RubyParserResult.getCommentNodes.
  1999         // However, I -still- had to rely on looking in the Document itself, since
  2000         // the CommentNodes are not attached to the AST, and to do things the way
  2001         // RDoc does, I have to (for example) look to see if a comment is at the
  2002         // beginning of a line or on the same line as something else, or if two
  2003         // comments have any empty lines between them, and so on.
  2004         // When I started looking in the document itself, I realized I might as well
  2005         // do all the manipulation on the document, since having the Comment nodes
  2006         // don't particularly help.
  2007         Snapshot snapshot;
  2008         if (element instanceof IndexedElement) {
  2009             FileObject f = ((IndexedElement) element).getFileObject();
  2010             snapshot = Source.create(f).createSnapshot();
  2011         } else if (info != null) {
  2012             snapshot = info.getSnapshot();
  2013         } else {
  2014             return null;
  2015         }
  2016 
  2017         List<String> comments = null;
  2018 
  2019         // Check for RubyComObject: These are external files (like Ruby lib) where I need to check many files
  2020         if (node instanceof ClassNode && !(element instanceof IndexedElement)) {
  2021             String className = AstUtilities.getClassOrModuleName((ClassNode)node);
  2022             List<ClassNode> classes = AstUtilities.getClasses(AstUtilities.getRoot(info));
  2023 
  2024             // Iterate backwards through the list because the most recent documentation
  2025             // should be chosen, if any
  2026             for (int i = classes.size() - 1; i >= 0; i--) {
  2027                 ClassNode clz = classes.get(i);
  2028                 String name = AstUtilities.getClassOrModuleName(clz);
  2029 
  2030                 if (name.equals(className)) {
  2031                     comments = AstUtilities.gatherDocumentation(snapshot, clz);
  2032 
  2033                     if ((comments != null) && (!comments.isEmpty())) {
  2034                         break;
  2035                     }
  2036                 }
  2037             }
  2038         } else {
  2039             comments = AstUtilities.gatherDocumentation(snapshot, node);
  2040         }
  2041 
  2042         if ((comments == null) || (comments.isEmpty())) {
  2043             return null;
  2044         }
  2045         
  2046         return comments;
  2047     }
  2048     
  2049     @Override
  2050     public String document(ParserResult info, ElementHandle handle) {
  2051         Element element = null;
  2052         if (handle instanceof ElementHandle.UrlHandle) {
  2053             String url = ((ElementHandle.UrlHandle)handle).getUrl();
  2054             DeclarationLocation loc = new RubyDeclarationFinder().findLinkedMethod(info, url);
  2055             if (loc != DeclarationLocation.NONE) {
  2056                 element = RubyParser.resolveHandle(info, loc.getElement());
  2057                 if (element == null) return null;
  2058             }
  2059         } else {
  2060             element = RubyParser.resolveHandle(info, handle);
  2061         }
  2062         if (element == null) return null;
  2063         if (element instanceof KeywordElement) return getKeywordHelp(((KeywordElement)element).getName());
  2064         if (element instanceof CommentElement) {
  2065             // Text is packaged as the name
  2066             String comment = element.getName();
  2067             RDocFormatter formatter = new RDocFormatter();
  2068             String[] comments = comment.split("\n");
  2069             for (String text : comments) {
  2070                 // Truncate off leading whitespace before # on comment lines
  2071                 for (int i = 0, n = text.length(); i < n; i++) {
  2072                     char c = text.charAt(i);
  2073                     if (c == '#') {
  2074                         if (i > 0) {
  2075                             text = text.substring(i);
  2076                             break;
  2077                         }
  2078                     } else if (!Character.isWhitespace(c)) {
  2079                         break;
  2080                     }
  2081                 }
  2082                 formatter.appendLine(text);
  2083             }
  2084             return formatter.toHtml();
  2085         }
  2086         
  2087         List<String> comments = getComments(info, element);
  2088         if (comments == null) {
  2089             if (FindersHelper.isFinderMethod(element.getName(), false)) {
  2090                 return new RDocFormatter().getSignature(element) + NbBundle.getMessage(RubyCodeCompleter.class, "DynamicMethod");
  2091             }
  2092             String html = new RDocFormatter().getSignature(element) + "\n<hr>\n<i>" + NbBundle.getMessage(RubyCodeCompleter.class, "NoCommentFound") +"</i>";
  2093 
  2094             return html;
  2095         }
  2096         
  2097         RDocFormatter formatter = new RDocFormatter();
  2098         String name = element.getName();
  2099         if (name != null && name.length() > 0) {
  2100             formatter.setSeqName(name);
  2101         }
  2102 
  2103         for (String text : comments) {
  2104             formatter.appendLine(text);
  2105         }
  2106 
  2107         String html = formatter.toHtml();
  2108         if (!formatter.wroteSignature()) html = formatter.getSignature(element) + "\n<hr>\n" + html;
  2109         
  2110         return html;
  2111     }
  2112 
  2113     @Override
  2114     public ElementHandle resolveLink(String link, ElementHandle elementHandle) {
  2115         if (elementHandle == null) return null;
  2116 
  2117         if (link.indexOf('#') != -1 && elementHandle.getMimeType().equals(RubyInstallation.RUBY_MIME_TYPE)) {
  2118             if (link.startsWith("#")) {
  2119                 // Put the current class etc. in front of the method call if necessary
  2120                 Element surrounding = RubyParser.resolveHandle(null, elementHandle);
  2121                 if (surrounding != null && surrounding.getKind() != ElementKind.KEYWORD) {
  2122                     String name = surrounding.getName();
  2123                     ElementKind kind = surrounding.getKind();
  2124                     if (!(kind == ElementKind.CLASS || kind == ElementKind.MODULE)) {
  2125                         String in = surrounding.getIn();
  2126                         if (in != null && in.length() > 0) {
  2127                             name = in;
  2128                         } else if (name != null) {
  2129                             int index = name.indexOf('#');
  2130                             if (index > 0) name = name.substring(0, index);
  2131                         }
  2132                     }
  2133                     if (name != null) link = name + link;
  2134                 }
  2135             }
  2136             return new ElementHandle.UrlHandle(link);
  2137         }
  2138         
  2139         return null;
  2140     }
  2141 
  2142     // before csl.api 2.11:
  2143     public Set<String> getApplicableTemplates(ParserResult info, int selectionBegin, int selectionEnd) {
  2144         return getApplicableTemplates(RubyUtils.getDocument(info), selectionBegin, selectionEnd);
  2145     }
  2146     // after csl.api 2.11:
  2147     @Override
  2148     public Set<String> getApplicableTemplates(Document d, int selectionBegin, int selectionEnd) {
  2149 
  2150         // TODO - check the code at the AST path and determine whether it makes sense to
  2151         // wrap it in a begin block etc.
  2152         // TODO - I'd like to be able to pass any selection-based templates I'm not familiar with
  2153         
  2154         boolean valid = false;
  2155 
  2156         if (selectionEnd != -1) {
  2157             if (d == null || !(d instanceof BaseDocument) || selectionBegin == selectionEnd) return Collections.emptySet();
  2158 
  2159             BaseDocument doc = (BaseDocument) d;
  2160             try {
  2161                 doc.readLock();
  2162                 if (selectionEnd < selectionBegin) {
  2163                     int temp = selectionBegin;
  2164                     selectionBegin = selectionEnd;
  2165                     selectionEnd = temp;
  2166                 }
  2167                 boolean startLineIsEmpty = Utilities.isRowEmpty(doc, selectionBegin);
  2168                 boolean endLineIsEmpty = Utilities.isRowEmpty(doc, selectionEnd);
  2169 
  2170                 if ((startLineIsEmpty || selectionBegin <= Utilities.getRowFirstNonWhite(doc, selectionBegin)) &&
  2171                         (endLineIsEmpty || selectionEnd > Utilities.getRowLastNonWhite(doc, selectionEnd))) {
  2172                     // I have no text to the left of the beginning or text to the right of the end, but I might
  2173                     // have just selected whitespace - check that
  2174                     String text = doc.getText(selectionBegin, selectionEnd-selectionBegin);
  2175                     for (int i = 0; i < text.length(); i++) {
  2176                         if (!Character.isWhitespace(text.charAt(i))) {
  2177 
  2178                             // Make sure that we're not in a string etc
  2179                             Token<?> token = LexUtilities.getToken(doc, selectionBegin);
  2180                             if (token != null) {
  2181                                 TokenId id = token.id();
  2182                                 if (id != RubyTokenId.STRING_LITERAL && id != RubyTokenId.LINE_COMMENT &&
  2183                                         id != RubyTokenId.QUOTED_STRING_LITERAL && id != RubyTokenId.REGEXP_LITERAL &&
  2184                                         id != RubyTokenId.DOCUMENTATION) {
  2185                                     // Yes - allow surround with here
  2186 
  2187                                     // TODO - make this smarter by looking at the AST and see if
  2188                                     // we have a complete set of nodes
  2189                                     valid = true;
  2190                                 }
  2191                             }
  2192 
  2193                             break;
  2194                         }
  2195                     }
  2196                 }
  2197             } catch (BadLocationException ble) {
  2198                 // do nothing - see #154991
  2199             } finally {
  2200                 doc.readUnlock();
  2201             }
  2202         } else {
  2203             valid = true;
  2204         }
  2205         
  2206         return valid ? selectionTemplates : Collections.<String>emptySet();
  2207     }
  2208 
  2209     private String suggestName(ParserResult info, int caretOffset, String prefix, Map params) {
  2210         // Look at the given context, compute fields and see if I can find a free name
  2211         caretOffset = AstUtilities.boundCaretOffset(info, caretOffset);
  2212 
  2213         Node root = AstUtilities.getRoot(info);
  2214         if (root == null) return null;
  2215 
  2216         AstPath path = new AstPath(root, caretOffset);
  2217         Node closest = path.leaf();
  2218         if (closest == null) return null;
  2219 
  2220         // TODO: Look for a unique {global,class,instance} variable -- this requires looking at the index
  2221         if (prefix.startsWith("$") || prefix.startsWith("@")) return null;
  2222 
  2223         // Look for a local variable in the given scope
  2224         Node method = AstUtilities.findLocalScope(closest, path);
  2225         Map<String, Node> variables = new HashMap<String, Node>();
  2226         addLocals(method, variables);
  2227 
  2228         for (Node block : AstUtilities.getApplicableBlocks(path, false)) {
  2229             addDynamic(block, variables);
  2230         }
  2231                 
  2232         // See if we have any name suggestions
  2233         String suggestions = (String) params.get(ATTR_DEFAULTS);
  2234 
  2235         // Check the suggestions
  2236         if (suggestions != null && !suggestions.isEmpty()) {
  2237             String[] names = suggestions.split(",");
  2238 
  2239             for (String suggestion : names) {
  2240                 if (!variables.containsKey(suggestion)) return suggestion;
  2241             }
  2242 
  2243             // Try some variations of the name
  2244             for (String suggestion : names) {
  2245                 for (int number = 2; number < 5; number++) {
  2246                     String name = suggestion + number;
  2247 
  2248                     if (!variables.containsKey(name)) return name;
  2249                 }
  2250             }
  2251         }
  2252 
  2253         // Try the prefix
  2254         if (!prefix.isEmpty() && !variables.containsKey(prefix)) return prefix;
  2255 
  2256         // TODO: What's the right algorithm for uniqueifying a variable name in Ruby?
  2257         // For now, will just append a number
  2258         if (isEmpty(prefix)) prefix = "var";
  2259 
  2260         for (int number = 1; number < 15; number++) {
  2261             String name = number == 1 ? prefix : (prefix + number);
  2262 
  2263             if (!variables.containsKey(name)) return name;
  2264         }
  2265 
  2266         return null;
  2267     }
  2268 
  2269     @Override
  2270     public String resolveTemplateVariable(String variable, ParserResult result, int caretOffset,
  2271         String name, Map params) {
  2272         if (variable.equals(KEY_PIPE)) return "||";
  2273 
  2274         // Old-style format - support temporarily
  2275         if (variable.equals(ATTR_UNUSEDLOCAL)) { // TODO REMOVEME
  2276             return suggestName(result, caretOffset, name, params);
  2277         }
  2278 
  2279         if (params != null && params.containsKey(ATTR_UNUSEDLOCAL)) {
  2280             return suggestName(result, caretOffset, name, params);
  2281         }
  2282 
  2283         if ((!(variable.equals(KEY_METHOD) || variable.equals(KEY_METHOD_FQN) ||
  2284                 variable.equals(KEY_CLASS) || variable.equals(KEY_CLASS_FQN) ||
  2285                 variable.equals(KEY_SUPERCLASS) || variable.equals(KEY_PATH) ||
  2286                 variable.equals(KEY_FILE)))) {
  2287             return null;
  2288         }
  2289 
  2290         caretOffset = AstUtilities.boundCaretOffset(result, caretOffset);
  2291 
  2292         Node root = AstUtilities.getRoot(result);
  2293         if (root == null) return null;
  2294 
  2295         AstPath path = new AstPath(root, caretOffset);
  2296 
  2297         if (variable.equals(KEY_METHOD)) {
  2298             MethodDefNode method = AstUtilities.findMethodAtOffset(root, caretOffset);
  2299 
  2300             if (method != null) return method.getName();
  2301         } else if (variable.equals(KEY_METHOD_FQN)) {
  2302             MethodDefNode method = AstUtilities.findMethodAtOffset(root, caretOffset);
  2303 
  2304             if (method != null) {
  2305                 String ctx = AstUtilities.getFqnName(path);
  2306 
  2307                 return !ctx.isEmpty() ? ctx + "#" + method.getName() : method.getName();
  2308             }
  2309         } else if (variable.equals(KEY_CLASS)) {
  2310             ClassNode node = AstUtilities.findClass(path);
  2311 
  2312             if (node != null) return node.getCPath().getName();
  2313         } else if (variable.equals(KEY_SUPERCLASS)) {
  2314             ClassNode node = AstUtilities.findClass(path);
  2315 
  2316             if (node != null) {
  2317                 RubyIndex index = RubyIndex.get(result);
  2318                 if (index != null) {
  2319                     IndexedClass cls = index.getSuperclass(AstUtilities.getFqnName(path));
  2320 
  2321                     if (cls != null) return cls.getFqn();
  2322                 }
  2323 
  2324                 String superCls = AstUtilities.getSuperclass(node);
  2325                 
  2326                 return superCls != null ? superCls : "Object";
  2327             }
  2328         } else if (variable.equals(KEY_CLASS_FQN)) {
  2329             return AstUtilities.getFqnName(path);
  2330         } else if (variable.equals(KEY_FILE)) {
  2331             return FileUtil.toFile(result.getSnapshot().getSource().getFileObject()).getName();
  2332         } else if (variable.equals(KEY_PATH)) {
  2333             return FileUtil.toFile(RubyUtils.getFileObject(result)).getPath();
  2334         }
  2335 
  2336         return null;
  2337     }
  2338 
  2339     @Override
  2340     public ParameterInfo parameters(ParserResult info, int lexOffset, CompletionProposal proposal) {
  2341         IndexedMethod[] methodHolder = new IndexedMethod[1];
  2342         int[] paramIndexHolder = new int[1];
  2343         int[] anchorOffsetHolder = new int[1];
  2344         int astOffset = AstUtilities.getAstOffset(info, lexOffset);
  2345         if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
  2346                 methodHolder, paramIndexHolder, anchorOffsetHolder, null, QuerySupport.Kind.PREFIX)) {
  2347 
  2348             return ParameterInfo.NONE;
  2349         }
  2350 
  2351         IndexedMethod method = methodHolder[0];
  2352         if (method == null) {
  2353             return ParameterInfo.NONE;
  2354         }
  2355         int index = paramIndexHolder[0];
  2356         int astAnchorOffset = anchorOffsetHolder[0];
  2357         int anchorOffset = LexUtilities.getLexerOffset(info, astAnchorOffset);
  2358 
  2359 
  2360         // TODO: Make sure the caret offset is inside the arguments portion
  2361         // (parameter hints shouldn't work on the method call name itself
  2362         
  2363                 // See if we can find the method corresponding to this call
  2364         //        if (proposal != null) {
  2365         //            Element element = proposal.getElement();
  2366         //            if (element instanceof IndexedMethod) {
  2367         //                method = ((IndexedMethod)element);
  2368         //            }
  2369         //        }
  2370 
  2371         List<String> params = method.getParameters();
  2372 
  2373         if ((params != null) && (!params.isEmpty())) {
  2374             return new ParameterInfo(params, index, anchorOffset);
  2375         }
  2376 
  2377         return ParameterInfo.NONE;
  2378     }
  2379     
  2380     /** Return true if we always want to use parentheses
  2381      * @todo Make into a user-configurable option
  2382      * @todo Avoid doing this if there's possible ambiguity (e.g. nested method calls
  2383      *   without spaces
  2384      */
  2385     
  2386     @Override
  2387     public QueryType getAutoQuery(JTextComponent component, String typedText) {
  2388         char c = typedText.charAt(0);
  2389         
  2390         if (c == '\n' || c == '(' || c == '[' || c == '{') return QueryType.STOP;
  2391         if (c != '.' && c != ':') return QueryType.NONE;
  2392 
  2393         int offset = component.getCaretPosition();
  2394         BaseDocument doc = (BaseDocument) component.getDocument();
  2395 
  2396         if (".".equals(typedText)) { // NOI18N
  2397             // See if we're in Ruby context
  2398             TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
  2399             if (ts == null) return QueryType.NONE;
  2400 
  2401             ts.move(offset);
  2402             
  2403             if (!ts.moveNext() && !ts.movePrevious()) return QueryType.NONE;
  2404 
  2405             if (ts.offset() == offset && !ts.movePrevious()) return QueryType.NONE;
  2406 
  2407             Token<? extends RubyTokenId> token = ts.token();
  2408             TokenId id = token.id();
  2409             
  2410             if (id == RubyTokenId.RANGE) return QueryType.NONE; // ".." is a range, not dot completion
  2411 
  2412             // TODO - handle embedded ruby
  2413             if ("comment".equals(id.primaryCategory()) || // NOI18N
  2414                     "string".equals(id.primaryCategory()) ||  // NOI18N
  2415                     "regexp".equals(id.primaryCategory())) { // NOI18N
  2416                 return QueryType.NONE;
  2417             }
  2418             
  2419             return QueryType.COMPLETION;
  2420         }
  2421         
  2422         if (":".equals(typedText)) { // NOI18N
  2423             // See if it was "::" and we're in ruby context
  2424             int dot = component.getSelectionStart();
  2425             try {
  2426                 if ((dot > 1 && component.getText(dot-2, 1).charAt(0) == ':') && // NOI18N
  2427                         isRubyContext(doc, dot-1)) {
  2428                     return QueryType.COMPLETION;
  2429                 }
  2430             } catch (BadLocationException ble) {
  2431                 // do nothing - see #154991
  2432             }
  2433         }
  2434         
  2435         return QueryType.NONE;
  2436     }
  2437     
  2438     public static boolean isRubyContext(BaseDocument doc, int offset) {
  2439         TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
  2440 
  2441         if (ts == null) return false;
  2442         
  2443         ts.move(offset);
  2444         
  2445         if (!ts.movePrevious() && !ts.moveNext()) return true;
  2446         
  2447         TokenId id = ts.token().id();
  2448         if ("comment".equals(id.primaryCategory()) || "string".equals(id.primaryCategory()) || // NOI18N
  2449                 "regexp".equals(id.primaryCategory())) { // NOI18N
  2450             return false;
  2451         }
  2452         
  2453         return true;
  2454     }
  2455 
  2456 }