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