/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 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]"
*
* 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.
*
* Contributor(s):
*
* Portions Copyrighted 2008 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.ImageIcon;
import org.jrubyparser.ast.Node;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.spi.DefaultCompletionProposal;
import org.netbeans.modules.ruby.elements.ClassElement;
import org.netbeans.modules.ruby.elements.Element;
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.openide.util.ImageUtilities;
class RubyCompletionItem extends DefaultCompletionProposal {
private static final boolean FORCE_COMPLETION_SPACES = Boolean.getBoolean("ruby.complete.spaces"); // NOI18N
protected final CompletionRequest request;
protected final Element element;
protected boolean symbol;
RubyCompletionItem(Element element, int anchorOffset, CompletionRequest request) {
this.element = element;
this.anchorOffset = anchorOffset;
this.request = request;
}
@Override
public String getName() {
return element.getName();
}
public void setSymbol(boolean symbol) {
this.symbol = symbol;
}
@Override
public String getInsertPrefix() {
if (symbol) {
return ":" + getName();
} else {
return getName();
}
}
public ElementHandle getElement() {
return element;
}
@Override
public ElementKind getKind() {
return element.getKind();
}
@Override
public ImageIcon getIcon() {
return null;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
if (element.getKind() == ElementKind.GLOBAL && (element instanceof IndexedVariable)) {
IndexedVariable idx = (IndexedVariable) element;
String in = idx.getIn();
if (in != null) {
formatter.appendText(in);
return formatter.getText();
}
}
return null;
}
@Override
public Set<Modifier> getModifiers() {
return element.getModifiers();
}
@Override
public String toString() {
String cls = getClass().getName();
cls = cls.substring(cls.lastIndexOf('.') + 1);
return cls + "(" + getKind() + "): " + getName();
}
@Override
public String[] getParamListDelimiters() {
return new String[]{"(", ")"}; // NOI18N
}
static class KeywordItem extends RubyCompletionItem {
private static ImageIcon keywordIcon;
private static final String RUBY_KEYWORD = "org/netbeans/modules/ruby/jruby.png"; //NOI18N
private final String keyword;
private final String description;
KeywordItem(String keyword, String description, int anchorOffset, CompletionRequest request) {
super(null, anchorOffset, request);
this.keyword = keyword;
this.description = description;
}
@Override
public String getName() {
return keyword;
}
@Override
public ElementKind getKind() {
return ElementKind.KEYWORD;
}
@Override
public String getRhsHtml(final HtmlFormatter formatter) {
return null;
}
@Override
public String getLhsHtml(final HtmlFormatter formatter) {
ElementKind kind = getKind();
formatter.name(kind, true);
formatter.appendText(keyword);
formatter.appendText(" "); // NOI18N
formatter.name(kind, false);
if (description != null) {
formatter.appendHtml(description);
}
return formatter.getText();
}
@Override
public ImageIcon getIcon() {
if (keywordIcon == null) {
keywordIcon = ImageUtilities.loadImageIcon(RUBY_KEYWORD, false);
}
return keywordIcon;
}
@Override
public Set<Modifier> getModifiers() {
return Collections.emptySet();
}
@Override
public ElementHandle getElement() {
// For completion documentation
return new KeywordElement(keyword);
}
}
static class ClassItem extends RubyCompletionItem {
ClassItem(Element element, int anchorOffset, CompletionRequest request) {
super(element, anchorOffset, request);
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
String in = ((ClassElement) element).getIn();
if (in != null) {
formatter.appendText(in);
} else {
return null;
}
return formatter.getText();
}
}
static class FieldItem extends RubyCompletionItem {
private final String forcedPrefix;
FieldItem(Element element, int anchorOffset, CompletionRequest request) {
this(element, anchorOffset, request, null);
}
FieldItem(Element element, int anchorOffset, CompletionRequest request, String forcedPrefix) {
super(element, anchorOffset, request);
this.forcedPrefix = forcedPrefix;
}
@Override
public String getLhsHtml(HtmlFormatter formatter) {
if (element instanceof IndexedField) {
IndexedField field = (IndexedField) element;
boolean emphasize = !field.isInherited();
if (emphasize) {
formatter.emphasis(true);
}
formatter.name(ElementKind.FIELD, true);
formatter.appendText(getName());
formatter.name(ElementKind.FIELD, false);
if (emphasize) {
formatter.emphasis(false);
}
return formatter.getText();
}
return super.getLhsHtml(formatter);
}
@Override
public String getInsertPrefix() {
if (forcedPrefix != null) {
return forcedPrefix + getName();
}
String name;
if (element.getModifiers().contains(Modifier.STATIC)) {
name = "@@" + getName();
} else {
name = "@" + getName();
}
if (symbol) {
name = ":" + name;
}
return name;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
// Top level methods (defined on Object) : print
// the defining file instead
if (element instanceof IndexedField) {
IndexedField idx = (IndexedField) element;
// TODO - check if top level?
//if (me.isTopLevel() && me instanceof IndexedMethod) {
//IndexedMethod im = (IndexedMethod)element;
//if (im.isTopLevel() && im.getRequire() != null) {
// formatter.appendText(im.getRequire());
//
// return formatter.getText();
//}
//}
String in = idx.getIn();
if (in != null) {
formatter.appendText(in);
return formatter.getText();
}
}
return null;
}
}
static class ParameterItem extends RubyCompletionItem {
private static ImageIcon symbolIcon;
private static final String CONSTANT_ICON = "org/netbeans/modules/ruby/symbol.png"; //NOI18N
private final String name;
private final String desc;
private final String insert;
ParameterItem(Element element, String name, String symbol, String insert, int anchorOffset, CompletionRequest request) {
super(element, anchorOffset, request);
this.name = name;
this.desc = symbol;
this.insert = insert;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
if (desc != null) {
formatter.appendText(desc);
return formatter.getText();
} else {
return null;
}
}
@Override
public ElementKind getKind() {
return ElementKind.PARAMETER;
}
@Override
public ImageIcon getIcon() {
if (symbolIcon == null) {
symbolIcon = ImageUtilities.loadImageIcon(CONSTANT_ICON, false);
}
return symbolIcon;
}
@Override
public String getName() {
return name;
}
@Override
public String getInsertPrefix() {
return insert;
}
}
static class CallItem extends MethodItem {
private final IndexedMethod method;
private final int index;
CallItem(IndexedMethod method, int parameterIndex, int anchorOffset, CompletionRequest request) {
super(method, anchorOffset, request);
this.method = method;
this.index = parameterIndex;
}
@Override
public ElementKind getKind() {
return ElementKind.CALL;
}
@Override
public String getInsertPrefix() {
return "";
}
@Override
public String getLhsHtml(HtmlFormatter formatter) {
ElementKind kind = getKind();
formatter.name(kind, true);
formatter.appendText(getName());
List<String> parameters = method.getParameters();
if ((parameters != null) && (!parameters.isEmpty())) {
formatter.appendHtml("("); // NOI18N
if (index > 0 && index < parameters.size()) {
formatter.appendText("... , ");
}
formatter.active(true);
formatter.appendText(parameters.get(Math.min(parameters.size() - 1, index)));
formatter.active(false);
if (index < parameters.size() - 1) {
formatter.appendText(", ...");
}
formatter.appendHtml(")"); // NOI18N
}
if (method.hasBlock() && !method.isBlockOptional()) {
formatter.appendText(" { }");
}
formatter.name(kind, false);
return formatter.getText();
}
@Override
public boolean isSmart() {
return true;
}
@Override
public List<String> getInsertParams() {
return null;
}
@Override
public String getCustomInsertTemplate() {
return null;
}
}
/** Methods/attributes inferred from ActiveRecord migrations */
static class DbItem extends RubyCompletionItem {
private final String name;
private final String type;
DbItem(Element element, String name, String type, int anchorOffset, CompletionRequest request) {
super(element, anchorOffset, request);
this.name = name;
this.type = type;
}
@Override
public String getLhsHtml(HtmlFormatter formatter) {
formatter.emphasis(true);
formatter.name(ElementKind.DB, true);
formatter.appendText(getName());
formatter.name(ElementKind.DB, false);
formatter.emphasis(false);
return formatter.getText();
}
@Override
public String getInsertPrefix() {
return name;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
// TODO - include table name somewhere?
formatter.appendText(type);
return formatter.getText();
}
@Override
public String getName() {
return name;
}
@Override
public ElementKind getKind() {
return ElementKind.DB;
}
@Override
public ImageIcon getIcon() {
return null;
}
@Override
public Set<Modifier> getModifiers() {
return Collections.emptySet();
}
@Override
public boolean isSmart() {
// All database attributes are considered smart matches
return true;
}
}
static class MethodItem extends RubyCompletionItem {
protected final IndexedMethod method;
MethodItem(IndexedMethod element, int anchorOffset, CompletionRequest request) {
super(element, anchorOffset, request);
this.method = element;
}
@Override
public String getLhsHtml(HtmlFormatter formatter) {
ElementKind kind = getKind();
boolean emphasize = !method.isInherited();
if (emphasize) {
formatter.emphasis(true);
}
formatter.name(kind, true);
formatter.appendText(getName());
formatter.name(kind, false);
if (emphasize) {
formatter.emphasis(false);
}
Collection<String> parameters = method.getParameters();
if ((parameters != null) && (!parameters.isEmpty())) {
formatter.appendHtml("("); // NOI18N
Iterator<String> it = parameters.iterator();
while (it.hasNext()) { // && tIt.hasNext()) {
formatter.parameters(true);
formatter.appendText(it.next());
formatter.parameters(false);
if (it.hasNext()) {
formatter.appendText(", "); // NOI18N
}
}
formatter.appendHtml(")"); // NOI18N
}
if (method.hasBlock() && !method.isBlockOptional()) {
formatter.appendText(" { }");
}
return formatter.getText();
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
// Top level methods (defined on Object) : print
// the defining file instead
if (method.isTopLevel() && method.getRequire() != null) {
formatter.appendText(method.getRequire());
return formatter.getText();
}
String in = method.getIn();
if (in != null) {
formatter.appendText(in);
return formatter.getText();
} else {
return null;
}
}
@Override
public String getCustomInsertTemplate() {
final String insertPrefix = getInsertPrefix();
List<String> params = method.getParameters();
String startDelimiter;
String endDelimiter;
boolean hasBlock = false;
int paramCount = params.size();
int printArgs = paramCount;
boolean hasHashArgs = method.getEncodedAttributes() != null &&
method.getEncodedAttributes().indexOf("=>") != -1; // NOI18N
if (paramCount > 0 && params.get(paramCount - 1).startsWith("&")) { // NOI18N
hasBlock = true;
printArgs--;
// Force parentheses around the call when using { } blocks
// to avoid presedence problems
startDelimiter = "("; // NOI18N
endDelimiter = ")"; // NOI18N
} else if (method.hasBlock()) {
hasBlock = true;
if (paramCount > 0) {
// Force parentheses around the call when using { } blocks
// to avoid presedence problems
startDelimiter = "("; // NOI18N
endDelimiter = ")"; // NOI18N
} else {
startDelimiter = "";
endDelimiter = "";
}
} else {
String[] delimiters = getParamListDelimiters();
assert delimiters.length == 2;
startDelimiter = delimiters[0];
endDelimiter = delimiters[1];
// When there are no args, don't use parentheses - and no spaces
// Don't add two blank spaces for the case where there are no args
if (printArgs == 0 /*&& startDelimiter.length() > 0 && startDelimiter.charAt(0) == ' '*/) {
startDelimiter = "";
endDelimiter = "";
}
}
StringBuilder sb = new StringBuilder();
sb.append(insertPrefix);
if (hasHashArgs && skipHashes()) {
// Uhm, no don't do this until we get to the first arg that takes a hash
// For methods with hashes, rely on code completion to insert args
sb.append(getInsertSuffix());
return sb.toString();
}
sb.append(startDelimiter);
int id = 1;
for (int i = 0; i < printArgs; i++) {
String paramDesc = params.get(i);
sb.append("${"); //NOI18N
// Ensure that we don't use one of the "known" logical parameters
// such that a parameter like "path" gets replaced with the source file
// path!
sb.append("ruby-cc-"); // NOI18N
sb.append(Integer.toString(id++));
sb.append(" default=\""); // NOI18N
sb.append(paramDesc);
sb.append("\""); // NOI18N
sb.append("}"); //NOI18N
if (i < printArgs - 1) {
sb.append(", "); //NOI18N
}
}
sb.append(endDelimiter);
if (hasBlock) {
String[] blockArgs = null;
String attrs = method.getEncodedAttributes();
if (attrs != null) {
int yieldNameBegin = attrs.indexOf(';');
if (yieldNameBegin != -1) {
int yieldNameEnd = attrs.indexOf(';', yieldNameBegin + 1);
if (yieldNameEnd != -1) {
blockArgs = attrs.substring(yieldNameBegin + 1,
yieldNameEnd).split(",");
}
}
}
// TODO - if it's not an indexed class, pull this from the
// method comments instead!
sb.append(" { |"); // NOI18N
if (blockArgs != null && blockArgs.length > 0) {
for (int i = 0; i < blockArgs.length; i++) {
if (i > 0) {
sb.append(","); // NOI18N
}
String arg = blockArgs[i];
sb.append("${unusedlocal defaults=\""); // NOI18N
sb.append(arg);
sb.append("\"}"); // NOI18N
}
} else {
sb.append("${unusedlocal defaults=\"i,e\"}"); // NOI18N
}
sb.append("| ${"); // NOI18N
sb.append("ruby-cc-"); // NOI18N
sb.append(Integer.toString(id++));
sb.append(" default=\"\"} }${cursor}"); // NOI18N
} else {
sb.append("${cursor}"); // NOI18N
}
// XXX: take this back, was commented after refactoring
// // Facilitate method parameter completion on this item
// try {
// callLineStart = Utilities.getRowStart(request.doc, anchorOffset);
// callMethod = method;
// } catch (BadLocationException ble) {
// Exceptions.printStackTrace(ble);
// }
return sb.toString();
}
protected boolean skipHashes() {
return true;
}
protected String getInsertSuffix() {
return " ";
}
@Override
public String[] getParamListDelimiters() {
// TODO - convert methods with NO parameters that take a block to insert { <here> }
String n = getName();
String in = element.getIn();
if ("Module".equals(in)) {
// Module.attr_ methods typically shouldn't use parentheses
if (n.startsWith("attr_")) {
return new String[]{" :", " "};
} else if (n.equals("include") || n.equals("import")) { // NOI18N
return new String[]{" ", " "};
} else if (n.equals("include_package")) { // NOI18N
return new String[]{" '", "'"}; // NOI18N
}
} else if ("Kernel".equals(in)) {
// Module.require: insert quotes!
if (n.equals("require")) { // NOI18N
return new String[]{" '", "'"}; // NOI18N
} else if (n.equals("p")) {
return new String[]{" ", " "}; // NOI18N
}
} else if ("Object".equals(in)) {
if (n.equals("include_class")) { // NOI18N
return new String[]{" '", "'"}; // NOI18N
}
}
if (forceCompletionSpaces()) {
// Can't have "" as the second arg because a bug causes pressing
// return to complete editing the last field (at he end of a buffer)
// such that the caret ends up BEFORE the last char instead of at the
// end of it
boolean ambiguous = false;
AstPath path = request.path;
if (path != null) {
Iterator<Node> it = path.leafToRoot();
while (it.hasNext()) {
Node node = it.next();
if (AstUtilities.isCall(node)) {
// We're in a call; see if it has parens
// TODO - no problem with ambiguity if it's on a separate line, correct?
// Is this the method we're trying to complete?
if (node != request.target) {
// See if the outer call has parentheses!
ambiguous = true;
break;
}
}
}
}
if (ambiguous) {
return new String[]{"(", ")"}; // NOI18N
} else {
return new String[]{" ", " "}; // NOI18N
}
}
if (element instanceof IndexedElement) {
List<String> comments = RubyCodeCompleter.getComments(null, element);
if (comments != null && !comments.isEmpty()) {
// Look through the comment, attempting to identify
// a usage of the current method and determine whether it
// is using parentheses or not.
// We only look for comments that look like code; e.g. they
// are indented according to rdoc conventions.
String name = getName();
boolean spaces = false;
boolean parens = false;
for (String line : comments) {
if (line.startsWith("# ")) { // NOI18N
// Look for usages - there could be many
int i = 0;
int length = line.length();
while (true) {
int index = line.indexOf(name, i);
if (index == -1) {
break;
}
index += name.length();
i = index;
if (index < length) {
char c = line.charAt(index);
if (c == ' ') {
spaces = true;
} else if (c == '(') {
parens = true;
}
}
}
}
}
// Only use spaces if no parens were seen and we saw spaces
if (!parens && spaces) {
//return new String[] { " ", "" }; // NOI18N
// HACK because live code template editing doesn't seem to work - it places the caret at theront of the word when the last param is in the text!
return new String[]{" ", " "}; // NOI18N
}
}
// Take a look at the method definition itself and look for parens there
}
// Default - (,)
return super.getParamListDelimiters();
}
@Override
public ElementKind getKind() {
if (method.getMethodType() == IndexedMethod.MethodType.ATTRIBUTE) {
return ElementKind.ATTRIBUTE;
}
return element.getKind();
}
}
/**
* Represents a completion item for a dynamic finder method, such as
* "find_all_by_name_and_price".
*/
static class FinderMethodItem extends MethodItem {
public FinderMethodItem(IndexedMethod element, int anchorOffset, CompletionRequest request) {
super(element, anchorOffset, request);
}
@Override
protected boolean skipHashes() {
// XXX: should return false, returning true for perf reasons now
return true;
}
}
static class VirtualFinderMethodItem extends MethodItem {
private final String prefix;
public VirtualFinderMethodItem(IndexedMethod element, int anchorOffset, CompletionRequest request, String prefix) {
super(element, anchorOffset, request);
this.prefix = prefix;
}
@Override
protected boolean skipHashes() {
// XXX: should return false, returning true for perf reasons now
return true;
}
@Override
protected String getInsertSuffix() {
return "";
}
@Override
public String getLhsHtml(HtmlFormatter formatter) {
ElementKind kind = getKind();
boolean emphasize = !method.isInherited();
if (emphasize) {
formatter.emphasis(true);
}
formatter.name(kind, true);
formatter.appendText(prefix + "...");
formatter.name(kind, false);
if (emphasize) {
formatter.emphasis(false);
}
return formatter.getText();
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
// Top level methods (defined on Object) : print
// the defining file instead
if (method.isTopLevel() && method.getRequire() != null) {
formatter.appendText(method.getRequire());
return formatter.getText();
}
String in = method.getIn();
if (in != null) {
formatter.appendText(in);
return formatter.getText();
} else {
return null;
}
}
@Override
public String getInsertPrefix() {
return prefix;
}
}
private static boolean forceCompletionSpaces() {
return FORCE_COMPLETION_SPACES;
}
}