2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
6 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7 * Other names may be trademarks of their respective owners.
9 * The contents of this file are subject to the terms of either the GNU
10 * General Public License Version 2 only ("GPL") or the Common
11 * Development and Distribution License("CDDL") (collectively, the
12 * "License"). You may not use this file except in compliance with the
13 * License. You can obtain a copy of the License at
14 * http://www.netbeans.org/cddl-gplv2.html
15 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
16 * specific language governing permissions and limitations under the
17 * License. When distributing the software, include this License Header
18 * Notice in each file and include the License file at
19 * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
20 * particular file as subject to the "Classpath" exception as provided
21 * by Oracle in the GPL Version 2 section of the License file that
22 * accompanied this code. If applicable, add the following below the
23 * License Header, with the fields enclosed by brackets [] replaced by
24 * your own identifying information:
25 * "Portions Copyrighted [year] [name of copyright owner]"
29 * The Original Software is NetBeans. The Initial Developer of the Original
30 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
31 * Microsystems, Inc. All Rights Reserved.
33 * If you wish your version of this file to be governed by only the CDDL
34 * or only the GPL Version 2, indicate your decision by adding
35 * "[Contributor] elects to include this software in this distribution
36 * under the [CDDL or GPL Version 2] license." If you do not indicate a
37 * single choice of license, a recipient has the option to distribute
38 * your version of this file under either the CDDL, the GPL Version 2 or
39 * to extend the choice of license to its licensees as provided above.
40 * However, if you add GPL Version 2 code and therefore, elected the GPL
41 * Version 2 license, then the option applies only if the new code is
42 * made subject to such option by the copyright holder.
44 package org.netbeans.modules.ruby;
46 import java.io.BufferedInputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.List;
56 import javax.swing.text.BadLocationException;
57 import javax.swing.text.Document;
58 import javax.swing.text.JTextComponent;
59 import org.jrubyparser.SourcePosition;
60 import org.jrubyparser.ast.ArgsNode;
61 import org.jrubyparser.ast.ArgumentNode;
62 import org.jrubyparser.ast.ClassNode;
63 import org.jrubyparser.ast.ILocalScope;
64 import org.jrubyparser.ast.ListNode;
65 import org.jrubyparser.ast.LocalAsgnNode;
66 import org.jrubyparser.ast.MethodDefNode;
67 import org.jrubyparser.ast.Node;
68 import org.jrubyparser.ast.NodeType;
69 import org.jrubyparser.ast.INameNode;
70 import org.netbeans.api.lexer.Token;
71 import org.netbeans.api.lexer.TokenHierarchy;
72 import org.netbeans.api.lexer.TokenId;
73 import org.netbeans.api.lexer.TokenSequence;
74 import org.netbeans.api.ruby.platform.RubyInstallation;
75 import org.netbeans.editor.BaseDocument;
76 import org.netbeans.editor.Utilities;
77 import org.netbeans.modules.csl.api.CodeCompletionContext;
78 import org.netbeans.modules.csl.api.CodeCompletionHandler;
79 import org.netbeans.modules.csl.api.CodeCompletionHandler.QueryType;
80 import org.netbeans.modules.csl.api.CodeCompletionResult;
81 import org.netbeans.modules.csl.api.CompletionProposal;
82 import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
83 import org.netbeans.modules.csl.api.ElementHandle;
84 import org.netbeans.modules.csl.api.ElementKind;
85 import org.netbeans.modules.csl.api.ParameterInfo;
86 import org.netbeans.modules.csl.spi.DefaultCompletionResult;
87 import org.netbeans.modules.csl.spi.ParserResult;
88 import org.netbeans.modules.parsing.api.Snapshot;
89 import org.netbeans.modules.parsing.api.Source;
90 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
91 import org.netbeans.modules.ruby.RubyCompletionItem.CallItem;
92 import org.netbeans.modules.ruby.RubyCompletionItem.ClassItem;
93 import org.netbeans.modules.ruby.RubyCompletionItem.FieldItem;
94 import org.netbeans.modules.ruby.RubyCompletionItem.MethodItem;
95 import org.netbeans.modules.ruby.RubyCompletionItem.ParameterItem;
96 import org.netbeans.modules.ruby.elements.AstElement;
97 import org.netbeans.modules.ruby.elements.AstFieldElement;
98 import org.netbeans.modules.ruby.elements.AstNameElement;
99 import org.netbeans.modules.ruby.elements.CommentElement;
100 import org.netbeans.modules.ruby.elements.Element;
101 import org.netbeans.modules.ruby.elements.IndexedClass;
102 import org.netbeans.modules.ruby.elements.IndexedElement;
103 import org.netbeans.modules.ruby.elements.IndexedField;
104 import org.netbeans.modules.ruby.elements.IndexedMethod;
105 import org.netbeans.modules.ruby.elements.IndexedVariable;
106 import org.netbeans.modules.ruby.elements.KeywordElement;
107 import org.netbeans.modules.ruby.elements.RubyElement;
108 import org.netbeans.modules.ruby.lexer.Call;
109 import org.netbeans.modules.ruby.lexer.LexUtilities;
110 import org.netbeans.modules.ruby.lexer.RubyStringTokenId;
111 import org.netbeans.modules.ruby.lexer.RubyTokenId;
112 import org.openide.filesystems.FileObject;
113 import org.openide.filesystems.FileUtil;
114 import org.openide.util.Exceptions;
115 import org.openide.util.NbBundle;
117 import static org.netbeans.modules.ruby.RubyUtils.*;
120 * Code completion handler for Ruby.
122 * Bug: I add lists of fields etc. But if these -overlap- the current line,
123 * I throw them away. The problem is that there may be other references
124 * to the field that I should -not- throw away, elsewhere!
125 * @todo Ensure that I prefer assignment over reference such that javadoc is
126 * more likely to be there!
129 * @todo Handle this case: {@code class HTTPBadResponse < StandardError; end}
130 * @todo Code completion should automatically suggest "initialize()" for def completion! (if I'm in a class)
131 * @todo It would be nice if you select a method that takes a block, such as Array.each, if we could
132 * insert a { ^ } suffix
133 * @todo Use lexical tokens to avoid attempting code completion within comments,
134 * literal strings and regexps
135 * @todo Percent-completion doesn't work if you at this to the end of the
136 * document: x = % and try to complete.
137 * @todo Handle more completion scenarios: Classes (no keywords) after "class Foo <",
138 * classes after "::", parameter completion (!!!), .new() completion (initialize), etc.
139 * @todo Make sure completion works during a "::"
140 * @todo I need to move the smart-determination from just checking in=Object/Class/Module
141 * to the code which computes matches, since we have for example ObjectMixin in pretty printer
142 * which adds mixin methods to Object.
143 * @todo Handle Rails methods that deal with hashes:
144 * - Try figuring out whether the method should take parameters by looking for examples;
145 * lines that start with the method name and looks like it might have arguments
146 * - Try to figure out what the different parameters are if there are hashes
147 * - <tt>: looks like a parameter, e.g. "<tt>:filename</tt>"
148 * and to see which parameter it might correspond to, see the
149 * label; see if any of the parameter names are listed there (possibly in the args list)
150 * A fallback is to look for args that look like they may be hashes, e.g.
151 * def(foo1, foo2, foo3={}) - the third one is obviously a hash
152 * @todo Make code completion when we're in a parameter list include the parameters as well!
153 * @todo For .rjs files, insert an object named "page" of type
154 * ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods
156 * @todo For .builder files, insert an object named "xml" of type
158 * @todo For .rhtml/.html.erb files, insert fields etc. as documented in actionpack's lib/action_view/base.rb
160 * @todo For test files in Rails, get testing context (#105043). In particular, actionpack's
161 * ActionController::Assertions needs to be pulled in. This happens in action_controller/assertions.rb.
162 * @todo Require-completion should handle ruby gems; it should provide the "preferred" (entry-point) files for
163 * all the ruby gems, and it should hide all the files that are inside the gem
164 * @todo Rakefiles files should inherit Rakefile context
165 * @todo See http://blog.diegodoval.com/2007/09/ruby_on_os_x_some_useful_links.html
166 * @todo Documentation completion in a rdoc should preview that rdoc section
167 * @todo Make a dedicated completion item which I return on documentation completion if I want to
168 * complete the CURRENT element; it basically just wraps the desired comment so we can pull it
169 * out in the document() method
170 * @todo Provide code completion for "3|" or "3 |" - show available overloaded operators! This
171 * shouldn't just apply to numbers - any class you've overridden
172 * @todo Digest http://blogs.sun.com/coolstuff/entry/using_java_classes_in_jruby
173 * to fix require'java' etc.
174 * @todo http://www.innovationontherun.com/scraping-dynamic-websites-using-jruby-and-htmlunit/
175 * Idea: Use a quicktip to require all the jars in the project?
176 * @todo The "h" method in <%= %> doesn't show up in RHTML files... where is it?
177 * @todo Completion AFTER a method which takes a block (optional or required) should offer
181 public class RubyCodeCompleter implements CodeCompletionHandler {
183 // Another good logical parameter would be SINGLE_WHITESPACE which would
184 // insert a whitespace separator IF NEEDED
186 /** Live code template parameter: require the given file, if not already done so */
187 private static final String KEY_REQUIRE = "require"; // NOI18N
189 /** Live code template parameter: find a name in scope that is known to be of the given type */
190 private static final String KEY_INSTANCEOF = "instanceof"; // NOI18N
192 /** Live code template parameter: compute an unused local variable name */
193 private static final String ATTR_UNUSEDLOCAL = "unusedlocal"; // NOI18N
195 /** Live code template parameter: pipe variable, since | is a bit mishandled in the UI for editing abbrevs */
196 private static final String KEY_PIPE = "pipe"; // NOI18N
198 /** Live code template parameter: compute the method name */
199 private static final String KEY_METHOD = "method"; // NOI18N
201 /** Live code template parameter: compute the method signature */
202 private static final String KEY_METHOD_FQN = "methodfqn"; // NOI18N
204 /** Live code template parameter: compute the class name (not including the module prefix) */
205 private static final String KEY_CLASS = "class"; // NOI18N
207 /** Live code template parameter: compute the class fully qualified name */
208 private static final String KEY_CLASS_FQN = "classfqn"; // NOI18N
210 /** Live code template parameter: compute the superclass of the current class */
211 private static final String KEY_SUPERCLASS = "superclass"; // NOI18N
213 /** Live code template parameter: compute the filename (not including the path) of the file */
214 private static final String KEY_FILE = "file"; // NOI18N
216 /** Live code template parameter: compute the full path of the source directory */
217 private static final String KEY_PATH = "path"; // NOI18N
219 /** Default name values for ATTR_UNUSEDLOCAL and friends */
220 private static final String ATTR_DEFAULTS = "defaults"; // NOI18N
222 private static final Set<String> selectionTemplates = new HashSet<String>();
225 selectionTemplates.add("begin"); // NOI18N
226 selectionTemplates.add("do"); // NOI18N
227 selectionTemplates.add("doc"); // NOI18N
228 //selectionTemplates.add("dop"); // NOI18N
229 selectionTemplates.add("if"); // NOI18N
230 selectionTemplates.add("ife"); // NOI18N
233 private boolean caseSensitive;
236 public RubyCodeCompleter() {
239 static boolean startsWith(String theString, String prefix, boolean caseSensitive) {
240 if (prefix.length() == 0) return true;
242 return caseSensitive ? theString.startsWith(prefix)
243 : theString.toLowerCase().startsWith(prefix.toLowerCase());
246 private boolean startsWith(String theString, String prefix) {
247 return RubyCodeCompleter.startsWith(theString, prefix, caseSensitive);
251 * Compute an appropriate prefix to use for code completion.
252 * In Strings, we want to return the -whole- string if you're in a
253 * require-statement string, otherwise we want to return simply "" or the previous "\"
254 * for quoted strings, and ditto for regular expressions.
255 * For non-string contexts, just return null to let the default identifier-computation
258 @SuppressWarnings("unchecked")
260 public String getPrefix(ParserResult info, int lexOffset, boolean upToOffset) {
262 BaseDocument doc = RubyUtils.getDocument(info);
263 if (doc == null) return null;
265 TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
266 doc.readLock(); // Read-lock due to token hierarchy use
268 int requireStart = LexUtilities.getRequireStringOffset(lexOffset, th);
270 // XXX todo - do upToOffset
271 if (requireStart != -1) return doc.getText(requireStart, lexOffset - requireStart);
273 TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
274 if (ts == null) return null;
278 if (!ts.moveNext() && !ts.movePrevious()) return null;
280 // We're looking at the offset to the RIGHT of the caret and here I care about what's on the left
281 if (ts.offset() == lexOffset) ts.movePrevious();
283 Token<? extends RubyTokenId> token = ts.token();
286 TokenId id = token.id();
288 // We're within a String that has embedded Ruby. Drop into the
289 // embedded language and see if we're within a literal string there.
290 if (id == RubyTokenId.EMBEDDED_RUBY) {
291 ts = (TokenSequence) ts.embedded();
295 if (!ts.moveNext() && !ts.movePrevious()) return null;
301 String tokenText = token.text().toString();
303 if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
304 ((id == RubyTokenId.ERROR) && tokenText.equals("%"))) {
305 int currOffset = ts.offset();
307 // Percent completion
308 if ((currOffset == (lexOffset - 1)) && (tokenText.length() > 0) &&
309 (tokenText.charAt(0) == '%')) {
315 int doubleQuotedOffset = LexUtilities.getDoubleQuotedStringOffset(lexOffset, th);
317 if (doubleQuotedOffset != -1) {
318 // Tokenize the string and offer the current token portion as the text
319 if (doubleQuotedOffset == lexOffset) {
321 } else if (doubleQuotedOffset < lexOffset) {
322 String text = doc.getText(doubleQuotedOffset, lexOffset - doubleQuotedOffset);
324 TokenHierarchy.create(text, RubyStringTokenId.languageDouble());
326 TokenSequence seq = hi.tokenSequence();
328 seq.move(lexOffset - doubleQuotedOffset);
330 if (!seq.moveNext() && !seq.movePrevious()) {
334 TokenId id = seq.token().id();
335 String s = seq.token().text().toString();
337 if ((id == RubyStringTokenId.STRING_ESCAPE) ||
338 (id == RubyStringTokenId.STRING_INVALID)) {
340 } else if (s.startsWith("\\")) {
346 // The String offset is greater than the caret position.
347 // This means that we're inside the string-begin section,
348 // for example here: %q|(
349 // In this case, report no prefix
354 int singleQuotedOffset = LexUtilities.getSingleQuotedStringOffset(lexOffset, th);
356 if (singleQuotedOffset != -1) {
357 if (singleQuotedOffset == lexOffset) {
359 } else if (singleQuotedOffset < lexOffset) {
360 String text = doc.getText(singleQuotedOffset, lexOffset - singleQuotedOffset);
362 TokenHierarchy.create(text, RubyStringTokenId.languageSingle());
364 TokenSequence seq = hi.tokenSequence();
366 seq.move(lexOffset - singleQuotedOffset);
368 if (!seq.moveNext() && !seq.movePrevious()) {
372 TokenId id = seq.token().id();
373 String s = seq.token().text().toString();
375 if ((id == RubyStringTokenId.STRING_ESCAPE) ||
376 (id == RubyStringTokenId.STRING_INVALID)) {
378 } else if (s.startsWith("\\")) {
384 // The String offset is greater than the caret position.
385 // This means that we're inside the string-begin section,
386 // for example here: %q|(
387 // In this case, report no prefix
392 // Regular expression
393 int regexpOffset = LexUtilities.getRegexpOffset(lexOffset, th);
395 if ((regexpOffset != -1) && (regexpOffset <= lexOffset)) {
396 // This is not right... I need to actually parse the regexp
397 // (I should use my Regexp lexer tokens which will be embedded here)
398 // such that escaping sequences (/\\\\\/) will work right, or
399 // character classes (/[foo\]). In both cases the \ may not mean escape.
400 String tokenText = token.text().toString();
401 int index = lexOffset - ts.offset();
403 if ((index > 0) && (index <= tokenText.length()) &&
404 (tokenText.charAt(index - 1) == '\\')) {
407 // No prefix for regexps unless it's \
411 //return doc.getText(regexpOffset, offset-regexpOffset);
414 int lineBegin = Utilities.getRowStart(doc, lexOffset);
415 if (lineBegin != -1) {
416 int lineEnd = Utilities.getRowEnd(doc, lexOffset);
417 String line = doc.getText(lineBegin, lineEnd - lineBegin);
418 int lineOffset = lexOffset - lineBegin;
419 int start = lineOffset;
420 if (lineOffset > 0) {
421 for (int i = lineOffset - 1; i >= 0; i--) {
422 char c = line.charAt(i);
423 if (!RubyUtils.isIdentifierChar(c)) {
431 // Find identifier end
434 prefix = line.substring(start, lineOffset);
436 if (lineOffset == line.length()) {
437 prefix = line.substring(start);
439 int n = line.length();
440 int end = lineOffset;
441 for (int j = lineOffset; j < n; j++) {
442 char d = line.charAt(j);
443 // Try to accept Foo::Bar as well
444 if (!RubyUtils.isStrictIdentifierChar(d)) {
450 prefix = line.substring(start, end);
454 if (prefix.length() > 0) {
455 if (prefix.endsWith("::")) {
459 if (prefix.endsWith(":") && prefix.length() > 1) {
463 // Strip out LHS if it's a qualified method, e.g. Benchmark::measure -> measure
464 int q = prefix.lastIndexOf("::");
467 prefix = prefix.substring(q + 2);
470 // The identifier chars identified by RubyLanguage are a bit too permissive;
471 // they include things like "=", "!" and even "&" such that double-clicks will
472 // pick up the whole "token" the user is after. But "=" is only allowed at the
473 // end of identifiers for example.
474 if (prefix.length() == 1) {
475 char c = prefix.charAt(0);
476 if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$' || c == ':')) {
480 for (int i = prefix.length() - 2; i >= 0; i--) { // -2: the last position (-1) can legally be =, ! or ?
481 char c = prefix.charAt(i);
482 if (i == 0 && c == ':') {
483 // : is okay at the begining of prefixes
484 } else if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$')) {
485 prefix = prefix.substring(i + 1);
497 // Else: normal identifier: just return null and let the machinery do the rest
498 } catch (BadLocationException ble) {
499 // do nothing - see #154991;
506 /** Determine if we're trying to complete the name for a "def" (in which case
507 * we'd show the inherited methods).
508 * This needs to be enhanced to handle "Foo." prefixes, e.g. def self.foo
510 private boolean completeDefOrInclude(List<CompletionProposal> proposals, CompletionRequest request, String fqn) {
511 RubyIndex index = request.index;
512 String prefix = request.prefix;
513 int lexOffset = request.lexOffset;
514 TokenHierarchy<Document> th = request.th;
515 QuerySupport.Kind kind = request.kind;
517 TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
519 if ((index != null) && (ts != null)) {
522 if (!ts.moveNext() && !ts.movePrevious()) {
526 if (ts.offset() == lexOffset) {
527 // We're looking at the offset to the RIGHT of the caret
528 // position, which could be whitespace, e.g.
529 // "def fo| " <-- looking at the whitespace
533 Token<?extends RubyTokenId> token = ts.token();
536 TokenId id = token.id();
538 // See if we're in the identifier - "foo" in "def foo"
539 // I could also be a keyword in case the prefix happens to currently
540 // match a keyword, such as "next"
541 if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CONSTANT) || id.primaryCategory().equals("keyword")) {
542 if (!ts.movePrevious()) {
550 // If we're not in the identifier we need to be in the whitespace after "def"
551 if (id != RubyTokenId.WHITESPACE) {
552 // Do something about http://www.netbeans.org/issues/show_bug.cgi?id=100452 here
553 // In addition to checking for whitespace I should look for "Foo." here
557 // There may be more than one whitespace; skip them
558 while (ts.movePrevious()) {
561 if (token.id() != RubyTokenId.WHITESPACE) {
566 if (token.id() == RubyTokenId.DEF) {
567 Set<IndexedMethod> methods = index.getInheritedMethods(fqn, prefix, kind);
569 for (IndexedMethod method : methods) {
570 // Hmmm, is this necessary? Filtering should happen in the getInheritedMEthods call
571 if ((prefix.length() > 0) && !method.getName().startsWith(prefix)) {
575 // For def completion, skip local methods, only include superclass and included
576 if ((fqn != null) && fqn.equals(method.getClz())) {
580 if (method.isNoDoc()) {
584 // If a method is an "initialize" method I should do something special so that
585 // it shows up as a "constructor" (in a new() statement) but not as a directly
586 // callable initialize method (it should already be culled because it's private)
587 MethodItem item = new MethodItem(method, anchor, request);
589 item.setSmart(method.isSmart());
594 } else if (token.id() == RubyTokenId.IDENTIFIER && "include".equals(token.text().toString())) {
596 Set<IndexedClass> classes = index.getClasses(prefix, kind, false, true, false);
597 for (IndexedClass clz : classes) {
602 ClassItem item = new ClassItem(clz, anchor, request);
615 private void completeGlobals(List<CompletionProposal> proposals, CompletionRequest request, boolean showSymbols) {
616 RubyIndex index = request.index;
617 String prefix = request.prefix;
618 QuerySupport.Kind kind = request.kind;
620 Set<IndexedVariable> globals = index.getGlobals(prefix, kind);
621 for (IndexedVariable global : globals) {
622 RubyCompletionItem item = new RubyCompletionItem(global, anchor, request);
626 item.setSymbol(true);
633 private boolean addParameters(List<CompletionProposal> proposals, CompletionRequest request) {
634 IndexedMethod[] methodHolder = new IndexedMethod[1];
635 @SuppressWarnings("unchecked")
636 Set<IndexedMethod>[] alternatesHolder = new Set[1];
637 int[] paramIndexHolder = new int[1];
638 int[] anchorOffsetHolder = new int[1];
639 ParserResult info = request.parserResult;
640 int lexOffset = request.lexOffset;
641 int astOffset = request.astOffset;
642 if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
643 methodHolder, paramIndexHolder, anchorOffsetHolder, alternatesHolder, request.kind)) {
648 IndexedMethod targetMethod = methodHolder[0];
649 int index = paramIndexHolder[0];
651 CallItem callItem = new CallItem(targetMethod, index, anchor, request);
652 proposals.add(callItem);
653 // Also show other documented, not nodoc'ed items (except for those
654 // with identical signatures, such as overrides of the same method)
655 if (alternatesHolder[0] != null) {
656 Set<String> signatures = new HashSet<String>();
657 signatures.add(targetMethod.getSignature().substring(targetMethod.getSignature().indexOf('#')+1));
658 for (IndexedMethod m : alternatesHolder[0]) {
659 if (m != targetMethod && m.isDocumented() && !m.isNoDoc()) {
660 String sig = m.getSignature().substring(m.getSignature().indexOf('#')+1);
661 if (!signatures.contains(sig)) {
662 CallItem item = new CallItem(m, index, anchor, request);
670 List<String> params = targetMethod.getParameters();
671 if (params == null || params.isEmpty()) {
675 if (params.size() <= index) {
676 // Just use the last parameter in these cases
677 // See for example the TableDefinition.binary dynamic method where
678 // you can add a number of parameter names and the options parameter
679 // is always the last one
680 index = params.size()-1;
683 boolean isLastArg = index < params.size()-1;
685 String attrs = targetMethod.getEncodedAttributes();
686 if (attrs != null && attrs.length() > 0) {
688 for (int i = 0; i < 3; i++) {
689 offset = attrs.indexOf(';', offset+1);
697 root = AstUtilities.getRoot(info);
700 IndexedElement match = findDocumentationEntry(root, targetMethod);
701 if (match == targetMethod || !(match instanceof IndexedMethod)) {
704 targetMethod = (IndexedMethod)match;
705 attrs = targetMethod.getEncodedAttributes();
706 if (attrs != null && attrs.length() > 0) {
708 for (int i = 0; i < 3; i++) {
709 offset = attrs.indexOf(';', offset+1);
716 String currentName = params.get(index);
717 if (currentName.startsWith("*")) {
718 // * and & are part of the sig
719 currentName = currentName.substring(1);
720 } else if (currentName.startsWith("&")) {
721 currentName = currentName.substring(1);
725 attrs = attrs.substring(offset+1);
726 if (attrs.length() == 0) {
729 String[] argEntries = attrs.split(",");
730 for (String entry : argEntries) {
731 int parenIndex = entry.indexOf('(');
732 assert parenIndex != -1 : attrs;
733 String name = entry.substring(0, parenIndex);
734 if (currentName.equals(name)) {
735 // Found a special parameter desc entry for this
736 // parameter - decode it and create completion items
738 int endIndex = entry.indexOf(')', parenIndex);
739 assert endIndex != -1;
740 String data = entry.substring(parenIndex+1, endIndex);
741 if (data.length() > 0 && data.charAt(0) == '-') {
742 // It's a plain item (e.g. not a hash etc) where
743 // we have some logical types to complete
744 if ("-table".equals(data)) {
745 completeDbTables(proposals, targetMethod, request, isLastArg);
746 // Not exiting - I may have other entries here too
747 } else if ("-column".equals(data)) {
748 completeDbColumns(proposals, targetMethod, request, isLastArg);
749 // Not exiting - I may have other entries here too
750 } else if ("-model".equals(data)) {
751 completeModels(proposals, targetMethod, request, isLastArg);
753 } else if (data.startsWith("=>")) {
754 // It's a hash; show the given keys
755 // TODO: Determine if the caret is in the
756 // value part, and if so, show the values instead
757 // Uhm... what about fields and such?
758 completeHash(proposals, request, targetMethod, data, isLastArg);
759 // Not exiting - I may have a non-hash entry here too!
761 // Just show a fixed set of values
762 completeFixed(proposals, request, targetMethod, data, isLastArg);
763 // Not exiting - I may have other entries here too
773 // /** Handle insertion of :action, :controller, etc. even for methods without
774 // * actual method signatures. Operate at the lexical level.
776 // private void handleRailsKeys(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
777 // TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
781 // boolean inValue = false;
783 // String line = null;
784 // while (ts.movePrevious()) {
785 // final Token token = ts.token();
786 // if (token.id() == RubyTokenId.WHITESPACE) {
788 // } else if (token.id() == RubyTokenId.NONUNARY_OP &&
789 // (token.text().toString().equals("=>"))) { // NOI18N
791 // // TODO - continue on to find out what the key is
793 // BaseDocument doc = request.doc;
794 // int lineStart = Utilities.getRowStart(doc, ts.offset());
795 // line = doc.getText(lineStart, ts.offset()-lineStart).trim();
796 // } catch (BadLocationException ble) {
797 // Exceptions.printStackTrace(ble);
806 // if (line.endsWith(":action")) {
808 // } else if (line.endsWith(":controller")) {
809 // // Dynamically produce controllers
810 // List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
811 // String prefix = request.prefix;
812 // for (String n : controllers) {
813 // n = "'" + n + "'";
814 // if (startsWith(n, prefix)) {
815 // String insert = n;
817 // insert = insert + ", ";
819 // ParameterItem item = new ParameterItem(target, n, null, insert, anchor, request);
820 // item.setSymbol(true);
821 // item.setSmart(true);
822 // proposals.add(item);
825 // } else if (line.endsWith(":partial")) {
831 private boolean completeHash(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
832 assert data.startsWith("=>");
833 data = data.substring(2);
834 String prefix = request.prefix;
836 // Determine if we're in the key part or the value part when completing
837 boolean inValue = false;
838 TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
845 while (ts.movePrevious()) {
846 final Token token = ts.token();
847 if (token.id() == RubyTokenId.WHITESPACE) {
849 } else if (token.id() == RubyTokenId.NONUNARY_OP &&
850 (token.text().toString().equals("=>"))) { // NOI18N
852 // TODO - continue on to find out what the key is
854 BaseDocument doc = request.doc;
855 int lineStart = Utilities.getRowStart(doc, ts.offset());
856 line = doc.getText(lineStart, ts.offset()-lineStart).trim();
857 } catch (BadLocationException ble) {
865 List<String> suggestions = new ArrayList<String>();
868 String[] values = data.split("\\|");
870 // Find the key and see if we have a type to offer for it
871 for (String value : values) {
872 int typeIndex = value.indexOf(':');
873 if (typeIndex != -1) {
874 String name = value.substring(0, typeIndex);
875 if (line.endsWith(name)) {
877 // Score - it appears we're using the
879 String type = value.substring(typeIndex+1);
880 if ("nil".equals(type)) { // NOI18N
881 suggestions.add("nil"); // NOI18N
882 } else if ("bool".equals(type)) { // NOI18N
883 suggestions.add("true"); // NOI18N
884 suggestions.add("false"); // NOI18N
885 } else if ("submitmethod".equals(type)) { // NOI18N
886 suggestions.add("post"); // NOI18N
887 suggestions.add("get"); // NOI18N
888 } else if ("validationactive".equals(type)) { // NOI18N
889 suggestions.add(":save"); // NOI18N
890 suggestions.add(":create"); // NOI18N
891 suggestions.add(":update"); // NOI18N
892 } else if ("string".equals(type)) { // NOI18N
893 suggestions.add("\""); // NOI18N
894 } else if ("hash".equals(type)) { // NOI18N
895 suggestions.add("{"); // NOI18N
896 } else if ("controller".equals(type)) {
897 // Dynamically produce controllers
898 List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
899 for (String n : controllers) {
900 suggestions.add("'" + n + "'");
902 } else if ("action".equals(type)) {
903 // Dynamically produce actions
904 // This would need to be scoped by the current
905 // context - look at the hash, find the specified
906 // controller and limit it to that
907 List<String> actions = getActionNames(request);
908 for (String n : actions) {
909 suggestions.add("'" + n + "'");
911 } else if ("status".equals(type)) {
912 return RubyHttpStatusCodeCompleter.complete(proposals, request, anchor, caseSensitive, target);
918 for (String value : values) {
919 int typeIndex = value.indexOf(':');
920 if (typeIndex != -1) {
921 value = value.substring(0, typeIndex);
923 value = ":" + value + " => ";
924 suggestions.add(value);
928 // I've gotta clean up the colon handling in complete()
929 // I originally stripped ":" to make direct (INameNode)getName()
930 // comparisons on symbols work directly but it's becoming a liability now
931 String colonPrefix = ":" + prefix;
932 for (String suggestion : suggestions) {
933 if (startsWith(suggestion, prefix) || startsWith(suggestion, colonPrefix)) {
934 String insert = suggestion;
938 insert = insert + ", ";
941 desc = ":" + key + " = " + suggestion;
944 ParameterItem item = new ParameterItem(target, suggestion, desc, insert, anchor, request);
945 item.setSymbol(true);
954 /** Get the actions for the given file. If the file is a controller, list the actions within it,
955 * otherwise, if the file is a view, list the actions for the corresponding controller.
957 * @param fileInProject the file we're looking up
958 * @return A List of action names
960 private List<String> getActionNames(CompletionRequest request) {
961 FileObject file = request.fileObject;
962 FileObject controllerFile = null;
963 if (file.getNameExt().endsWith("_controller.rb")) {
964 controllerFile = file;
966 controllerFile = RubyUtils.getRailsControllerFor(file);
968 // TODO - check for other :controller-> settings in the hashmap and if present, use it
969 if (controllerFile == null) {
970 return Collections.emptyList();
973 String controllerClass = RubyUtils.getControllerClass(controllerFile);
974 if (controllerClass != null) {
975 String prefix = request.prefix;
976 Set<IndexedMethod> methods = request.index.getMethods(prefix, controllerClass, request.kind);
977 List<String> actions = new ArrayList<String>();
978 for (IndexedMethod method : methods) {
979 if (method.isPublic() && method.getArgs() == null || method.getArgs().length == 0) {
980 actions.add(method.getName());
987 // TODO - pull out the methods or this class
989 return Collections.emptyList();
992 private void completeFixed(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
993 String[] values = data.split("\\|");
994 String prefix = request.prefix;
995 // I originally stripped ":" to make direct (INameNode)getName()
996 // comparisons on symbols work directly but it's becoming a liability now
997 String colonPrefix = ":" + prefix;
998 for (String value : values) {
999 if (startsWith(value, prefix) || startsWith(value, colonPrefix)) {
1000 String insert = isLastArg ? value : (value + ", ");
1001 ParameterItem item = new ParameterItem(target, value, null, insert, anchor, request);
1002 item.setSymbol(true);
1003 item.setSmart(true);
1004 proposals.add(item);
1009 private void completeDbTables(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
1010 // Add in the eligible database tables found in this project
1011 // Assumes this is a Rails project
1012 String p = request.prefix;
1013 String colonPrefix = p;
1014 if (":".equals(p)) { // NOI18N
1017 colonPrefix = ":" + p; // NOI18N
1019 Set<String> tables = request.index.getDatabaseTables(p, request.kind);
1021 // I originally stripped ":" to make direct (INameNode)getName()
1022 // comparisons on symbols work directly but it's becoming a liability now
1023 String prefix = request.prefix;
1024 for (String table : tables) {
1025 // PENDING: Should I insert :tablename or 'tablename' or "tablename" ?
1026 String tableName = ":" + table;
1027 if (startsWith(tableName, prefix) || startsWith(tableName, colonPrefix)) {
1028 String insert = isLastArg ? tableName : (tableName + ", ");
1029 ParameterItem item = new ParameterItem(target, tableName, null, insert, anchor, request);
1030 item.setSymbol(true);
1031 item.setSmart(true);
1032 proposals.add(item);
1037 private void completeModels(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
1038 Set<IndexedClass> clz = request.index.getSubClasses(request.prefix, RubyIndex.ACTIVE_RECORD_BASE, request.kind);
1040 String prefix = request.prefix;
1041 // I originally stripped ":" to make direct (INameNode)getName()
1042 // comparisons on symbols work directly but it's becoming a liability now
1043 String colonPrefix = ":" + prefix;
1044 for (IndexedClass c : clz) {
1045 String name = c.getName();
1046 String symbol = ":"+RubyUtils.camelToUnderlinedName(name);
1047 if (startsWith(symbol, prefix) || startsWith(symbol, colonPrefix)) {
1048 String insert = isLastArg ? symbol : (symbol + ", ");
1049 ParameterItem item = new ParameterItem(target, symbol, name, insert, anchor, request);
1050 item.setSymbol(true);
1051 item.setSmart(true);
1052 proposals.add(item);
1057 private void completeDbColumns(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
1058 // Add in the eligible database tables found in this project
1059 // Assumes this is a Rails project
1060 // Set<String> tables = request.index.getDatabaseTables(request.prefix, request.kind);
1063 // for (String table : tables) {
1064 // if (startsWith(table, prefix)) {
1065 // SymbolHashItem item = new SymbolHashItem(target, ":" + table, null, anchor, request);
1066 // item.setSymbol(true);
1067 // proposals.add(item);
1072 private boolean isEmpty(String value) {
1073 return value == null || value.length() == 0;
1076 // TODO: Move to the top
1078 public CodeCompletionResult complete(final CodeCompletionContext context) {
1079 ParserResult ir = context.getParserResult();
1080 int lexOffset = context.getCaretOffset();
1081 String prefix = context.getPrefix();
1082 QuerySupport.Kind kind = context.isPrefixMatch() ? QuerySupport.Kind.PREFIX : QuerySupport.Kind.EXACT;
1083 QueryType queryType = context.getQueryType();
1084 this.caseSensitive = context.isCaseSensitive();
1086 final int astOffset = AstUtilities.getAstOffset(ir, lexOffset);
1087 if (astOffset == -1) return null;
1089 // Avoid all those annoying null checks
1090 if (prefix == null) prefix = "";
1092 List<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
1093 DefaultCompletionResult completionResult = new DefaultCompletionResult(proposals, false);
1095 anchor = lexOffset - prefix.length();
1097 final RubyIndex index = RubyIndex.get(ir);
1099 final Document document = RubyUtils.getDocument(ir);
1100 if (document == null) return CodeCompletionResult.NONE;
1102 // TODO - move to LexUtilities now that this applies to the lexing offset?
1103 lexOffset = AstUtilities.boundCaretOffset(ir, lexOffset);
1105 // Discover whether we're in a require statement, and if so, use special completion
1106 final TokenHierarchy<Document> th = TokenHierarchy.get(document);
1107 final BaseDocument doc = (BaseDocument)document;
1108 final FileObject fileObject = RubyUtils.getFileObject(ir);
1110 boolean showLower = true;
1111 boolean showUpper = true;
1112 boolean showSymbols = false;
1115 doc.readLock(); // Read-lock due to Token hierarchy use
1117 if (prefix.length() > 0) {
1118 first = prefix.charAt(0);
1120 // Foo::bar --> first char is "b" - we're looking for a method
1121 int qualifier = prefix.lastIndexOf("::");
1122 if (qualifier != -1 && qualifier < prefix.length() - 2) first = prefix.charAt(qualifier + 2);
1124 showLower = Character.isLowerCase(first);
1125 // showLower is not necessarily !showUpper - prefix can be ":foo" for example
1126 showUpper = Character.isUpperCase(first);
1131 if (prefix.length() > 1) {
1132 char second = prefix.charAt(1);
1133 prefix = prefix.substring(1);
1134 showLower = Character.isLowerCase(second);
1135 showUpper = Character.isUpperCase(second);
1140 // Carry completion context around since this logic is split across lots of methods
1141 // and I don't want to pass dozens of parameters from method to method; just pass
1142 // a request context with supporting parserResult needed by the various completion helpers.
1143 CompletionRequest request = new CompletionRequest(
1144 completionResult, th, ir, lexOffset, astOffset,
1145 doc, prefix, index, kind, queryType, fileObject);
1147 // See if we're inside a string or regular expression and if so,
1148 // do completions applicable to strings - require-completion,
1149 // escape codes for quoted strings and regular expressions, etc.
1150 if (RubyStringCompleter.complete(proposals, request, anchor, caseSensitive)) {
1151 completionResult.setFilterable(false);
1152 return completionResult;
1155 Call call = Call.getCallType(doc, th, lexOffset);
1158 // This is a bit stupid at the moment, not looking at the current typing context etc.
1159 Node root = AstUtilities.getRoot(ir);
1162 RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
1163 return completionResult;
1166 // Compute the bounds of the line that the caret is on, and suppress nodes overlapping the line.
1167 // This will hide not only paritally typed identifiers, but surrounding contents like the current class and module
1168 final int astLineBegin;
1169 final int astLineEnd;
1172 astLineBegin = AstUtilities.getAstOffset(ir, Utilities.getRowStart(doc, lexOffset));
1173 astLineEnd = AstUtilities.getAstOffset(ir, Utilities.getRowEnd(doc, lexOffset));
1174 } catch (BadLocationException ble) {
1175 return CodeCompletionResult.NONE;
1178 final AstPath path = new AstPath(root, astOffset);
1179 request.path = path;
1181 Map<String, Node> variables = new HashMap<String, Node>();
1182 Map<String, Node> fields = new HashMap<String, Node>();
1183 Map<String, Node> globals = new HashMap<String, Node>();
1184 Map<String, Node> constants = new HashMap<String, Node>();
1186 final Node closest = path.leaf();
1187 request.target = closest;
1189 // Don't try to add local vars, globals etc. as part of calls or class fqns
1190 if (call.getLhs() == null) {
1191 if (showLower && closest != null) {
1192 for (Node block : AstUtilities.getApplicableBlocks(path, false)) {
1193 addDynamic(block, variables);
1196 for (Node child : AstUtilities.findLocalScope(closest, path).childNodes()) {
1197 addLocals(child, variables);
1201 boolean inAttrCall = isInAttr(closest, path);
1203 if (prefix.length() == 0 || first == '@' || showSymbols || inAttrCall) {
1204 String fqn = AstUtilities.getFqnName(path);
1207 String fileName = RubyUtils.getFileObject(context.getParserResult()).getName();
1208 if (fileName.endsWith("_spec")) { //NOI18N
1209 // use the virtual class created for the spec file
1210 fqn = RubyUtils.underlinedNameToCamel(fileName);
1212 fqn = "Object"; // NOI18N
1216 // TODO - if fqn has multiple ::'s, try various combinations? or is
1217 // add inherited already doing that?
1219 Set<IndexedField> f;
1220 if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
1221 f = new HashSet<IndexedField>();
1222 addActionViewFields(f, fileObject, index, prefix, kind);
1224 //strip out ':' when querying fields for cases like 'attr_reader :^'
1225 if (inAttrCall && first == ':' && prefix.length() == 1) {
1226 f = index.getInheritedFields(fqn, "", kind, false);
1228 f = index.getInheritedFields(fqn, prefix, kind, false);
1232 for (IndexedField field : f) {
1233 String insertPrefix = inAttrCall ? ":" : null;
1234 FieldItem item = new FieldItem(field, anchor, request, insertPrefix);
1236 item.setSmart(field.isSmart());
1238 if (showSymbols) item.setSymbol(true);
1240 proposals.add(item);
1243 // return just the fields for attr_
1244 if (inAttrCall) return completionResult;
1247 // $ is neither upper nor lower
1248 if (prefix.length() == 0 || first == '$' || showSymbols) {
1249 if (prefix.startsWith("$") || showSymbols) {
1250 completeGlobals(proposals, request, showSymbols);
1251 // Dollar variables too
1252 RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
1254 if (!showSymbols) return completionResult;
1259 // TODO: should only include fields etc. down to caret location??? Decide. (Depends on language semantics. Can I have forward referemces?
1260 if (call.isConstantExpected()) {
1261 RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
1262 RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
1263 RubyType type = call.getType();
1264 if (type.isKnown() && type.isSingleton()) {
1265 RubyMethodCompleter.complete(proposals, request, type.first(), call, anchor, caseSensitive);
1267 return completionResult;
1270 // If we're in a call, add in some parserResult and help for the code completion call
1271 boolean inCall = addParameters(proposals, request);
1273 // Code completion from the index.
1274 if (index != null) {
1275 if (showLower || showSymbols) {
1276 String fqn = AstUtilities.getFqnName(path);
1277 if (isEmpty(fqn)) fqn = "Object"; // NOI18N
1279 // doesn't apply to (or work with) documentation/tooltip help
1280 if (queryType == QueryType.COMPLETION && completeDefOrInclude(proposals, request, fqn)) {
1281 return completionResult;
1284 if (RubyMethodCompleter.complete(proposals, request, fqn, call, anchor, caseSensitive)) {
1285 return completionResult;
1288 // Only call local and inherited methods if we don't have an LHS, such as Foo::
1289 if (call.getLhs() == null) {
1290 // TODO - pull this into a completeInheritedMethod call
1291 // Complete inherited methods or local methods only (plus keywords) since there
1292 // is no receiver so it must be a local or inherited method call
1293 Set<IndexedMethod> inheritedMethods = index.getInheritedMethods(fqn, prefix, kind);
1295 inheritedMethods = RubyDynamicFindersCompleter.proposeDynamicMethods(inheritedMethods, proposals, request, anchor);
1296 // Handle action view completion for RHTML and Markaby files
1297 if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
1298 addActionViewMethods(inheritedMethods, fileObject, index, prefix, kind);
1299 } else if (fileObject.getName().endsWith("_spec")) { // NOI18N
1302 /* My spec object had the following extras methods over a plain Object:
1303 x = self.class.methods
1309 > copy_instance_variables_from
1319 String includes[] = {
1320 // "describe" should be in Kernel already, from spec/runner/extensions/kernel.rb
1322 // This one shouldn't be necessary since there's a
1323 // "class Object; include xxx::ObjectExpectations; end" in rspec's object.rb
1324 "Spec::Expectations::ObjectExpectations",
1325 "Spec::DSL::BehaviourEval::InstanceMethods"}; // NOI18N
1326 for (String fqns : includes) {
1327 inheritedMethods.addAll(index.getInheritedMethods(fqns, prefix, kind));
1331 for (IndexedMethod method : inheritedMethods) {
1332 // This should not be necessary - filtering happens in getInheritedMethods right?
1333 if (prefix.length() > 0 && !method.getName().startsWith(prefix)) continue;
1334 if (method.isNoDoc()) continue;
1336 // If a method is an "initialize" method I should do something special so that
1337 // it shows up as a "constructor" (in a new() statement) but not as a directly
1338 // callable initialize method (it should already be culled because it's private)
1339 MethodItem item = new MethodItem(method, anchor, request);
1341 item.setSmart(method.isSmart());
1343 if (showSymbols) item.setSymbol(true);
1345 proposals.add(item);
1351 // doesn't apply to (or work with) documentation/tooltip help
1352 if (queryType == QueryType.COMPLETION && completeDefOrInclude(proposals, request, "")) {
1353 return completionResult;
1356 if ((showUpper && ((prefix != null && prefix.length() > 0) ||
1357 (!call.isMethodExpected() && call.getLhs() != null && call.getLhs().length() > 0))) || (showSymbols && !inCall)) {
1358 // TODO - allow method calls if you're already entered the first char!
1359 RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
1360 RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
1363 assert (kind == QuerySupport.Kind.PREFIX) || (kind == QuerySupport.Kind.CASE_INSENSITIVE_PREFIX) ||
1364 (kind == QuerySupport.Kind.EXACT);
1367 // Remove fields and variables whose names are already taken, e.g. do a fields.removeAll(variables) etc.
1368 for (String variable : variables.keySet()) {
1369 if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
1370 ((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
1371 Node node = variables.get(variable);
1373 if (!overlapsLine(node, astLineBegin, astLineEnd)) {
1374 AstElement co = new AstNameElement(ir, node, variable,
1375 ElementKind.VARIABLE);
1376 RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
1377 item.setSmart(true);
1380 item.setSymbol(true);
1383 proposals.add(item);
1388 for (String field : fields.keySet()) {
1389 if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(field)) ||
1390 ((kind != QuerySupport.Kind.EXACT) && startsWith(field, prefix))) {
1391 Node node = fields.get(field);
1393 if (overlapsLine(node, astLineBegin, astLineEnd)) {
1397 Element co = new AstFieldElement(ir, node);
1398 FieldItem item = new FieldItem(co, anchor, request);
1399 item.setSmart(true);
1402 item.setSymbol(true);
1405 proposals.add(item);
1409 // TODO - model globals and constants using different icons / etc.
1410 for (String variable : globals.keySet()) {
1411 // TODO - kind.EXACT
1412 if (startsWith(variable, prefix) ||
1413 (showSymbols && startsWith(variable.substring(1), prefix))) {
1414 Node node = globals.get(variable);
1416 if (overlapsLine(node, astLineBegin, astLineEnd)) {
1420 AstElement co = new AstNameElement(ir, node, variable,
1421 ElementKind.VARIABLE);
1422 RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
1423 item.setSmart(true);
1426 item.setSymbol(true);
1429 proposals.add(item);
1433 // TODO - model globals and constants using different icons / etc.
1434 for (String variable : constants.keySet()) {
1435 if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
1436 ((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
1437 // Skip constants that are known to be classes
1438 Node node = constants.get(variable);
1440 if (overlapsLine(node, astLineBegin, astLineEnd)) continue;
1443 // if (isClassName(variable)) {
1444 // co = JRubyNode.create(target, null);
1445 // if (co == null) {
1449 // co = new DefaultComVariable(variable, false, -1, -1);
1450 // ((DefaultComVariable)co).setNode(target);
1451 AstElement co = new AstNameElement(ir, node, variable, ElementKind.VARIABLE);
1453 RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
1454 item.setSmart(true);
1457 item.setSymbol(true);
1460 proposals.add(item);
1464 if (RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols)) {
1465 return completionResult;
1468 if (queryType == QueryType.DOCUMENTATION) {
1469 proposals = filterDocumentation(proposals, root, doc, ir, astOffset, lexOffset, prefix, path,
1476 return completionResult;
1480 private boolean isInAttr(Node closest, AstPath path) {
1481 if (closest != null) {
1482 // first argument in attr_*
1483 for (Node child : closest.childNodes()) {
1484 if (AstUtilities.isAttr(child)) {
1488 // others, e.g. attr_reader :foo, :ba^r
1489 if (AstUtilities.isAttr(path.leafParent()) || AstUtilities.isAttr(path.leafGrandParent())) {
1496 private void addActionViewMethods(Set<IndexedMethod> inheritedMethods, FileObject fileObject, RubyIndex index, String prefix,
1497 QuerySupport.Kind kind) {
1498 // RHTML and Markaby: Add in the helper methods etc. from the associated files
1499 boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
1501 Set<IndexedMethod> actionView = index.getInheritedMethods("ActionView::Base", prefix, kind); // NOI18N
1502 inheritedMethods.addAll(actionView);
1505 if (isRhtmlFile(fileObject) || isMarkaby) {
1506 // Hack - include controller and helper files as well
1507 FileObject f = fileObject.getParent();
1508 // name of the controller w/o the "controller" suffix
1509 String controllerName = null;
1510 // XXX Will this work for .mab files? Where do they go?
1511 while (f != null && !f.getName().equals("views")) { // todo - make sure grandparent is app
1512 String n = underlinedNameToCamel(f.getName());
1513 if (controllerName == null) {
1516 controllerName = n + "::" + controllerName;
1521 // // add in all methods from the associated helper and inherited helpers. this will
1522 // add also ApplicationHelper, which is global
1523 Set<String> helperNames = new HashSet<String>();
1524 helperNames.add(helperName(controllerName));
1525 for (IndexedClass superClass : index.getSuperClasses(controllerName(controllerName))) {
1526 if ("ActionController::Base".equals(superClass.getFqn())) { //NOI18N
1529 helperNames.add(helperName(superClass.getFqn()));
1531 for (String helper : helperNames) {
1532 inheritedMethods.addAll(index.getInheritedMethods(helper, prefix, kind));
1535 index.getSuperClasses(controllerName);
1536 // TODO - pull in the fields (NOT THE METHODS) from the controller
1537 //Set<IndexedMethod> controller = index.getInheritedMethods(controllerName+"Controller", prefix, kind);
1538 //inheritedMethods.addAll(controller);
1542 private void addActionViewFields(Set<IndexedField> inheritedFields, FileObject fileObject, RubyIndex index, String prefix,
1543 QuerySupport.Kind kind) {
1544 // RHTML and Markaby: Add in the helper methods etc. from the associated files
1545 boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
1547 Set<IndexedField> actionView = index.getInheritedFields("ActionView::Base", prefix, kind, true); // NOI18N
1548 inheritedFields.addAll(actionView);
1551 if (RubyUtils.isRhtmlFile(fileObject) || isMarkaby) {
1552 // Hack - include controller and helper files as well
1553 FileObject f = fileObject.getParent();
1554 String controllerName = null;
1555 // XXX Will this work for .mab files? Where do they go?
1556 while (f != null && !f.getName().equals("views")) { // NOI18N // todo - make sure grandparent is app
1557 String n = RubyUtils.underlinedNameToCamel(f.getName());
1558 if (controllerName == null) {
1561 controllerName = n + "::" + controllerName; // NOI18N
1566 String fqn = controllerName+"Controller"; // NOI18N
1567 Set<IndexedField> controllerFields = index.getInheritedFields(fqn, prefix, kind, true);
1568 for (IndexedField field : controllerFields) {
1569 if ("ActionController::Base".equals(field.getIn())) { // NOI18N
1572 inheritedFields.add(field);
1577 /** If we're doing documentation completion, try to drop the list down to a single alternative
1578 * (since the framework will just use the first produced result), and in particular, the -best-
1581 // TODO - pass in request object here!
1582 private List<CompletionProposal> filterDocumentation(List<CompletionProposal> proposals,
1583 Node root, BaseDocument doc, ParserResult parserResult, int astOffset, int lexOffset, String name,
1584 AstPath path, RubyIndex index) {
1585 // Look to see if this symbol is either a "class Foo" or a "def foo", and if we invoke
1586 // completion on it, prefer this element provided it has documentation
1587 List<CompletionProposal> candidates = new ArrayList<CompletionProposal>();
1588 FileObject fo = RubyUtils.getFileObject(parserResult);
1589 Map<IndexedElement, CompletionProposal> elementMap =
1590 new HashMap<IndexedElement, CompletionProposal>();
1591 Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
1592 Set<IndexedClass> classes = new HashSet<IndexedClass>();
1594 for (CompletionProposal proposal : proposals) {
1595 RubyElement e = (RubyElement) proposal.getElement();
1597 if (e instanceof IndexedElement) {
1598 IndexedElement ie = (IndexedElement)e;
1600 if (ie instanceof IndexedClass) {
1601 classes.add((IndexedClass)ie);
1602 elementMap.put(ie, proposal);
1603 } else if (ie instanceof IndexedMethod) {
1604 methods.add((IndexedMethod)ie);
1605 elementMap.put(ie, proposal);
1608 if (ie.getFileObject() == fo) {
1609 // The class is in this file - if it has documentation, prefer it
1610 candidates.add(proposal);
1615 // Check the candidates to see if one of them is actually -defined-
1616 // under the caret; e.g. if you have "class File" with documentation,
1617 // and you ctrl-space on it, you always want to show THIS documentation
1618 // for File, not the standard one defined elsewhere.
1619 for (CompletionProposal candidate : candidates) {
1620 // See if the candidate corresponds to the caret position
1621 RubyElement re = (RubyElement) candidate.getElement();
1622 if (!(re instanceof IndexedElement)) {
1625 IndexedElement e = (IndexedElement)re;
1626 String signature = e.getSignature();
1627 Node node = AstUtilities.findBySignature(root, signature);
1630 SourcePosition pos = node.getPosition();
1631 int startPos = LexUtilities.getLexerOffset(parserResult, pos.getStartOffset());
1634 int lineBegin = AstUtilities.getAstOffset(parserResult, Utilities.getRowFirstNonWhite(doc, startPos));
1635 int lineEnd = AstUtilities.getAstOffset(parserResult, Utilities.getRowEnd(doc, startPos));
1637 if ((astOffset >= lineBegin) && (astOffset <= lineEnd)) {
1638 // Look for documentation
1639 List<String> rdoc = AstUtilities.gatherDocumentation(parserResult.getSnapshot(), node);
1641 if (rdoc != null && !rdoc.isEmpty()) {
1642 return Collections.singletonList(candidate);
1645 } catch (BadLocationException ble) {
1646 // The parse information is too old - the document has shrunk. Do nothing, the
1647 // AST nodes are pointing into the old contents.
1652 // Try to pick the best match among many documentation entries: Heuristic time.
1653 // Similar to heuristics used for Go To Declaration: Prefer long documentation,
1654 // prefer documentation related to the require-statements in this file, etc.
1655 IndexedElement candidate = null;
1657 if (!classes.isEmpty()) {
1658 RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(parserResult, null, path, index, path.leaf());
1659 candidate = cdf.findBestElementMatch(classes);
1660 } else if (!methods.isEmpty()) {
1661 RubyDeclarationFinder finder = new RubyDeclarationFinder();
1662 candidate = finder.findBestMethodMatch(name, methods, doc, astOffset, lexOffset, path,
1663 path.leaf(), index);
1666 if (candidate != null) {
1667 CompletionProposal proposal = elementMap.get(candidate);
1669 if (proposal != null) {
1670 return Collections.singletonList(proposal);
1677 // private boolean isClassName(String s) {
1678 // // Initial capital letter, second letter is not
1679 // if (s.length() == 1) {
1680 // return Character.isUpperCase(s.charAt(0));
1683 // if (Character.isLowerCase(s.charAt(0))) {
1687 // return Character.isLowerCase(s.charAt(1));
1689 private boolean overlapsLine(Node node, int lineBegin, int lineEnd) {
1690 SourcePosition pos = node.getPosition();
1692 //return (((pos.getStartOffset() <= lineEnd) && (pos.getEndOffset() >= lineBegin)));
1693 // Don't look to see if the line is within the target. See if the target is started on this line (where
1694 // the declaration is, e.g. it might be an incomplete line.
1695 return ((pos.getStartOffset() >= lineBegin) && (pos.getStartOffset() <= lineEnd));
1698 // /** Return true iff the name looks like an operator name */
1699 // private boolean isOperator(String name) {
1700 // // If a name contains not a single letter, it is probably an operator - especially
1701 // // if it is a short name
1702 // int n = name.length();
1708 // for (int i = 0; i < n; i++) {
1709 // if (Character.isLetter(name.charAt(i))) {
1717 static void addLocals(Node node, Map<String, Node> variables) {
1718 switch (node.getNodeType()) {
1719 case LOCALASGNNODE: {
1720 String name = ((INameNode)node).getName();
1722 if (!variables.containsKey(name)) {
1723 variables.put(name, node);
1728 // TODO - use AstUtilities.getDefArgs here - but avoid hitting them twice!
1729 //List<String> parameters = AstUtilities.getDefArgs(def, true);
1730 // However, I've gotta find the parameter nodes themselves too!
1731 ArgsNode an = (ArgsNode)node;
1733 if (an.getRequiredCount() > 0) {
1734 for (Node arg : an.childNodes()) {
1735 if (arg instanceof ListNode) {
1736 for (Node arg2 : arg.childNodes()) {
1737 if (arg2 instanceof ArgumentNode) {
1738 variables.put(((ArgumentNode)arg2).getName(), arg2);
1739 } else if (arg2 instanceof LocalAsgnNode) {
1740 variables.put(((INameNode)arg2).getName(), arg2);
1748 if (an.getRest() != null) {
1749 String name = an.getRest().getName();
1750 variables.put(name, an.getRest());
1754 if (an.getBlock() != null) {
1755 String name = an.getBlock().getName();
1756 variables.put(name, an.getBlock());
1762 // } else if (target instanceof AliasNode) {
1763 // AliasNode an = (AliasNode)target;
1764 // Tricky -- which NODE do we add here? Completion creator needs to be aware of new name etc. Do later.
1765 // Besides, do we show it as a field or a method or what?
1768 // if (an.getNewName().equals(name)) {
1769 // OffsetRange range = AstUtilities.getAliasNewRange(an);
1770 // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1771 // } else if (an.getOldName().equals(name)) {
1772 // OffsetRange range = AstUtilities.getAliasOldRange(an);
1773 // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1778 for (Node child : node.childNodes()) {
1779 if (child instanceof ILocalScope) continue; // ignore nested local scopes
1781 addLocals(child, variables);
1785 static void addDynamic(Node node, Map<String, Node> variables) {
1786 if (node.getNodeType() == NodeType.DASGNNODE) {
1787 String name = ((INameNode)node).getName();
1789 if (!variables.containsKey(name)) {
1790 variables.put(name, node);
1793 //} else if (target instanceof ArgsNode) {
1794 // ArgsNode an = (ArgsNode)target;
1796 // if (an.getArgsCount() > 0) {
1797 // List<Node> args = an.childNodes();
1798 // List<String> parameters = null;
1800 // for (Node arg : args) {
1801 // if (arg instanceof ListNode) {
1802 // List<Node> args2 = arg.childNodes();
1803 // parameters = new ArrayList<String>(args2.size());
1805 // for (Node arg2 : args2) {
1806 // if (arg2 instanceof ArgumentNode) {
1807 // OffsetRange range = AstUtilities.getRange(arg2);
1808 // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1809 // } else if (arg2 instanceof LocalAsgnNode) {
1810 // OffsetRange range = AstUtilities.getRange(arg2);
1811 // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1817 // } else if (!ignoreAlias && target instanceof AliasNode) {
1818 // AliasNode an = (AliasNode)target;
1820 // if (an.getNewName().equals(name)) {
1821 // OffsetRange range = AstUtilities.getAliasNewRange(an);
1822 // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1823 // } else if (an.getOldName().equals(name)) {
1824 // OffsetRange range = AstUtilities.getAliasOldRange(an);
1825 // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1829 for (Node child : node.childNodes()) {
1830 if (child instanceof ILocalScope || child.getNodeType() == NodeType.ITERNODE) continue;
1832 addDynamic(child, variables);
1836 private String loadResource(String basename) {
1838 InputStream is = null;
1841 StringBuilder sb = new StringBuilder();
1842 is = new BufferedInputStream(RubyCodeCompleter.class.getResourceAsStream("resources/" +
1853 if (sb.length() > 0) return sb.toString();
1854 } catch (IOException ie) {
1855 Exceptions.printStackTrace(ie);
1858 if (is != null) is.close();
1859 } catch (IOException ie) {
1860 Exceptions.printStackTrace(ie);
1867 private String getKeywordHelp(String keyword) {
1868 // Difficulty here with context; "else" is used for both the ifelse.html and case.html both define it.
1869 // End is even more used.
1870 if (keyword.equals("if") || keyword.equals("elsif") || keyword.equals("else") ||
1871 keyword.equals("then") || keyword.equals("unless")) { // NOI18N
1873 return loadResource("ifelse.html"); // NOI18N
1874 } else if (keyword.equals("case") || keyword.equals("when") || keyword.equals("else")) { // NOI18N
1876 return loadResource("case.html"); // NOI18N
1877 } else if (keyword.equals("rescue") || keyword.equals("ensure")) { // NOI18N
1879 return loadResource("rescue.html"); // NOI18N
1880 } else if (keyword.equals("yield")) { // NOI18N
1882 return loadResource("yield.html"); // NOI18N
1889 * Find the best possible documentation match for the given IndexedClass or IndexedMethod.
1890 * This involves looking at index to see which instances of this class or method
1891 * definition have associated rdoc, as well as choosing between them based on the
1892 * require statements in the file.
1894 static IndexedElement findDocumentationEntry(Node root, IndexedElement obj) {
1895 // 1. Find entries known to have documentation
1896 String fqn = obj.getSignature();
1897 Set<?extends IndexedElement> result = obj.getIndex().getDocumented(fqn);
1899 if ((result == null) || (result.isEmpty())) {
1901 } else if (result.size() == 1) {
1902 return result.iterator().next();
1905 // 2. There are multiple matches so try to disambiguate them by the imports in this file.
1906 // For example, for "File" we usually show the standard (builtin) documentation,
1907 // unless you have required "ftools", which redefines File with new docs.
1908 Set<IndexedElement> candidates;
1910 candidates = new HashSet<IndexedElement>();
1911 Set<String> requires = AstUtilities.getRequires(root);
1913 for (IndexedElement o : result) {
1914 String require = o.getRequire();
1916 if (requires.contains(require)) {
1921 if (candidates.size() == 1) {
1922 return candidates.iterator().next();
1923 } else if (!candidates.isEmpty()) {
1924 result = candidates;
1928 // 3. Prefer builtin (kernel) docs over other docs.
1929 candidates = new HashSet<IndexedElement>();
1931 for (IndexedElement o : result) {
1932 String url = o.getFileUrl();
1934 if (RubyUtils.isRubyStubsURL(url)) {
1939 if (candidates.size() == 1) {
1940 return candidates.iterator().next();
1941 } else if (!candidates.isEmpty()) {
1942 result = candidates;
1945 // 4. Consider other heuristics, like picking the "larger" documentation
1948 // 5. Just pick an arbitrary one.
1949 return result.iterator().next();
1953 * @todo If you invoke this on top of a symbol, I should really just show
1954 * the documentation for that symbol!
1956 * @param element The element we want to look up comments for
1957 * @param parserResult The (optional) compilation parserResult for a document referencing the element.
1958 * This is used to consult require-statements in the given compilation context etc.
1959 * to choose among many alternatives. May be null, in which case the element had
1960 * better be an IndexedElement.
1962 static List<String> getComments(ParserResult info, Element element) {
1963 assert info != null || element instanceof IndexedElement;
1965 if (element == null) {
1971 if (element instanceof AstElement) {
1972 node = ((AstElement)element).getNode();
1973 } else if (element instanceof IndexedElement) {
1974 IndexedElement com = (IndexedElement)element;
1977 root = AstUtilities.getRoot(info);
1980 IndexedElement match = findDocumentationEntry(root, com);
1982 if (match != null) {
1987 node = AstUtilities.getForeignNode(com);
1993 assert false : element;
1998 // Initially, I implemented this by using RubyParserResult.getCommentNodes.
1999 // However, I -still- had to rely on looking in the Document itself, since
2000 // the CommentNodes are not attached to the AST, and to do things the way
2001 // RDoc does, I have to (for example) look to see if a comment is at the
2002 // beginning of a line or on the same line as something else, or if two
2003 // comments have any empty lines between them, and so on.
2004 // When I started looking in the document itself, I realized I might as well
2005 // do all the manipulation on the document, since having the Comment nodes
2006 // don't particularly help.
2008 if (element instanceof IndexedElement) {
2009 FileObject f = ((IndexedElement) element).getFileObject();
2010 snapshot = Source.create(f).createSnapshot();
2011 } else if (info != null) {
2012 snapshot = info.getSnapshot();
2017 List<String> comments = null;
2019 // Check for RubyComObject: These are external files (like Ruby lib) where I need to check many files
2020 if (node instanceof ClassNode && !(element instanceof IndexedElement)) {
2021 String className = AstUtilities.getClassOrModuleName((ClassNode)node);
2022 List<ClassNode> classes = AstUtilities.getClasses(AstUtilities.getRoot(info));
2024 // Iterate backwards through the list because the most recent documentation
2025 // should be chosen, if any
2026 for (int i = classes.size() - 1; i >= 0; i--) {
2027 ClassNode clz = classes.get(i);
2028 String name = AstUtilities.getClassOrModuleName(clz);
2030 if (name.equals(className)) {
2031 comments = AstUtilities.gatherDocumentation(snapshot, clz);
2033 if ((comments != null) && (!comments.isEmpty())) {
2039 comments = AstUtilities.gatherDocumentation(snapshot, node);
2042 if ((comments == null) || (comments.isEmpty())) {
2050 public String document(ParserResult info, ElementHandle handle) {
2051 Element element = null;
2052 if (handle instanceof ElementHandle.UrlHandle) {
2053 String url = ((ElementHandle.UrlHandle)handle).getUrl();
2054 DeclarationLocation loc = new RubyDeclarationFinder().findLinkedMethod(info, url);
2055 if (loc != DeclarationLocation.NONE) {
2056 element = RubyParser.resolveHandle(info, loc.getElement());
2057 if (element == null) return null;
2060 element = RubyParser.resolveHandle(info, handle);
2062 if (element == null) return null;
2063 if (element instanceof KeywordElement) return getKeywordHelp(((KeywordElement)element).getName());
2064 if (element instanceof CommentElement) {
2065 // Text is packaged as the name
2066 String comment = element.getName();
2067 RDocFormatter formatter = new RDocFormatter();
2068 String[] comments = comment.split("\n");
2069 for (String text : comments) {
2070 // Truncate off leading whitespace before # on comment lines
2071 for (int i = 0, n = text.length(); i < n; i++) {
2072 char c = text.charAt(i);
2075 text = text.substring(i);
2078 } else if (!Character.isWhitespace(c)) {
2082 formatter.appendLine(text);
2084 return formatter.toHtml();
2087 List<String> comments = getComments(info, element);
2088 if (comments == null) {
2089 if (FindersHelper.isFinderMethod(element.getName(), false)) {
2090 return new RDocFormatter().getSignature(element) + NbBundle.getMessage(RubyCodeCompleter.class, "DynamicMethod");
2092 String html = new RDocFormatter().getSignature(element) + "\n<hr>\n<i>" + NbBundle.getMessage(RubyCodeCompleter.class, "NoCommentFound") +"</i>";
2097 RDocFormatter formatter = new RDocFormatter();
2098 String name = element.getName();
2099 if (name != null && name.length() > 0) {
2100 formatter.setSeqName(name);
2103 for (String text : comments) {
2104 formatter.appendLine(text);
2107 String html = formatter.toHtml();
2108 if (!formatter.wroteSignature()) html = formatter.getSignature(element) + "\n<hr>\n" + html;
2114 public ElementHandle resolveLink(String link, ElementHandle elementHandle) {
2115 if (elementHandle == null) return null;
2117 if (link.indexOf('#') != -1 && elementHandle.getMimeType().equals(RubyInstallation.RUBY_MIME_TYPE)) {
2118 if (link.startsWith("#")) {
2119 // Put the current class etc. in front of the method call if necessary
2120 Element surrounding = RubyParser.resolveHandle(null, elementHandle);
2121 if (surrounding != null && surrounding.getKind() != ElementKind.KEYWORD) {
2122 String name = surrounding.getName();
2123 ElementKind kind = surrounding.getKind();
2124 if (!(kind == ElementKind.CLASS || kind == ElementKind.MODULE)) {
2125 String in = surrounding.getIn();
2126 if (in != null && in.length() > 0) {
2128 } else if (name != null) {
2129 int index = name.indexOf('#');
2130 if (index > 0) name = name.substring(0, index);
2133 if (name != null) link = name + link;
2136 return new ElementHandle.UrlHandle(link);
2142 // before csl.api 2.11:
2143 public Set<String> getApplicableTemplates(ParserResult info, int selectionBegin, int selectionEnd) {
2144 return getApplicableTemplates(RubyUtils.getDocument(info), selectionBegin, selectionEnd);
2146 // after csl.api 2.11:
2148 public Set<String> getApplicableTemplates(Document d, int selectionBegin, int selectionEnd) {
2150 // TODO - check the code at the AST path and determine whether it makes sense to
2151 // wrap it in a begin block etc.
2152 // TODO - I'd like to be able to pass any selection-based templates I'm not familiar with
2154 boolean valid = false;
2156 if (selectionEnd != -1) {
2157 if (d == null || !(d instanceof BaseDocument) || selectionBegin == selectionEnd) return Collections.emptySet();
2159 BaseDocument doc = (BaseDocument) d;
2162 if (selectionEnd < selectionBegin) {
2163 int temp = selectionBegin;
2164 selectionBegin = selectionEnd;
2165 selectionEnd = temp;
2167 boolean startLineIsEmpty = Utilities.isRowEmpty(doc, selectionBegin);
2168 boolean endLineIsEmpty = Utilities.isRowEmpty(doc, selectionEnd);
2170 if ((startLineIsEmpty || selectionBegin <= Utilities.getRowFirstNonWhite(doc, selectionBegin)) &&
2171 (endLineIsEmpty || selectionEnd > Utilities.getRowLastNonWhite(doc, selectionEnd))) {
2172 // I have no text to the left of the beginning or text to the right of the end, but I might
2173 // have just selected whitespace - check that
2174 String text = doc.getText(selectionBegin, selectionEnd-selectionBegin);
2175 for (int i = 0; i < text.length(); i++) {
2176 if (!Character.isWhitespace(text.charAt(i))) {
2178 // Make sure that we're not in a string etc
2179 Token<?> token = LexUtilities.getToken(doc, selectionBegin);
2180 if (token != null) {
2181 TokenId id = token.id();
2182 if (id != RubyTokenId.STRING_LITERAL && id != RubyTokenId.LINE_COMMENT &&
2183 id != RubyTokenId.QUOTED_STRING_LITERAL && id != RubyTokenId.REGEXP_LITERAL &&
2184 id != RubyTokenId.DOCUMENTATION) {
2185 // Yes - allow surround with here
2187 // TODO - make this smarter by looking at the AST and see if
2188 // we have a complete set of nodes
2197 } catch (BadLocationException ble) {
2198 // do nothing - see #154991
2206 return valid ? selectionTemplates : Collections.<String>emptySet();
2209 private String suggestName(ParserResult info, int caretOffset, String prefix, Map params) {
2210 // Look at the given context, compute fields and see if I can find a free name
2211 caretOffset = AstUtilities.boundCaretOffset(info, caretOffset);
2213 Node root = AstUtilities.getRoot(info);
2214 if (root == null) return null;
2216 AstPath path = new AstPath(root, caretOffset);
2217 Node closest = path.leaf();
2218 if (closest == null) return null;
2220 // TODO: Look for a unique {global,class,instance} variable -- this requires looking at the index
2221 if (prefix.startsWith("$") || prefix.startsWith("@")) return null;
2223 // Look for a local variable in the given scope
2224 Node method = AstUtilities.findLocalScope(closest, path);
2225 Map<String, Node> variables = new HashMap<String, Node>();
2226 addLocals(method, variables);
2228 for (Node block : AstUtilities.getApplicableBlocks(path, false)) {
2229 addDynamic(block, variables);
2232 // See if we have any name suggestions
2233 String suggestions = (String) params.get(ATTR_DEFAULTS);
2235 // Check the suggestions
2236 if (suggestions != null && !suggestions.isEmpty()) {
2237 String[] names = suggestions.split(",");
2239 for (String suggestion : names) {
2240 if (!variables.containsKey(suggestion)) return suggestion;
2243 // Try some variations of the name
2244 for (String suggestion : names) {
2245 for (int number = 2; number < 5; number++) {
2246 String name = suggestion + number;
2248 if (!variables.containsKey(name)) return name;
2254 if (!prefix.isEmpty() && !variables.containsKey(prefix)) return prefix;
2256 // TODO: What's the right algorithm for uniqueifying a variable name in Ruby?
2257 // For now, will just append a number
2258 if (isEmpty(prefix)) prefix = "var";
2260 for (int number = 1; number < 15; number++) {
2261 String name = number == 1 ? prefix : (prefix + number);
2263 if (!variables.containsKey(name)) return name;
2270 public String resolveTemplateVariable(String variable, ParserResult result, int caretOffset,
2271 String name, Map params) {
2272 if (variable.equals(KEY_PIPE)) return "||";
2274 // Old-style format - support temporarily
2275 if (variable.equals(ATTR_UNUSEDLOCAL)) { // TODO REMOVEME
2276 return suggestName(result, caretOffset, name, params);
2279 if (params != null && params.containsKey(ATTR_UNUSEDLOCAL)) {
2280 return suggestName(result, caretOffset, name, params);
2283 if ((!(variable.equals(KEY_METHOD) || variable.equals(KEY_METHOD_FQN) ||
2284 variable.equals(KEY_CLASS) || variable.equals(KEY_CLASS_FQN) ||
2285 variable.equals(KEY_SUPERCLASS) || variable.equals(KEY_PATH) ||
2286 variable.equals(KEY_FILE)))) {
2290 caretOffset = AstUtilities.boundCaretOffset(result, caretOffset);
2292 Node root = AstUtilities.getRoot(result);
2293 if (root == null) return null;
2295 AstPath path = new AstPath(root, caretOffset);
2297 if (variable.equals(KEY_METHOD)) {
2298 MethodDefNode method = AstUtilities.findMethodAtOffset(root, caretOffset);
2300 if (method != null) return method.getName();
2301 } else if (variable.equals(KEY_METHOD_FQN)) {
2302 MethodDefNode method = AstUtilities.findMethodAtOffset(root, caretOffset);
2304 if (method != null) {
2305 String ctx = AstUtilities.getFqnName(path);
2307 return !ctx.isEmpty() ? ctx + "#" + method.getName() : method.getName();
2309 } else if (variable.equals(KEY_CLASS)) {
2310 ClassNode node = AstUtilities.findClass(path);
2312 if (node != null) return node.getCPath().getName();
2313 } else if (variable.equals(KEY_SUPERCLASS)) {
2314 ClassNode node = AstUtilities.findClass(path);
2317 RubyIndex index = RubyIndex.get(result);
2318 if (index != null) {
2319 IndexedClass cls = index.getSuperclass(AstUtilities.getFqnName(path));
2321 if (cls != null) return cls.getFqn();
2324 String superCls = AstUtilities.getSuperclass(node);
2326 return superCls != null ? superCls : "Object";
2328 } else if (variable.equals(KEY_CLASS_FQN)) {
2329 return AstUtilities.getFqnName(path);
2330 } else if (variable.equals(KEY_FILE)) {
2331 return FileUtil.toFile(result.getSnapshot().getSource().getFileObject()).getName();
2332 } else if (variable.equals(KEY_PATH)) {
2333 return FileUtil.toFile(RubyUtils.getFileObject(result)).getPath();
2340 public ParameterInfo parameters(ParserResult info, int lexOffset, CompletionProposal proposal) {
2341 IndexedMethod[] methodHolder = new IndexedMethod[1];
2342 int[] paramIndexHolder = new int[1];
2343 int[] anchorOffsetHolder = new int[1];
2344 int astOffset = AstUtilities.getAstOffset(info, lexOffset);
2345 if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
2346 methodHolder, paramIndexHolder, anchorOffsetHolder, null, QuerySupport.Kind.PREFIX)) {
2348 return ParameterInfo.NONE;
2351 IndexedMethod method = methodHolder[0];
2352 if (method == null) {
2353 return ParameterInfo.NONE;
2355 int index = paramIndexHolder[0];
2356 int astAnchorOffset = anchorOffsetHolder[0];
2357 int anchorOffset = LexUtilities.getLexerOffset(info, astAnchorOffset);
2360 // TODO: Make sure the caret offset is inside the arguments portion
2361 // (parameter hints shouldn't work on the method call name itself
2363 // See if we can find the method corresponding to this call
2364 // if (proposal != null) {
2365 // Element element = proposal.getElement();
2366 // if (element instanceof IndexedMethod) {
2367 // method = ((IndexedMethod)element);
2371 List<String> params = method.getParameters();
2373 if ((params != null) && (!params.isEmpty())) {
2374 return new ParameterInfo(params, index, anchorOffset);
2377 return ParameterInfo.NONE;
2380 /** Return true if we always want to use parentheses
2381 * @todo Make into a user-configurable option
2382 * @todo Avoid doing this if there's possible ambiguity (e.g. nested method calls
2387 public QueryType getAutoQuery(JTextComponent component, String typedText) {
2388 char c = typedText.charAt(0);
2390 if (c == '\n' || c == '(' || c == '[' || c == '{') return QueryType.STOP;
2391 if (c != '.' && c != ':') return QueryType.NONE;
2393 int offset = component.getCaretPosition();
2394 BaseDocument doc = (BaseDocument) component.getDocument();
2396 if (".".equals(typedText)) { // NOI18N
2397 // See if we're in Ruby context
2398 TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
2399 if (ts == null) return QueryType.NONE;
2403 if (!ts.moveNext() && !ts.movePrevious()) return QueryType.NONE;
2405 if (ts.offset() == offset && !ts.movePrevious()) return QueryType.NONE;
2407 Token<? extends RubyTokenId> token = ts.token();
2408 TokenId id = token.id();
2410 if (id == RubyTokenId.RANGE) return QueryType.NONE; // ".." is a range, not dot completion
2412 // TODO - handle embedded ruby
2413 if ("comment".equals(id.primaryCategory()) || // NOI18N
2414 "string".equals(id.primaryCategory()) || // NOI18N
2415 "regexp".equals(id.primaryCategory())) { // NOI18N
2416 return QueryType.NONE;
2419 return QueryType.COMPLETION;
2422 if (":".equals(typedText)) { // NOI18N
2423 // See if it was "::" and we're in ruby context
2424 int dot = component.getSelectionStart();
2426 if ((dot > 1 && component.getText(dot-2, 1).charAt(0) == ':') && // NOI18N
2427 isRubyContext(doc, dot-1)) {
2428 return QueryType.COMPLETION;
2430 } catch (BadLocationException ble) {
2431 // do nothing - see #154991
2435 return QueryType.NONE;
2438 public static boolean isRubyContext(BaseDocument doc, int offset) {
2439 TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
2441 if (ts == null) return false;
2445 if (!ts.movePrevious() && !ts.moveNext()) return true;
2447 TokenId id = ts.token().id();
2448 if ("comment".equals(id.primaryCategory()) || "string".equals(id.primaryCategory()) || // NOI18N
2449 "regexp".equals(id.primaryCategory())) { // NOI18N