/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.jrubyparser.SourcePosition;
import org.jrubyparser.ast.ArgsNode;
import org.jrubyparser.ast.ArgumentNode;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.LocalAsgnNode;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.INameNode;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.csl.api.CodeCompletionContext;
import org.netbeans.modules.csl.api.CodeCompletionHandler;
import org.netbeans.modules.csl.api.CodeCompletionHandler.QueryType;
import org.netbeans.modules.csl.api.CodeCompletionResult;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.ParameterInfo;
import org.netbeans.modules.csl.spi.DefaultCompletionResult;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.ruby.RubyCompletionItem.CallItem;
import org.netbeans.modules.ruby.RubyCompletionItem.ClassItem;
import org.netbeans.modules.ruby.RubyCompletionItem.FieldItem;
import org.netbeans.modules.ruby.RubyCompletionItem.MethodItem;
import org.netbeans.modules.ruby.RubyCompletionItem.ParameterItem;
import org.netbeans.modules.ruby.elements.AstElement;
import org.netbeans.modules.ruby.elements.AstFieldElement;
import org.netbeans.modules.ruby.elements.AstNameElement;
import org.netbeans.modules.ruby.elements.CommentElement;
import org.netbeans.modules.ruby.elements.Element;
import org.netbeans.modules.ruby.elements.IndexedClass;
import org.netbeans.modules.ruby.elements.IndexedElement;
import org.netbeans.modules.ruby.elements.IndexedField;
import org.netbeans.modules.ruby.elements.IndexedMethod;
import org.netbeans.modules.ruby.elements.IndexedVariable;
import org.netbeans.modules.ruby.elements.KeywordElement;
import org.netbeans.modules.ruby.elements.RubyElement;
import org.netbeans.modules.ruby.lexer.Call;
import org.netbeans.modules.ruby.lexer.LexUtilities;
import org.netbeans.modules.ruby.lexer.RubyStringTokenId;
import org.netbeans.modules.ruby.lexer.RubyTokenId;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import static org.netbeans.modules.ruby.RubyUtils.*;
/**
* Code completion handler for Ruby.
*
* Bug: I add lists of fields etc. But if these -overlap- the current line,
* I throw them away. The problem is that there may be other references
* to the field that I should -not- throw away, elsewhere!
* @todo Ensure that I prefer assignment over reference such that javadoc is
* more likely to be there!
*
*
* @todo Handle this case: {@code class HTTPBadResponse < StandardError; end}
* @todo Code completion should automatically suggest "initialize()" for def completion! (if I'm in a class)
* @todo It would be nice if you select a method that takes a block, such as Array.each, if we could
* insert a { ^ } suffix
* @todo Use lexical tokens to avoid attempting code completion within comments,
* literal strings and regexps
* @todo Percent-completion doesn't work if you at this to the end of the
* document: x = % and try to complete.
* @todo Handle more completion scenarios: Classes (no keywords) after "class Foo <",
* classes after "::", parameter completion (!!!), .new() completion (initialize), etc.
* @todo Make sure completion works during a "::"
* @todo I need to move the smart-determination from just checking in=Object/Class/Module
* to the code which computes matches, since we have for example ObjectMixin in pretty printer
* which adds mixin methods to Object.
* @todo Handle Rails methods that deal with hashes:
* - Try figuring out whether the method should take parameters by looking for examples;
* lines that start with the method name and looks like it might have arguments
* - Try to figure out what the different parameters are if there are hashes
* - <tt>: looks like a parameter, e.g. "<tt>:filename</tt>"
* and to see which parameter it might correspond to, see the
* label; see if any of the parameter names are listed there (possibly in the args list)
* A fallback is to look for args that look like they may be hashes, e.g.
* def(foo1, foo2, foo3={}) - the third one is obviously a hash
* @todo Make code completion when we're in a parameter list include the parameters as well!
* @todo For .rjs files, insert an object named "page" of type
* ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods
* (#105088)
* @todo For .builder files, insert an object named "xml" of type
* Builder::XmlMarkup
* @todo For .rhtml/.html.erb files, insert fields etc. as documented in actionpack's lib/action_view/base.rb
* (#105095)
* @todo For test files in Rails, get testing context (#105043). In particular, actionpack's
* ActionController::Assertions needs to be pulled in. This happens in action_controller/assertions.rb.
* @todo Require-completion should handle ruby gems; it should provide the "preferred" (entry-point) files for
* all the ruby gems, and it should hide all the files that are inside the gem
* @todo Rakefiles files should inherit Rakefile context
* @todo See http://blog.diegodoval.com/2007/09/ruby_on_os_x_some_useful_links.html
* @todo Documentation completion in a rdoc should preview that rdoc section
* @todo Make a dedicated completion item which I return on documentation completion if I want to
* complete the CURRENT element; it basically just wraps the desired comment so we can pull it
* out in the document() method
* @todo Provide code completion for "3|" or "3 |" - show available overloaded operators! This
* shouldn't just apply to numbers - any class you've overridden
* @todo Digest http://blogs.sun.com/coolstuff/entry/using_java_classes_in_jruby
* to fix require'java' etc.
* @todo http://www.innovationontherun.com/scraping-dynamic-websites-using-jruby-and-htmlunit/
* Idea: Use a quicktip to require all the jars in the project?
* @todo The "h" method in <%= %> doesn't show up in RHTML files... where is it?
* @todo Completion AFTER a method which takes a block (optional or required) should offer
* { } and do/end !!
* @author Tor Norbye
*/
public class RubyCodeCompleter implements CodeCompletionHandler {
// Another good logical parameter would be SINGLE_WHITESPACE which would
// insert a whitespace separator IF NEEDED
/** Live code template parameter: require the given file, if not already done so */
private static final String KEY_REQUIRE = "require"; // NOI18N
/** Live code template parameter: find a name in scope that is known to be of the given type */
private static final String KEY_INSTANCEOF = "instanceof"; // NOI18N
/** Live code template parameter: compute an unused local variable name */
private static final String ATTR_UNUSEDLOCAL = "unusedlocal"; // NOI18N
/** Live code template parameter: pipe variable, since | is a bit mishandled in the UI for editing abbrevs */
private static final String KEY_PIPE = "pipe"; // NOI18N
/** Live code template parameter: compute the method name */
private static final String KEY_METHOD = "method"; // NOI18N
/** Live code template parameter: compute the method signature */
private static final String KEY_METHOD_FQN = "methodfqn"; // NOI18N
/** Live code template parameter: compute the class name (not including the module prefix) */
private static final String KEY_CLASS = "class"; // NOI18N
/** Live code template parameter: compute the class fully qualified name */
private static final String KEY_CLASS_FQN = "classfqn"; // NOI18N
/** Live code template parameter: compute the superclass of the current class */
private static final String KEY_SUPERCLASS = "superclass"; // NOI18N
/** Live code template parameter: compute the filename (not including the path) of the file */
private static final String KEY_FILE = "file"; // NOI18N
/** Live code template parameter: compute the full path of the source directory */
private static final String KEY_PATH = "path"; // NOI18N
/** Default name values for ATTR_UNUSEDLOCAL and friends */
private static final String ATTR_DEFAULTS = "defaults"; // NOI18N
private static final Set<String> selectionTemplates = new HashSet<String>();
static {
selectionTemplates.add("begin"); // NOI18N
selectionTemplates.add("do"); // NOI18N
selectionTemplates.add("doc"); // NOI18N
//selectionTemplates.add("dop"); // NOI18N
selectionTemplates.add("if"); // NOI18N
selectionTemplates.add("ife"); // NOI18N
}
private boolean caseSensitive;
private int anchor;
public RubyCodeCompleter() {
}
static boolean startsWith(String theString, String prefix, boolean caseSensitive) {
if (prefix.length() == 0) {
return true;
}
return caseSensitive ? theString.startsWith(prefix)
: theString.toLowerCase().startsWith(prefix.toLowerCase());
}
private boolean startsWith(String theString, String prefix) {
return RubyCodeCompleter.startsWith(theString, prefix, caseSensitive);
}
/**
* Compute an appropriate prefix to use for code completion.
* In Strings, we want to return the -whole- string if you're in a
* require-statement string, otherwise we want to return simply "" or the previous "\"
* for quoted strings, and ditto for regular expressions.
* For non-string contexts, just return null to let the default identifier-computation
* kick in.
*/
@SuppressWarnings("unchecked")
public String getPrefix(ParserResult info, int lexOffset, boolean upToOffset) {
try {
BaseDocument doc = RubyUtils.getDocument(info);
if (doc == null) {
return null;
}
TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
doc.readLock(); // Read-lock due to token hierarchy use
try {
int requireStart = LexUtilities.getRequireStringOffset(lexOffset, th);
if (requireStart != -1) {
// XXX todo - do upToOffset
return doc.getText(requireStart, lexOffset - requireStart);
}
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
if (ts == null) {
return null;
}
ts.move(lexOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
if (ts.offset() == lexOffset) {
// We're looking at the offset to the RIGHT of the caret
// and here I care about what's on the left
ts.movePrevious();
}
Token<? extends RubyTokenId> token = ts.token();
if (token != null) {
TokenId id = token.id();
// We're within a String that has embedded Ruby. Drop into the
// embedded language and see if we're within a literal string there.
if (id == RubyTokenId.EMBEDDED_RUBY) {
ts = (TokenSequence) ts.embedded();
assert ts != null;
ts.move(lexOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
token = ts.token();
id = token.id();
}
String tokenText = token.text().toString();
if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
((id == RubyTokenId.ERROR) && tokenText.equals("%"))) {
int currOffset = ts.offset();
// Percent completion
if ((currOffset == (lexOffset - 1)) && (tokenText.length() > 0) &&
(tokenText.charAt(0) == '%')) {
return "%";
}
}
}
int doubleQuotedOffset = LexUtilities.getDoubleQuotedStringOffset(lexOffset, th);
if (doubleQuotedOffset != -1) {
// Tokenize the string and offer the current token portion as the text
if (doubleQuotedOffset == lexOffset) {
return "";
} else if (doubleQuotedOffset < lexOffset) {
String text = doc.getText(doubleQuotedOffset, lexOffset - doubleQuotedOffset);
TokenHierarchy hi =
TokenHierarchy.create(text, RubyStringTokenId.languageDouble());
TokenSequence seq = hi.tokenSequence();
seq.move(lexOffset - doubleQuotedOffset);
if (!seq.moveNext() && !seq.movePrevious()) {
return "";
}
TokenId id = seq.token().id();
String s = seq.token().text().toString();
if ((id == RubyStringTokenId.STRING_ESCAPE) ||
(id == RubyStringTokenId.STRING_INVALID)) {
return s;
} else if (s.startsWith("\\")) {
return s;
} else {
return "";
}
} else {
// The String offset is greater than the caret position.
// This means that we're inside the string-begin section,
// for example here: %q|(
// In this case, report no prefix
return "";
}
}
int singleQuotedOffset = LexUtilities.getSingleQuotedStringOffset(lexOffset, th);
if (singleQuotedOffset != -1) {
if (singleQuotedOffset == lexOffset) {
return "";
} else if (singleQuotedOffset < lexOffset) {
String text = doc.getText(singleQuotedOffset, lexOffset - singleQuotedOffset);
TokenHierarchy hi =
TokenHierarchy.create(text, RubyStringTokenId.languageSingle());
TokenSequence seq = hi.tokenSequence();
seq.move(lexOffset - singleQuotedOffset);
if (!seq.moveNext() && !seq.movePrevious()) {
return "";
}
TokenId id = seq.token().id();
String s = seq.token().text().toString();
if ((id == RubyStringTokenId.STRING_ESCAPE) ||
(id == RubyStringTokenId.STRING_INVALID)) {
return s;
} else if (s.startsWith("\\")) {
return s;
} else {
return "";
}
} else {
// The String offset is greater than the caret position.
// This means that we're inside the string-begin section,
// for example here: %q|(
// In this case, report no prefix
return "";
}
}
// Regular expression
int regexpOffset = LexUtilities.getRegexpOffset(lexOffset, th);
if ((regexpOffset != -1) && (regexpOffset <= lexOffset)) {
// This is not right... I need to actually parse the regexp
// (I should use my Regexp lexer tokens which will be embedded here)
// such that escaping sequences (/\\\\\/) will work right, or
// character classes (/[foo\]). In both cases the \ may not mean escape.
String tokenText = token.text().toString();
int index = lexOffset - ts.offset();
if ((index > 0) && (index <= tokenText.length()) &&
(tokenText.charAt(index - 1) == '\\')) {
return "\\";
} else {
// No prefix for regexps unless it's \
return "";
}
//return doc.getText(regexpOffset, offset-regexpOffset);
}
int lineBegin = Utilities.getRowStart(doc, lexOffset);
if (lineBegin != -1) {
int lineEnd = Utilities.getRowEnd(doc, lexOffset);
String line = doc.getText(lineBegin, lineEnd - lineBegin);
int lineOffset = lexOffset - lineBegin;
int start = lineOffset;
if (lineOffset > 0) {
for (int i = lineOffset - 1; i >= 0; i--) {
char c = line.charAt(i);
if (!RubyUtils.isIdentifierChar(c)) {
break;
} else {
start = i;
}
}
}
// Find identifier end
String prefix;
if (upToOffset) {
prefix = line.substring(start, lineOffset);
} else {
if (lineOffset == line.length()) {
prefix = line.substring(start);
} else {
int n = line.length();
int end = lineOffset;
for (int j = lineOffset; j < n; j++) {
char d = line.charAt(j);
// Try to accept Foo::Bar as well
if (!RubyUtils.isStrictIdentifierChar(d)) {
break;
} else {
end = j + 1;
}
}
prefix = line.substring(start, end);
}
}
if (prefix.length() > 0) {
if (prefix.endsWith("::")) {
return "";
}
if (prefix.endsWith(":") && prefix.length() > 1) {
return null;
}
// Strip out LHS if it's a qualified method, e.g. Benchmark::measure -> measure
int q = prefix.lastIndexOf("::");
if (q != -1) {
prefix = prefix.substring(q + 2);
}
// The identifier chars identified by RubyLanguage are a bit too permissive;
// they include things like "=", "!" and even "&" such that double-clicks will
// pick up the whole "token" the user is after. But "=" is only allowed at the
// end of identifiers for example.
if (prefix.length() == 1) {
char c = prefix.charAt(0);
if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$' || c == ':')) {
return null;
}
} else {
for (int i = prefix.length() - 2; i >= 0; i--) { // -2: the last position (-1) can legally be =, ! or ?
char c = prefix.charAt(i);
if (i == 0 && c == ':') {
// : is okay at the begining of prefixes
} else if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$')) {
prefix = prefix.substring(i + 1);
break;
}
}
}
return prefix;
}
}
} finally {
doc.readUnlock();
}
// Else: normal identifier: just return null and let the machinery do the rest
} catch (BadLocationException ble) {
// do nothing - see #154991;
}
// Default behavior
return null;
}
/** Determine if we're trying to complete the name for a "def" (in which case
* we'd show the inherited methods).
* This needs to be enhanced to handle "Foo." prefixes, e.g. def self.foo
*/
private boolean completeDefOrInclude(List<CompletionProposal> proposals, CompletionRequest request, String fqn) {
RubyIndex index = request.index;
String prefix = request.prefix;
int lexOffset = request.lexOffset;
TokenHierarchy<Document> th = request.th;
QuerySupport.Kind kind = request.kind;
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
if ((index != null) && (ts != null)) {
ts.move(lexOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return false;
}
if (ts.offset() == lexOffset) {
// We're looking at the offset to the RIGHT of the caret
// position, which could be whitespace, e.g.
// "def fo| " <-- looking at the whitespace
ts.movePrevious();
}
Token<?extends RubyTokenId> token = ts.token();
if (token != null) {
TokenId id = token.id();
// See if we're in the identifier - "foo" in "def foo"
// I could also be a keyword in case the prefix happens to currently
// match a keyword, such as "next"
if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CONSTANT) || id.primaryCategory().equals("keyword")) {
if (!ts.movePrevious()) {
return false;
}
token = ts.token();
id = token.id();
}
// If we're not in the identifier we need to be in the whitespace after "def"
if (id != RubyTokenId.WHITESPACE) {
// Do something about http://www.netbeans.org/issues/show_bug.cgi?id=100452 here
// In addition to checking for whitespace I should look for "Foo." here
return false;
}
// There may be more than one whitespace; skip them
while (ts.movePrevious()) {
token = ts.token();
if (token.id() != RubyTokenId.WHITESPACE) {
break;
}
}
if (token.id() == RubyTokenId.DEF) {
Set<IndexedMethod> methods = index.getInheritedMethods(fqn, prefix, kind);
for (IndexedMethod method : methods) {
// Hmmm, is this necessary? Filtering should happen in the getInheritedMEthods call
if ((prefix.length() > 0) && !method.getName().startsWith(prefix)) {
continue;
}
// For def completion, skip local methods, only include superclass and included
if ((fqn != null) && fqn.equals(method.getClz())) {
continue;
}
if (method.isNoDoc()) {
continue;
}
// If a method is an "initialize" method I should do something special so that
// it shows up as a "constructor" (in a new() statement) but not as a directly
// callable initialize method (it should already be culled because it's private)
MethodItem item = new MethodItem(method, anchor, request);
// Exact matches
item.setSmart(method.isSmart());
proposals.add(item);
}
return true;
} else if (token.id() == RubyTokenId.IDENTIFIER && "include".equals(token.text().toString())) {
// Module completion
Set<IndexedClass> classes = index.getClasses(prefix, kind, false, true, false);
for (IndexedClass clz : classes) {
if (clz.isNoDoc()) {
continue;
}
ClassItem item = new ClassItem(clz, anchor, request);
item.setSmart(true);
proposals.add(item);
}
return true;
}
}
}
return false;
}
private void completeGlobals(List<CompletionProposal> proposals, CompletionRequest request, boolean showSymbols) {
RubyIndex index = request.index;
String prefix = request.prefix;
QuerySupport.Kind kind = request.kind;
Set<IndexedVariable> globals = index.getGlobals(prefix, kind);
for (IndexedVariable global : globals) {
RubyCompletionItem item = new RubyCompletionItem(global, anchor, request);
item.setSmart(true);
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
}
private boolean addParameters(List<CompletionProposal> proposals, CompletionRequest request) {
IndexedMethod[] methodHolder = new IndexedMethod[1];
@SuppressWarnings("unchecked")
Set<IndexedMethod>[] alternatesHolder = new Set[1];
int[] paramIndexHolder = new int[1];
int[] anchorOffsetHolder = new int[1];
ParserResult info = request.parserResult;
int lexOffset = request.lexOffset;
int astOffset = request.astOffset;
if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
methodHolder, paramIndexHolder, anchorOffsetHolder, alternatesHolder, request.kind)) {
return false;
}
IndexedMethod targetMethod = methodHolder[0];
int index = paramIndexHolder[0];
CallItem callItem = new CallItem(targetMethod, index, anchor, request);
proposals.add(callItem);
// Also show other documented, not nodoc'ed items (except for those
// with identical signatures, such as overrides of the same method)
if (alternatesHolder[0] != null) {
Set<String> signatures = new HashSet<String>();
signatures.add(targetMethod.getSignature().substring(targetMethod.getSignature().indexOf('#')+1));
for (IndexedMethod m : alternatesHolder[0]) {
if (m != targetMethod && m.isDocumented() && !m.isNoDoc()) {
String sig = m.getSignature().substring(m.getSignature().indexOf('#')+1);
if (!signatures.contains(sig)) {
CallItem item = new CallItem(m, index, anchor, request);
proposals.add(item);
signatures.add(sig);
}
}
}
}
List<String> params = targetMethod.getParameters();
if (params == null || params.isEmpty()) {
return false;
}
if (params.size() <= index) {
// Just use the last parameter in these cases
// See for example the TableDefinition.binary dynamic method where
// you can add a number of parameter names and the options parameter
// is always the last one
index = params.size()-1;
}
boolean isLastArg = index < params.size()-1;
String attrs = targetMethod.getEncodedAttributes();
if (attrs != null && attrs.length() > 0) {
int offset = -1;
for (int i = 0; i < 3; i++) {
offset = attrs.indexOf(';', offset+1);
if (offset == -1) {
break;
}
}
if (offset == -1) {
Node root = null;
if (info != null) {
root = AstUtilities.getRoot(info);
}
IndexedElement match = findDocumentationEntry(root, targetMethod);
if (match == targetMethod || !(match instanceof IndexedMethod)) {
return false;
}
targetMethod = (IndexedMethod)match;
attrs = targetMethod.getEncodedAttributes();
if (attrs != null && attrs.length() > 0) {
offset = -1;
for (int i = 0; i < 3; i++) {
offset = attrs.indexOf(';', offset+1);
if (offset == -1) {
break;
}
}
}
}
String currentName = params.get(index);
if (currentName.startsWith("*")) {
// * and & are part of the sig
currentName = currentName.substring(1);
} else if (currentName.startsWith("&")) {
currentName = currentName.substring(1);
}
if (offset != -1) {
// Pick apart
attrs = attrs.substring(offset+1);
if (attrs.length() == 0) {
return false;
}
String[] argEntries = attrs.split(",");
for (String entry : argEntries) {
int parenIndex = entry.indexOf('(');
assert parenIndex != -1 : attrs;
String name = entry.substring(0, parenIndex);
if (currentName.equals(name)) {
// Found a special parameter desc entry for this
// parameter - decode it and create completion items
// Decode
int endIndex = entry.indexOf(')', parenIndex);
assert endIndex != -1;
String data = entry.substring(parenIndex+1, endIndex);
if (data.length() > 0 && data.charAt(0) == '-') {
// It's a plain item (e.g. not a hash etc) where
// we have some logical types to complete
if ("-table".equals(data)) {
completeDbTables(proposals, targetMethod, request, isLastArg);
// Not exiting - I may have other entries here too
} else if ("-column".equals(data)) {
completeDbColumns(proposals, targetMethod, request, isLastArg);
// Not exiting - I may have other entries here too
} else if ("-model".equals(data)) {
completeModels(proposals, targetMethod, request, isLastArg);
}
} else if (data.startsWith("=>")) {
// It's a hash; show the given keys
// TODO: Determine if the caret is in the
// value part, and if so, show the values instead
// Uhm... what about fields and such?
completeHash(proposals, request, targetMethod, data, isLastArg);
// Not exiting - I may have a non-hash entry here too!
} else {
// Just show a fixed set of values
completeFixed(proposals, request, targetMethod, data, isLastArg);
// Not exiting - I may have other entries here too
}
}
}
}
}
return true;
}
// /** Handle insertion of :action, :controller, etc. even for methods without
// * actual method signatures. Operate at the lexical level.
// */
// private void handleRailsKeys(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
// TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
// if (ts == null) {
// return;
// }
// boolean inValue = false;
// ts.move(anchor);
// String line = null;
// while (ts.movePrevious()) {
// final Token token = ts.token();
// if (token.id() == RubyTokenId.WHITESPACE) {
// continue;
// } else if (token.id() == RubyTokenId.NONUNARY_OP &&
// (token.text().toString().equals("=>"))) { // NOI18N
// inValue = true;
// // TODO - continue on to find out what the key is
// try {
// BaseDocument doc = request.doc;
// int lineStart = Utilities.getRowStart(doc, ts.offset());
// line = doc.getText(lineStart, ts.offset()-lineStart).trim();
// } catch (BadLocationException ble) {
// Exceptions.printStackTrace(ble);
// return;
// }
// } else {
// break;
// }
// }
//
// if (inValue) {
// if (line.endsWith(":action")) {
// // TODO
// } else if (line.endsWith(":controller")) {
// // Dynamically produce controllers
// List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
// String prefix = request.prefix;
// for (String n : controllers) {
// n = "'" + n + "'";
// if (startsWith(n, prefix)) {
// String insert = n;
// if (!isLastArg) {
// insert = insert + ", ";
// }
// ParameterItem item = new ParameterItem(target, n, null, insert, anchor, request);
// item.setSymbol(true);
// item.setSmart(true);
// proposals.add(item);
// }
// }
// } else if (line.endsWith(":partial")) {
// // TODO
// }
// }
// }
private boolean completeHash(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
assert data.startsWith("=>");
data = data.substring(2);
String prefix = request.prefix;
// Determine if we're in the key part or the value part when completing
boolean inValue = false;
TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
if (ts == null) {
return false;
}
ts.move(anchor);
String line = null;
while (ts.movePrevious()) {
final Token token = ts.token();
if (token.id() == RubyTokenId.WHITESPACE) {
continue;
} else if (token.id() == RubyTokenId.NONUNARY_OP &&
(token.text().toString().equals("=>"))) { // NOI18N
inValue = true;
// TODO - continue on to find out what the key is
try {
BaseDocument doc = request.doc;
int lineStart = Utilities.getRowStart(doc, ts.offset());
line = doc.getText(lineStart, ts.offset()-lineStart).trim();
} catch (BadLocationException ble) {
return false;
}
} else {
break;
}
}
List<String> suggestions = new ArrayList<String>();
String key = null;
String[] values = data.split("\\|");
if (inValue) {
// Find the key and see if we have a type to offer for it
for (String value : values) {
int typeIndex = value.indexOf(':');
if (typeIndex != -1) {
String name = value.substring(0, typeIndex);
if (line.endsWith(name)) {
key = name;
// Score - it appears we're using the
// key for this item
String type = value.substring(typeIndex+1);
if ("nil".equals(type)) { // NOI18N
suggestions.add("nil"); // NOI18N
} else if ("bool".equals(type)) { // NOI18N
suggestions.add("true"); // NOI18N
suggestions.add("false"); // NOI18N
} else if ("submitmethod".equals(type)) { // NOI18N
suggestions.add("post"); // NOI18N
suggestions.add("get"); // NOI18N
} else if ("validationactive".equals(type)) { // NOI18N
suggestions.add(":save"); // NOI18N
suggestions.add(":create"); // NOI18N
suggestions.add(":update"); // NOI18N
} else if ("string".equals(type)) { // NOI18N
suggestions.add("\""); // NOI18N
} else if ("hash".equals(type)) { // NOI18N
suggestions.add("{"); // NOI18N
} else if ("controller".equals(type)) {
// Dynamically produce controllers
List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
for (String n : controllers) {
suggestions.add("'" + n + "'");
}
} else if ("action".equals(type)) {
// Dynamically produce actions
// This would need to be scoped by the current
// context - look at the hash, find the specified
// controller and limit it to that
List<String> actions = getActionNames(request);
for (String n : actions) {
suggestions.add("'" + n + "'");
}
} else if ("status".equals(type)) {
return RubyHttpStatusCodeCompleter.complete(proposals, request, anchor, caseSensitive, target);
}
}
}
}
} else {
for (String value : values) {
int typeIndex = value.indexOf(':');
if (typeIndex != -1) {
value = value.substring(0, typeIndex);
}
value = ":" + value + " => ";
suggestions.add(value);
}
}
// I've gotta clean up the colon handling in complete()
// I originally stripped ":" to make direct (INameNode)getName()
// comparisons on symbols work directly but it's becoming a liability now
String colonPrefix = ":" + prefix;
for (String suggestion : suggestions) {
if (startsWith(suggestion, prefix) || startsWith(suggestion, colonPrefix)) {
String insert = suggestion;
String desc = null;
if (inValue) {
if (!isLastArg) {
insert = insert + ", ";
}
if (key != null) {
desc = ":" + key + " = " + suggestion;
}
}
ParameterItem item = new ParameterItem(target, suggestion, desc, insert, anchor, request);
item.setSymbol(true);
item.setSmart(true);
proposals.add(item);
}
}
return true;
}
/** Get the actions for the given file. If the file is a controller, list the actions within it,
* otherwise, if the file is a view, list the actions for the corresponding controller.
*
* @param fileInProject the file we're looking up
* @return A List of action names
*/
private List<String> getActionNames(CompletionRequest request) {
FileObject file = request.fileObject;
FileObject controllerFile = null;
if (file.getNameExt().endsWith("_controller.rb")) {
controllerFile = file;
} else {
controllerFile = RubyUtils.getRailsControllerFor(file);
}
// TODO - check for other :controller-> settings in the hashmap and if present, use it
if (controllerFile == null) {
return Collections.emptyList();
}
String controllerClass = RubyUtils.getControllerClass(controllerFile);
if (controllerClass != null) {
String prefix = request.prefix;
Set<IndexedMethod> methods = request.index.getMethods(prefix, controllerClass, request.kind);
List<String> actions = new ArrayList<String>();
for (IndexedMethod method : methods) {
if (method.isPublic() && method.getArgs() == null || method.getArgs().length == 0) {
actions.add(method.getName());
}
}
return actions;
}
// TODO - pull out the methods or this class
return Collections.emptyList();
}
private void completeFixed(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
String[] values = data.split("\\|");
String prefix = request.prefix;
// I originally stripped ":" to make direct (INameNode)getName()
// comparisons on symbols work directly but it's becoming a liability now
String colonPrefix = ":" + prefix;
for (String value : values) {
if (startsWith(value, prefix) || startsWith(value, colonPrefix)) {
String insert = isLastArg ? value : (value + ", ");
ParameterItem item = new ParameterItem(target, value, null, insert, anchor, request);
item.setSymbol(true);
item.setSmart(true);
proposals.add(item);
}
}
}
private void completeDbTables(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
// Add in the eligible database tables found in this project
// Assumes this is a Rails project
String p = request.prefix;
String colonPrefix = p;
if (":".equals(p)) { // NOI18N
p = "";
} else {
colonPrefix = ":" + p; // NOI18N
}
Set<String> tables = request.index.getDatabaseTables(p, request.kind);
// I originally stripped ":" to make direct (INameNode)getName()
// comparisons on symbols work directly but it's becoming a liability now
String prefix = request.prefix;
for (String table : tables) {
// PENDING: Should I insert :tablename or 'tablename' or "tablename" ?
String tableName = ":" + table;
if (startsWith(tableName, prefix) || startsWith(tableName, colonPrefix)) {
String insert = isLastArg ? tableName : (tableName + ", ");
ParameterItem item = new ParameterItem(target, tableName, null, insert, anchor, request);
item.setSymbol(true);
item.setSmart(true);
proposals.add(item);
}
}
}
private void completeModels(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
Set<IndexedClass> clz = request.index.getSubClasses(request.prefix, RubyIndex.ACTIVE_RECORD_BASE, request.kind);
String prefix = request.prefix;
// I originally stripped ":" to make direct (INameNode)getName()
// comparisons on symbols work directly but it's becoming a liability now
String colonPrefix = ":" + prefix;
for (IndexedClass c : clz) {
String name = c.getName();
String symbol = ":"+RubyUtils.camelToUnderlinedName(name);
if (startsWith(symbol, prefix) || startsWith(symbol, colonPrefix)) {
String insert = isLastArg ? symbol : (symbol + ", ");
ParameterItem item = new ParameterItem(target, symbol, name, insert, anchor, request);
item.setSymbol(true);
item.setSmart(true);
proposals.add(item);
}
}
}
private void completeDbColumns(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
// Add in the eligible database tables found in this project
// Assumes this is a Rails project
// Set<String> tables = request.index.getDatabaseTables(request.prefix, request.kind);
// TODO
// for (String table : tables) {
// if (startsWith(table, prefix)) {
// SymbolHashItem item = new SymbolHashItem(target, ":" + table, null, anchor, request);
// item.setSymbol(true);
// proposals.add(item);
// }
// }
}
// TODO: Move to the top
public CodeCompletionResult complete(final CodeCompletionContext context) {
ParserResult ir = context.getParserResult();
int lexOffset = context.getCaretOffset();
String prefix = context.getPrefix();
QuerySupport.Kind kind = context.isPrefixMatch() ? QuerySupport.Kind.PREFIX : QuerySupport.Kind.EXACT;
QueryType queryType = context.getQueryType();
this.caseSensitive = context.isCaseSensitive();
final int astOffset = AstUtilities.getAstOffset(ir, lexOffset);
if (astOffset == -1) {
return null;
}
// Avoid all those annoying null checks
if (prefix == null) {
prefix = "";
}
List<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
DefaultCompletionResult completionResult = new DefaultCompletionResult(proposals, false);
anchor = lexOffset - prefix.length();
final RubyIndex index = RubyIndex.get(ir);
final Document document = RubyUtils.getDocument(ir);
if (document == null) {
return CodeCompletionResult.NONE;
}
// TODO - move to LexUtilities now that this applies to the lexing offset?
lexOffset = AstUtilities.boundCaretOffset(ir, lexOffset);
// Discover whether we're in a require statement, and if so, use special completion
final TokenHierarchy<Document> th = TokenHierarchy.get(document);
final BaseDocument doc = (BaseDocument)document;
final FileObject fileObject = RubyUtils.getFileObject(ir);
boolean showLower = true;
boolean showUpper = true;
boolean showSymbols = false;
char first = 0;
doc.readLock(); // Read-lock due to Token hierarchy use
try {
if (prefix.length() > 0) {
first = prefix.charAt(0);
// Foo::bar --> first char is "b" - we're looking for a method
int qualifier = prefix.lastIndexOf("::");
if ((qualifier != -1) && (qualifier < (prefix.length() - 2))) {
first = prefix.charAt(qualifier + 2);
}
showLower = Character.isLowerCase(first);
// showLower is not necessarily !showUpper - prefix can be ":foo" for example
showUpper = Character.isUpperCase(first);
if (first == ':') {
showSymbols = true;
if (prefix.length() > 1) {
char second = prefix.charAt(1);
prefix = prefix.substring(1);
showLower = Character.isLowerCase(second);
showUpper = Character.isUpperCase(second);
}
}
}
// Carry completion context around since this logic is split across lots of methods
// and I don't want to pass dozens of parameters from method to method; just pass
// a request context with supporting parserResult needed by the various completion helpers.
CompletionRequest request = new CompletionRequest(
completionResult, th, ir, lexOffset, astOffset,
doc, prefix, index, kind, queryType, fileObject);
// See if we're inside a string or regular expression and if so,
// do completions applicable to strings - require-completion,
// escape codes for quoted strings and regular expressions, etc.
if (RubyStringCompleter.complete(proposals, request, anchor, caseSensitive)) {
completionResult.setFilterable(false);
return completionResult;
}
Call call = Call.getCallType(doc, th, lexOffset);
// Fields
// This is a bit stupid at the moment, not looking at the current typing context etc.
Node root = AstUtilities.getRoot(ir);
if (root == null) {
RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
return completionResult;
}
// Compute the bounds of the line that the caret is on, and suppress nodes overlapping the line.
// This will hide not only paritally typed identifiers, but surrounding contents like the current class and module
final int astLineBegin;
final int astLineEnd;
try {
astLineBegin = AstUtilities.getAstOffset(ir, Utilities.getRowStart(doc, lexOffset));
astLineEnd = AstUtilities.getAstOffset(ir, Utilities.getRowEnd(doc, lexOffset));
} catch (BadLocationException ble) {
return CodeCompletionResult.NONE;
}
final AstPath path = new AstPath(root, astOffset);
request.path = path;
Map<String, Node> variables = new HashMap<String, Node>();
Map<String, Node> fields = new HashMap<String, Node>();
Map<String, Node> globals = new HashMap<String, Node>();
Map<String, Node> constants = new HashMap<String, Node>();
final Node closest = path.leaf();
request.target = closest;
// Don't try to add local vars, globals etc. as part of calls or class fqns
if (call.getLhs() == null) {
if (showLower && (closest != null)) {
List<Node> applicableBlocks = AstUtilities.getApplicableBlocks(path, false);
for (Node block : applicableBlocks) {
addDynamic(block, variables);
}
Node method = AstUtilities.findLocalScope(closest, path);
List<Node> list2 = method.childNodes();
for (Node child : list2) {
if (child.isInvisible()) {
continue;
}
addLocals(child, variables);
}
}
boolean inAttrCall = isInAttr(closest, path);
if (prefix.length() == 0 || first == '@' || showSymbols || inAttrCall) {
String fqn = AstUtilities.getFqnName(path);
if ((fqn == null) || (fqn.length() == 0)) {
String fileName = RubyUtils.getFileObject(context.getParserResult()).getName();
if (fileName.endsWith("_spec")) { //NOI18N
// use the virtual class created for the spec file
fqn = RubyUtils.underlinedNameToCamel(fileName);
} else {
fqn = "Object"; // NOI18N
}
}
// TODO - if fqn has multiple ::'s, try various combinations? or is
// add inherited already doing that?
Set<IndexedField> f;
if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
f = new HashSet<IndexedField>();
addActionViewFields(f, fileObject, index, prefix, kind);
} else {
//strip out ':' when querying fields for cases like 'attr_reader :^'
if (inAttrCall && first == ':' && prefix.length() == 1) {
f = index.getInheritedFields(fqn, "", kind, false);
} else {
f = index.getInheritedFields(fqn, prefix, kind, false);
}
}
for (IndexedField field : f) {
String insertPrefix = inAttrCall ? ":" : null;
FieldItem item = new FieldItem(field, anchor, request, insertPrefix);
item.setSmart(field.isSmart());
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
// return just the fields for attr_
if (inAttrCall) {
return completionResult;
}
}
// $ is neither upper nor lower
if ((prefix.length() == 0) || (first == '$') || showSymbols) {
if (prefix.startsWith("$") || showSymbols) {
completeGlobals(proposals, request, showSymbols);
// Dollar variables too
RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
if (!showSymbols) {
return completionResult;
}
}
}
}
// TODO: should only include fields etc. down to caret location??? Decide. (Depends on language semantics. Can I have forward referemces?
if (call.isConstantExpected()) {
RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
RubyType type = call.getType();
if (type.isKnown() && type.isSingleton()) {
RubyMethodCompleter.complete(proposals, request, type.first(), call, anchor, caseSensitive);
}
return completionResult;
}
// If we're in a call, add in some parserResult and help for the code completion call
boolean inCall = addParameters(proposals, request);
// Code completion from the index.
if (index != null) {
if (showLower || showSymbols) {
String fqn = AstUtilities.getFqnName(path);
if ((fqn == null) || (fqn.length() == 0)) {
fqn = "Object"; // NOI18N
}
if ((fqn != null) && queryType == QueryType.COMPLETION && // doesn't apply to (or work with) documentation/tooltip help
completeDefOrInclude(proposals, request, fqn)) {
return completionResult;
}
if ((fqn != null) &&
RubyMethodCompleter.complete(proposals, request, fqn, call, anchor, caseSensitive)) {
return completionResult;
}
// Only call local and inherited methods if we don't have an LHS, such as Foo::
if (call.getLhs() == null) {
// TODO - pull this into a completeInheritedMethod call
// Complete inherited methods or local methods only (plus keywords) since there
// is no receiver so it must be a local or inherited method call
Set<IndexedMethod> inheritedMethods =
index.getInheritedMethods(fqn, prefix, kind);
inheritedMethods = RubyDynamicFindersCompleter.proposeDynamicMethods(inheritedMethods, proposals, request, anchor);
// Handle action view completion for RHTML and Markaby files
if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
addActionViewMethods(inheritedMethods, fileObject, index, prefix, kind);
} else if (fileObject.getName().endsWith("_spec")) { // NOI18N
// RSpec
/* My spec object had the following extras methods over a plain Object:
x = self.class.methods
x.each {|c|
puts c
}
> args_and_options
> context
> copy_instance_variables_from
> describe
> gem
> metaclass
> require
> require_gem
> respond_to
> should
> should_not
*/
String includes[] = {
// "describe" should be in Kernel already, from spec/runner/extensions/kernel.rb
"Spec::Matchers",
// This one shouldn't be necessary since there's a
// "class Object; include xxx::ObjectExpectations; end" in rspec's object.rb
"Spec::Expectations::ObjectExpectations",
"Spec::DSL::BehaviourEval::InstanceMethods"}; // NOI18N
for (String fqns : includes) {
Set<IndexedMethod> helper = index.getInheritedMethods(fqns, prefix, kind);
inheritedMethods.addAll(helper);
}
}
for (IndexedMethod method : inheritedMethods) {
// This should not be necessary - filtering happens in getInheritedMethods right?
if ((prefix.length() > 0) && !method.getName().startsWith(prefix)) {
continue;
}
if (method.isNoDoc()) {
continue;
}
// If a method is an "initialize" method I should do something special so that
// it shows up as a "constructor" (in a new() statement) but not as a directly
// callable initialize method (it should already be culled because it's private)
MethodItem item = new MethodItem(method, anchor, request);
item.setSmart(method.isSmart());
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
}
}
if (showUpper) {
if (queryType == QueryType.COMPLETION && // doesn't apply to (or work with) documentation/tooltip help
completeDefOrInclude(proposals, request, "")) {
return completionResult;
}
}
if ((showUpper && ((prefix != null && prefix.length() > 0) ||
(!call.isMethodExpected() && call.getLhs() != null && call.getLhs().length() > 0))) || (showSymbols && !inCall)) {
// TODO - allow method calls if you're already entered the first char!
RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
}
}
assert (kind == QuerySupport.Kind.PREFIX) || (kind == QuerySupport.Kind.CASE_INSENSITIVE_PREFIX) ||
(kind == QuerySupport.Kind.EXACT);
// TODO
// Remove fields and variables whose names are already taken, e.g. do a fields.removeAll(variables) etc.
for (String variable : variables.keySet()) {
if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
Node node = variables.get(variable);
if (!overlapsLine(node, astLineBegin, astLineEnd)) {
AstElement co = new AstNameElement(ir, node, variable,
ElementKind.VARIABLE);
RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
item.setSmart(true);
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
}
}
for (String field : fields.keySet()) {
if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(field)) ||
((kind != QuerySupport.Kind.EXACT) && startsWith(field, prefix))) {
Node node = fields.get(field);
if (overlapsLine(node, astLineBegin, astLineEnd)) {
continue;
}
Element co = new AstFieldElement(ir, node);
FieldItem item = new FieldItem(co, anchor, request);
item.setSmart(true);
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
}
// TODO - model globals and constants using different icons / etc.
for (String variable : globals.keySet()) {
// TODO - kind.EXACT
if (startsWith(variable, prefix) ||
(showSymbols && startsWith(variable.substring(1), prefix))) {
Node node = globals.get(variable);
if (overlapsLine(node, astLineBegin, astLineEnd)) {
continue;
}
AstElement co = new AstNameElement(ir, node, variable,
ElementKind.VARIABLE);
RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
item.setSmart(true);
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
}
// TODO - model globals and constants using different icons / etc.
for (String variable : constants.keySet()) {
if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
// Skip constants that are known to be classes
Node node = constants.get(variable);
if (overlapsLine(node, astLineBegin, astLineEnd)) {
continue;
}
// ComObject co;
// if (isClassName(variable)) {
// co = JRubyNode.create(target, null);
// if (co == null) {
// continue;
// }
// } else {
// co = new DefaultComVariable(variable, false, -1, -1);
// ((DefaultComVariable)co).setNode(target);
AstElement co = new AstNameElement(ir, node, variable,
ElementKind.VARIABLE);
RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
item.setSmart(true);
if (showSymbols) {
item.setSymbol(true);
}
proposals.add(item);
}
}
if (RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols)) {
return completionResult;
}
if (queryType == QueryType.DOCUMENTATION) {
proposals = filterDocumentation(proposals, root, doc, ir, astOffset, lexOffset, prefix, path,
index);
}
} finally {
doc.readUnlock();
}
return completionResult;
}
private boolean isInAttr(Node closest, AstPath path) {
if (closest != null) {
// first argument in attr_*
for (Node child : closest.childNodes()) {
if (AstUtilities.isAttr(child)) {
return true;
}
}
// others, e.g. attr_reader :foo, :ba^r
if (AstUtilities.isAttr(path.leafParent()) || AstUtilities.isAttr(path.leafGrandParent())) {
return true;
}
}
return false;
}
private void addActionViewMethods(Set<IndexedMethod> inheritedMethods, FileObject fileObject, RubyIndex index, String prefix,
QuerySupport.Kind kind) {
// RHTML and Markaby: Add in the helper methods etc. from the associated files
boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
if (isMarkaby) {
Set<IndexedMethod> actionView = index.getInheritedMethods("ActionView::Base", prefix, kind); // NOI18N
inheritedMethods.addAll(actionView);
}
if (isRhtmlFile(fileObject) || isMarkaby) {
// Hack - include controller and helper files as well
FileObject f = fileObject.getParent();
// name of the controller w/o the "controller" suffix
String controllerName = null;
// XXX Will this work for .mab files? Where do they go?
while (f != null && !f.getName().equals("views")) { // todo - make sure grandparent is app
String n = underlinedNameToCamel(f.getName());
if (controllerName == null) {
controllerName = n;
} else {
controllerName = n + "::" + controllerName;
}
f = f.getParent();
}
// // add in all methods from the associated helper and inherited helpers. this will
// add also ApplicationHelper, which is global
Set<String> helperNames = new HashSet<String>();
helperNames.add(helperName(controllerName));
for (IndexedClass superClass : index.getSuperClasses(controllerName(controllerName))) {
if ("ActionController::Base".equals(superClass.getFqn())) { //NOI18N
break;
}
helperNames.add(helperName(superClass.getFqn()));
}
for (String helper : helperNames) {
inheritedMethods.addAll(index.getInheritedMethods(helper, prefix, kind));
}
index.getSuperClasses(controllerName);
// TODO - pull in the fields (NOT THE METHODS) from the controller
//Set<IndexedMethod> controller = index.getInheritedMethods(controllerName+"Controller", prefix, kind);
//inheritedMethods.addAll(controller);
}
}
private void addActionViewFields(Set<IndexedField> inheritedFields, FileObject fileObject, RubyIndex index, String prefix,
QuerySupport.Kind kind) {
// RHTML and Markaby: Add in the helper methods etc. from the associated files
boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
if (isMarkaby) {
Set<IndexedField> actionView = index.getInheritedFields("ActionView::Base", prefix, kind, true); // NOI18N
inheritedFields.addAll(actionView);
}
if (RubyUtils.isRhtmlFile(fileObject) || isMarkaby) {
// Hack - include controller and helper files as well
FileObject f = fileObject.getParent();
String controllerName = null;
// XXX Will this work for .mab files? Where do they go?
while (f != null && !f.getName().equals("views")) { // NOI18N // todo - make sure grandparent is app
String n = RubyUtils.underlinedNameToCamel(f.getName());
if (controllerName == null) {
controllerName = n;
} else {
controllerName = n + "::" + controllerName; // NOI18N
}
f = f.getParent();
}
String fqn = controllerName+"Controller"; // NOI18N
Set<IndexedField> controllerFields = index.getInheritedFields(fqn, prefix, kind, true);
for (IndexedField field : controllerFields) {
if ("ActionController::Base".equals(field.getIn())) { // NOI18N
continue;
}
inheritedFields.add(field);
}
}
}
/** If we're doing documentation completion, try to drop the list down to a single alternative
* (since the framework will just use the first produced result), and in particular, the -best-
* alternative
*/
// TODO - pass in request object here!
private List<CompletionProposal> filterDocumentation(List<CompletionProposal> proposals,
Node root, BaseDocument doc, ParserResult parserResult, int astOffset, int lexOffset, String name,
AstPath path, RubyIndex index) {
// Look to see if this symbol is either a "class Foo" or a "def foo", and if we invoke
// completion on it, prefer this element provided it has documentation
List<CompletionProposal> candidates = new ArrayList<CompletionProposal>();
FileObject fo = RubyUtils.getFileObject(parserResult);
Map<IndexedElement, CompletionProposal> elementMap =
new HashMap<IndexedElement, CompletionProposal>();
Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
Set<IndexedClass> classes = new HashSet<IndexedClass>();
for (CompletionProposal proposal : proposals) {
RubyElement e = (RubyElement) proposal.getElement();
if (e instanceof IndexedElement) {
IndexedElement ie = (IndexedElement)e;
if (ie instanceof IndexedClass) {
classes.add((IndexedClass)ie);
elementMap.put(ie, proposal);
} else if (ie instanceof IndexedMethod) {
methods.add((IndexedMethod)ie);
elementMap.put(ie, proposal);
}
if (ie.getFileObject() == fo) {
// The class is in this file - if it has documentation, prefer it
candidates.add(proposal);
}
}
}
// Check the candidates to see if one of them is actually -defined-
// under the caret; e.g. if you have "class File" with documentation,
// and you ctrl-space on it, you always want to show THIS documentation
// for File, not the standard one defined elsewhere.
for (CompletionProposal candidate : candidates) {
// See if the candidate corresponds to the caret position
RubyElement re = (RubyElement) candidate.getElement();
if (!(re instanceof IndexedElement)) {
continue;
}
IndexedElement e = (IndexedElement)re;
String signature = e.getSignature();
Node node = AstUtilities.findBySignature(root, signature);
if (node != null) {
SourcePosition pos = node.getPosition();
int startPos = LexUtilities.getLexerOffset(parserResult, pos.getStartOffset());
try {
int lineBegin = AstUtilities.getAstOffset(parserResult, Utilities.getRowFirstNonWhite(doc, startPos));
int lineEnd = AstUtilities.getAstOffset(parserResult, Utilities.getRowEnd(doc, startPos));
if ((astOffset >= lineBegin) && (astOffset <= lineEnd)) {
// Look for documentation
List<String> rdoc = AstUtilities.gatherDocumentation(parserResult.getSnapshot(), node);
if (rdoc != null && !rdoc.isEmpty()) {
return Collections.singletonList(candidate);
}
}
} catch (BadLocationException ble) {
// The parse information is too old - the document has shrunk. Do nothing, the
// AST nodes are pointing into the old contents.
}
}
}
// Try to pick the best match among many documentation entries: Heuristic time.
// Similar to heuristics used for Go To Declaration: Prefer long documentation,
// prefer documentation related to the require-statements in this file, etc.
IndexedElement candidate = null;
if (!classes.isEmpty()) {
RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(parserResult, null, path, index, path.leaf());
candidate = cdf.findBestElementMatch(classes);
} else if (!methods.isEmpty()) {
RubyDeclarationFinder finder = new RubyDeclarationFinder();
candidate = finder.findBestMethodMatch(name, methods, doc, astOffset, lexOffset, path,
path.leaf(), index);
}
if (candidate != null) {
CompletionProposal proposal = elementMap.get(candidate);
if (proposal != null) {
return Collections.singletonList(proposal);
}
}
return proposals;
}
// private boolean isClassName(String s) {
// // Initial capital letter, second letter is not
// if (s.length() == 1) {
// return Character.isUpperCase(s.charAt(0));
// }
//
// if (Character.isLowerCase(s.charAt(0))) {
// return false;
// }
//
// return Character.isLowerCase(s.charAt(1));
// }
private boolean overlapsLine(Node node, int lineBegin, int lineEnd) {
SourcePosition pos = node.getPosition();
//return (((pos.getStartOffset() <= lineEnd) && (pos.getEndOffset() >= lineBegin)));
// Don't look to see if the line is within the target. See if the target is started on this line (where
// the declaration is, e.g. it might be an incomplete line.
return ((pos.getStartOffset() >= lineBegin) && (pos.getStartOffset() <= lineEnd));
}
// /** Return true iff the name looks like an operator name */
// private boolean isOperator(String name) {
// // If a name contains not a single letter, it is probably an operator - especially
// // if it is a short name
// int n = name.length();
//
// if (n > 2) {
// return false;
// }
//
// for (int i = 0; i < n; i++) {
// if (Character.isLetter(name.charAt(i))) {
// return false;
// }
// }
//
// return true;
// }
static void addLocals(Node node, Map<String, Node> variables) {
switch (node.getNodeType()) {
case LOCALASGNNODE: {
String name = ((INameNode)node).getName();
if (!variables.containsKey(name)) {
variables.put(name, node);
}
break;
}
case ARGSNODE: {
// TODO - use AstUtilities.getDefArgs here - but avoid hitting them twice!
//List<String> parameters = AstUtilities.getDefArgs(def, true);
// However, I've gotta find the parameter nodes themselves too!
ArgsNode an = (ArgsNode)node;
if (an.getRequiredCount() > 0) {
List<Node> args = an.childNodes();
for (Node arg : args) {
if (arg instanceof ListNode) {
List<Node> args2 = arg.childNodes();
for (Node arg2 : args2) {
if (arg2 instanceof ArgumentNode) {
variables.put(((ArgumentNode)arg2).getName(), arg2);
} else if (arg2 instanceof LocalAsgnNode) {
variables.put(((INameNode)arg2).getName(), arg2);
}
}
}
}
}
// Rest args
if (an.getRest() != null) {
String name = an.getRest().getName();
variables.put(name, an.getRest());
}
// Block args
if (an.getBlock() != null) {
String name = an.getBlock().getName();
variables.put(name, an.getBlock());
}
break;
}
// } else if (target instanceof AliasNode) {
// AliasNode an = (AliasNode)target;
// Tricky -- which NODE do we add here? Completion creator needs to be aware of new name etc. Do later.
// Besides, do we show it as a field or a method or what?
// variab
// if (an.getNewName().equals(name)) {
// OffsetRange range = AstUtilities.getAliasNewRange(an);
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
// } else if (an.getOldName().equals(name)) {
// OffsetRange range = AstUtilities.getAliasOldRange(an);
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
// }
// break;
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
switch (child.getNodeType()) {
case DEFNNODE:
case DEFSNODE:
case CLASSNODE:
case SCLASSNODE:
case MODULENODE:
// Don't look in nested context for local vars
continue;
}
addLocals(child, variables);
}
}
static void addDynamic(Node node, Map<String, Node> variables) {
if (node.getNodeType() == NodeType.DASGNNODE) {
String name = ((INameNode)node).getName();
if (!variables.containsKey(name)) {
variables.put(name, node);
}
//} else if (target instanceof ArgsNode) {
// ArgsNode an = (ArgsNode)target;
//
// if (an.getArgsCount() > 0) {
// List<Node> args = an.childNodes();
// List<String> parameters = null;
//
// for (Node arg : args) {
// if (arg instanceof ListNode) {
// List<Node> args2 = arg.childNodes();
// parameters = new ArrayList<String>(args2.size());
//
// for (Node arg2 : args2) {
// if (arg2 instanceof ArgumentNode) {
// OffsetRange range = AstUtilities.getRange(arg2);
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
// } else if (arg2 instanceof LocalAsgnNode) {
// OffsetRange range = AstUtilities.getRange(arg2);
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
// }
// }
// }
// }
// }
// } else if (!ignoreAlias && target instanceof AliasNode) {
// AliasNode an = (AliasNode)target;
//
// if (an.getNewName().equals(name)) {
// OffsetRange range = AstUtilities.getAliasNewRange(an);
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
// } else if (an.getOldName().equals(name)) {
// OffsetRange range = AstUtilities.getAliasOldRange(an);
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
// }
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
switch (child.getNodeType()) {
case ITERNODE:
//case BLOCKNODE:
case DEFNNODE:
case DEFSNODE:
case CLASSNODE:
case SCLASSNODE:
case MODULENODE:
continue;
}
addDynamic(child, variables);
}
}
private void addConstants(Node node, Map<String, Node> constants) {
if (node.getNodeType() == NodeType.CONSTDECLNODE) {
constants.put(((INameNode)node).getName(), node);
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
addConstants(child, constants);
}
}
private String loadResource(String basename) {
// TODO: I18N
InputStream is = null;
StringBuilder sb = new StringBuilder();
try {
is = new BufferedInputStream(RubyCodeCompleter.class.getResourceAsStream("resources/" +
basename));
//while (is)
while (true) {
int c = is.read();
if (c == -1) {
break;
}
sb.append((char)c);
}
if (sb.length() > 0) {
return sb.toString();
}
} catch (IOException ie) {
Exceptions.printStackTrace(ie);
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException ie) {
Exceptions.printStackTrace(ie);
}
}
return null;
}
private String getKeywordHelp(String keyword) {
// Difficulty here with context; "else" is used for both the ifelse.html and case.html both define it.
// End is even more used.
if (keyword.equals("if") || keyword.equals("elsif") || keyword.equals("else") ||
keyword.equals("then") || keyword.equals("unless")) { // NOI18N
return loadResource("ifelse.html"); // NOI18N
} else if (keyword.equals("case") || keyword.equals("when") || keyword.equals("else")) { // NOI18N
return loadResource("case.html"); // NOI18N
} else if (keyword.equals("rescue") || keyword.equals("ensure")) { // NOI18N
return loadResource("rescue.html"); // NOI18N
} else if (keyword.equals("yield")) { // NOI18N
return loadResource("yield.html"); // NOI18N
}
return null;
}
/**
* Find the best possible documentation match for the given IndexedClass or IndexedMethod.
* This involves looking at index to see which instances of this class or method
* definition have associated rdoc, as well as choosing between them based on the
* require statements in the file.
*/
static IndexedElement findDocumentationEntry(Node root, IndexedElement obj) {
// 1. Find entries known to have documentation
String fqn = obj.getSignature();
Set<?extends IndexedElement> result = obj.getIndex().getDocumented(fqn);
if ((result == null) || (result.isEmpty())) {
return null;
} else if (result.size() == 1) {
return result.iterator().next();
}
// 2. There are multiple matches so try to disambiguate them by the imports in this file.
// For example, for "File" we usually show the standard (builtin) documentation,
// unless you have required "ftools", which redefines File with new docs.
Set<IndexedElement> candidates;
if (root != null) {
candidates = new HashSet<IndexedElement>();
Set<String> requires = AstUtilities.getRequires(root);
for (IndexedElement o : result) {
String require = o.getRequire();
if (requires.contains(require)) {
candidates.add(o);
}
}
if (candidates.size() == 1) {
return candidates.iterator().next();
} else if (!candidates.isEmpty()) {
result = candidates;
}
}
// 3. Prefer builtin (kernel) docs over other docs.
candidates = new HashSet<IndexedElement>();
for (IndexedElement o : result) {
String url = o.getFileUrl();
if (RubyUtils.isRubyStubsURL(url)) {
candidates.add(o);
}
}
if (candidates.size() == 1) {
return candidates.iterator().next();
} else if (!candidates.isEmpty()) {
result = candidates;
}
// 4. Consider other heuristics, like picking the "larger" documentation
// (more lines)
// 5. Just pick an arbitrary one.
return result.iterator().next();
}
/**
* @todo If you invoke this on top of a symbol, I should really just show
* the documentation for that symbol!
*
* @param element The element we want to look up comments for
* @param parserResult The (optional) compilation parserResult for a document referencing the element.
* This is used to consult require-statements in the given compilation context etc.
* to choose among many alternatives. May be null, in which case the element had
* better be an IndexedElement.
*/
static List<String> getComments(ParserResult info, Element element) {
assert info != null || element instanceof IndexedElement;
if (element == null) {
return null;
}
Node node = null;
if (element instanceof AstElement) {
node = ((AstElement)element).getNode();
} else if (element instanceof IndexedElement) {
IndexedElement com = (IndexedElement)element;
Node root = null;
if (info != null) {
root = AstUtilities.getRoot(info);
}
IndexedElement match = findDocumentationEntry(root, com);
if (match != null) {
com = match;
element = com;
}
node = AstUtilities.getForeignNode(com);
if (node == null) {
return null;
}
} else {
assert false : element;
return null;
}
// Initially, I implemented this by using RubyParserResult.getCommentNodes.
// However, I -still- had to rely on looking in the Document itself, since
// the CommentNodes are not attached to the AST, and to do things the way
// RDoc does, I have to (for example) look to see if a comment is at the
// beginning of a line or on the same line as something else, or if two
// comments have any empty lines between them, and so on.
// When I started looking in the document itself, I realized I might as well
// do all the manipulation on the document, since having the Comment nodes
// don't particularly help.
Snapshot snapshot;
if (element instanceof IndexedElement) {
FileObject f = ((IndexedElement) element).getFileObject();
snapshot = Source.create(f).createSnapshot();
} else if (info != null) {
snapshot = info.getSnapshot();
} else {
return null;
}
List<String> comments = null;
// Check for RubyComObject: These are external files (like Ruby lib) where I need to check many files
if (node instanceof ClassNode && !(element instanceof IndexedElement)) {
String className = AstUtilities.getClassOrModuleName((ClassNode)node);
List<ClassNode> classes = AstUtilities.getClasses(AstUtilities.getRoot(info));
// Iterate backwards through the list because the most recent documentation
// should be chosen, if any
for (int i = classes.size() - 1; i >= 0; i--) {
ClassNode clz = classes.get(i);
String name = AstUtilities.getClassOrModuleName(clz);
if (name.equals(className)) {
comments = AstUtilities.gatherDocumentation(snapshot, clz);
if ((comments != null) && (!comments.isEmpty())) {
break;
}
}
}
} else {
comments = AstUtilities.gatherDocumentation(snapshot, node);
}
if ((comments == null) || (comments.isEmpty())) {
return null;
}
return comments;
}
public String document(ParserResult info, ElementHandle handle) {
Element element = null;
if (handle instanceof ElementHandle.UrlHandle) {
String url = ((ElementHandle.UrlHandle)handle).getUrl();
DeclarationLocation loc = new RubyDeclarationFinder().findLinkedMethod(info, url);
if (loc != DeclarationLocation.NONE) {
//element = loc.getElement();
ElementHandle h = loc.getElement();
if (handle != null) {
element = RubyParser.resolveHandle(info, h);
if (element == null) {
return null;
}
}
}
} else {
element = RubyParser.resolveHandle(info, handle);
}
if (element == null) {
return null;
}
if (element instanceof KeywordElement) {
return getKeywordHelp(((KeywordElement)element).getName());
} else if (element instanceof CommentElement) {
// Text is packaged as the name
String comment = element.getName();
RDocFormatter formatter = new RDocFormatter();
String[] comments = comment.split("\n");
for (String text : comments) {
// Truncate off leading whitespace before # on comment lines
for (int i = 0, n = text.length(); i < n; i++) {
char c = text.charAt(i);
if (c == '#') {
if (i > 0) {
text = text.substring(i);
break;
}
} else if (!Character.isWhitespace(c)) {
break;
}
}
formatter.appendLine(text);
}
return formatter.toHtml();
}
List<String> comments = getComments(info, element);
if (comments == null) {
if (FindersHelper.isFinderMethod(element.getName(), false)) {
return new RDocFormatter().getSignature(element) + NbBundle.getMessage(RubyCodeCompleter.class, "DynamicMethod");
}
String html = new RDocFormatter().getSignature(element) + "\n<hr>\n<i>" + NbBundle.getMessage(RubyCodeCompleter.class, "NoCommentFound") +"</i>";
return html;
}
RDocFormatter formatter = new RDocFormatter();
String name = element.getName();
if (name != null && name.length() > 0) {
formatter.setSeqName(name);
}
for (String text : comments) {
formatter.appendLine(text);
}
String html = formatter.toHtml();
if (!formatter.wroteSignature()) {
html = formatter.getSignature(element) + "\n<hr>\n" + html;
}
return html;
}
@Override
public ElementHandle resolveLink(String link, ElementHandle elementHandle) {
if (elementHandle == null) {
return null;
}
if (link.indexOf('#') != -1 && elementHandle.getMimeType().equals(RubyInstallation.RUBY_MIME_TYPE)) {
if (link.startsWith("#")) {
// Put the current class etc. in front of the method call if necessary
Element surrounding = RubyParser.resolveHandle(null, elementHandle);
if (surrounding != null && surrounding.getKind() != ElementKind.KEYWORD) {
String name = surrounding.getName();
ElementKind kind = surrounding.getKind();
if (!(kind == ElementKind.CLASS || kind == ElementKind.MODULE)) {
String in = surrounding.getIn();
if (in != null && in.length() > 0) {
name = in;
} else if (name != null) {
int index = name.indexOf('#');
if (index > 0) {
name = name.substring(0, index);
}
}
}
if (name != null) {
link = name + link;
}
}
}
return new ElementHandle.UrlHandle(link);
}
return null;
}
// before csl.api 2.11:
public Set<String> getApplicableTemplates(ParserResult info, int selectionBegin, int selectionEnd) {
return getApplicableTemplates(RubyUtils.getDocument(info), selectionBegin, selectionEnd);
}
// after csl.api 2.11:
public Set<String> getApplicableTemplates(Document d, int selectionBegin,
int selectionEnd) {
// TODO - check the code at the AST path and determine whether it makes sense to
// wrap it in a begin block etc.
// TODO - I'd like to be able to pass any selection-based templates I'm not familiar with
boolean valid = false;
if (selectionEnd != -1) {
if ((d == null) || (! (d instanceof BaseDocument))) {
return Collections.emptySet();
}
BaseDocument doc = (BaseDocument)d;
try {
doc.readLock();
if (selectionBegin == selectionEnd) {
return Collections.emptySet();
} else if (selectionEnd < selectionBegin) {
int temp = selectionBegin;
selectionBegin = selectionEnd;
selectionEnd = temp;
}
boolean startLineIsEmpty = Utilities.isRowEmpty(doc, selectionBegin);
boolean endLineIsEmpty = Utilities.isRowEmpty(doc, selectionEnd);
if ((startLineIsEmpty || selectionBegin <= Utilities.getRowFirstNonWhite(doc, selectionBegin)) &&
(endLineIsEmpty || selectionEnd > Utilities.getRowLastNonWhite(doc, selectionEnd))) {
// I have no text to the left of the beginning or text to the right of the end, but I might
// have just selected whitespace - check that
String text = doc.getText(selectionBegin, selectionEnd-selectionBegin);
for (int i = 0; i < text.length(); i++) {
if (!Character.isWhitespace(text.charAt(i))) {
// Make sure that we're not in a string etc
Token<?> token = LexUtilities.getToken(doc, selectionBegin);
if (token != null) {
TokenId id = token.id();
if (id != RubyTokenId.STRING_LITERAL && id != RubyTokenId.LINE_COMMENT &&
id != RubyTokenId.QUOTED_STRING_LITERAL && id != RubyTokenId.REGEXP_LITERAL &&
id != RubyTokenId.DOCUMENTATION) {
// Yes - allow surround with here
// TODO - make this smarter by looking at the AST and see if
// we have a complete set of nodes
valid = true;
}
}
break;
}
}
}
} catch (BadLocationException ble) {
// do nothing - see #154991
} finally {
doc.readUnlock();
}
} else {
valid = true;
}
if (valid) {
return selectionTemplates;
} else {
return Collections.emptySet();
}
}
private String suggestName(ParserResult info, int caretOffset, String prefix, Map params) {
// Look at the given context, compute fields and see if I can find a free name
caretOffset = AstUtilities.boundCaretOffset(info, caretOffset);
Node root = AstUtilities.getRoot(info);
if (root == null) {
return null;
}
AstPath path = new AstPath(root, caretOffset);
Node closest = path.leaf();
if (closest == null) {
return null;
}
if (prefix.startsWith("$")) {
// Look for a unique global variable -- this requires looking at the index
// XXX TODO
return null;
} else if (prefix.startsWith("@@")) {
// Look for a unique class variable -- this requires looking at superclasses and other class parts
// XXX TODO
return null;
} else if (prefix.startsWith("@")) {
// Look for a unique instance variable -- this requires looking at superclasses and other class parts
// XXX TODO
return null;
} else {
// Look for a local variable in the given scope
if (closest != null) {
Node method = AstUtilities.findLocalScope(closest, path);
Map<String, Node> variables = new HashMap<String, Node>();
addLocals(method, variables);
List<Node> applicableBlocks = AstUtilities.getApplicableBlocks(path, false);
for (Node block : applicableBlocks) {
addDynamic(block, variables);
}
// See if we have any name suggestions
String suggestions = (String)params.get(ATTR_DEFAULTS);
// Check the suggestions
if ((suggestions != null) && (suggestions.length() > 0)) {
String[] names = suggestions.split(",");
for (String suggestion : names) {
if (!variables.containsKey(suggestion)) {
return suggestion;
}
}
// Try some variations of the name
for (String suggestion : names) {
for (int number = 2; number < 5; number++) {
String name = suggestion + number;
if ((name.length() > 0) && !variables.containsKey(name)) {
return name;
}
}
}
}
// Try the prefix
if ((prefix.length() > 0) && !variables.containsKey(prefix)) {
return prefix;
}
// TODO: What's the right algorithm for uniqueifying a variable
// name in Ruby?
// For now, will just append a number
if (prefix.length() == 0) {
prefix = "var";
}
for (int number = 1; number < 15; number++) {
String name = (number == 1) ? prefix : (prefix + number);
if ((name.length() > 0) && !variables.containsKey(name)) {
return name;
}
}
}
return null;
}
}
public String resolveTemplateVariable(String variable, ParserResult result, int caretOffset,
String name, Map params) {
if (variable.equals(KEY_PIPE)) {
return "||";
}
// Old-style format - support temporarily
if (variable.equals(ATTR_UNUSEDLOCAL)) { // TODO REMOVEME
return suggestName(result, caretOffset, name, params);
}
if (params != null && params.containsKey(ATTR_UNUSEDLOCAL)) {
return suggestName(result, caretOffset, name, params);
}
if ((!(variable.equals(KEY_METHOD) || variable.equals(KEY_METHOD_FQN) ||
variable.equals(KEY_CLASS) || variable.equals(KEY_CLASS_FQN) ||
variable.equals(KEY_SUPERCLASS) || variable.equals(KEY_PATH) ||
variable.equals(KEY_FILE)))) {
return null;
}
caretOffset = AstUtilities.boundCaretOffset(result, caretOffset);
Node root = AstUtilities.getRoot(result);
if (root == null) {
return null;
}
AstPath path = new AstPath(root, caretOffset);
if (variable.equals(KEY_METHOD)) {
Node node = AstUtilities.findMethod(path);
if (node != null) {
return AstUtilities.getDefName(node);
}
} else if (variable.equals(KEY_METHOD_FQN)) {
MethodDefNode node = AstUtilities.findMethod(path);
if (node != null) {
String ctx = AstUtilities.getFqnName(path);
String methodName = AstUtilities.getDefName(node);
if ((ctx != null) && (ctx.length() > 0)) {
return ctx + "#" + methodName;
} else {
return methodName;
}
}
} else if (variable.equals(KEY_CLASS)) {
ClassNode node = AstUtilities.findClass(path);
if (node != null) {
return node.getCPath().getName();
}
} else if (variable.equals(KEY_SUPERCLASS)) {
ClassNode node = AstUtilities.findClass(path);
if (node != null) {
RubyIndex index = RubyIndex.get(result);
if (index != null) {
IndexedClass cls = index.getSuperclass(AstUtilities.getFqnName(path));
if (cls != null) {
return cls.getFqn();
}
}
String superCls = AstUtilities.getSuperclass(node);
if (superCls != null) {
return superCls;
} else {
return "Object";
}
}
} else if (variable.equals(KEY_CLASS_FQN)) {
return AstUtilities.getFqnName(path);
} else if (variable.equals(KEY_FILE)) {
return FileUtil.toFile(result.getSnapshot().getSource().getFileObject()).getName();
} else if (variable.equals(KEY_PATH)) {
return FileUtil.toFile(RubyUtils.getFileObject(result)).getPath();
}
return null;
}
public ParameterInfo parameters(ParserResult info, int lexOffset, CompletionProposal proposal) {
IndexedMethod[] methodHolder = new IndexedMethod[1];
int[] paramIndexHolder = new int[1];
int[] anchorOffsetHolder = new int[1];
int astOffset = AstUtilities.getAstOffset(info, lexOffset);
if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
methodHolder, paramIndexHolder, anchorOffsetHolder, null, QuerySupport.Kind.PREFIX)) {
return ParameterInfo.NONE;
}
IndexedMethod method = methodHolder[0];
if (method == null) {
return ParameterInfo.NONE;
}
int index = paramIndexHolder[0];
int astAnchorOffset = anchorOffsetHolder[0];
int anchorOffset = LexUtilities.getLexerOffset(info, astAnchorOffset);
// TODO: Make sure the caret offset is inside the arguments portion
// (parameter hints shouldn't work on the method call name itself
// See if we can find the method corresponding to this call
// if (proposal != null) {
// Element element = proposal.getElement();
// if (element instanceof IndexedMethod) {
// method = ((IndexedMethod)element);
// }
// }
List<String> params = method.getParameters();
if ((params != null) && (!params.isEmpty())) {
return new ParameterInfo(params, index, anchorOffset);
}
return ParameterInfo.NONE;
}
/** Return true if we always want to use parentheses
* @todo Make into a user-configurable option
* @todo Avoid doing this if there's possible ambiguity (e.g. nested method calls
* without spaces
*/
public QueryType getAutoQuery(JTextComponent component, String typedText) {
char c = typedText.charAt(0);
if (c == '\n' || c == '(' || c == '[' || c == '{') {
return QueryType.STOP;
}
if (c != '.' && c != ':') {
return QueryType.NONE;
}
int offset = component.getCaretPosition();
BaseDocument doc = (BaseDocument) component.getDocument();
if (".".equals(typedText)) { // NOI18N
// See if we're in Ruby context
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts == null) {
return QueryType.NONE;
}
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return QueryType.NONE;
}
if (ts.offset() == offset && !ts.movePrevious()) {
return QueryType.NONE;
}
Token<? extends RubyTokenId> token = ts.token();
TokenId id = token.id();
// ".." is a range, not dot completion
if (id == RubyTokenId.RANGE) {
return QueryType.NONE;
}
// TODO - handle embedded ruby
if ("comment".equals(id.primaryCategory()) || // NOI18N
"string".equals(id.primaryCategory()) || // NOI18N
"regexp".equals(id.primaryCategory())) { // NOI18N
return QueryType.NONE;
}
return QueryType.COMPLETION;
}
if (":".equals(typedText)) { // NOI18N
// See if it was "::" and we're in ruby context
int dot = component.getSelectionStart();
try {
if ((dot > 1 && component.getText(dot-2, 1).charAt(0) == ':') && // NOI18N
isRubyContext(doc, dot-1)) {
return QueryType.COMPLETION;
}
} catch (BadLocationException ble) {
// do nothing - see #154991
}
}
return QueryType.NONE;
}
public static boolean isRubyContext(BaseDocument doc, int offset) {
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts == null) {
return false;
}
ts.move(offset);
if (!ts.movePrevious() && !ts.moveNext()) {
return true;
}
TokenId id = ts.token().id();
if ("comment".equals(id.primaryCategory()) || "string".equals(id.primaryCategory()) || // NOI18N
"regexp".equals(id.primaryCategory())) { // NOI18N
return false;
}
return true;
}
}