blob: e1f3c376c58e7431d6751f83443883838ec623e5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.java.completion;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.prefs.Preferences;
import java.util.regex.Pattern;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.*;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.CodeStyle;
import org.netbeans.api.java.source.CodeStyleUtils;
import org.openide.util.WeakListeners;
/**
*
* @author Dusan Balek
*/
public final class Utilities {
private static final String EMPTY = ""; //NOI18N
private static final String ERROR = "<error>"; //NOI18N
private static final String COMPLETION_CASE_SENSITIVE = "completion-case-sensitive"; // NOI18N
private static final boolean COMPLETION_CASE_SENSITIVE_DEFAULT = true;
private static final String SHOW_DEPRECATED_MEMBERS = "show-deprecated-members"; // NOI18N
private static final boolean SHOW_DEPRECATED_MEMBERS_DEFAULT = true;
private static final String JAVA_COMPLETION_WHITELIST = "javaCompletionWhitelist"; //NOI18N
private static final String JAVA_COMPLETION_BLACKLIST = "javaCompletionBlacklist"; //NOI18N
private static final String JAVA_COMPLETION_BLACKLIST_DEFAULT = ""; //NOI18N
private static final String JAVA_COMPLETION_EXCLUDER_METHODS = "javaCompletionExcluderMethods"; //NOI18N
private static final boolean JAVA_COMPLETION_EXCLUDER_METHODS_DEFAULT = false;
private static final String JAVA_COMPLETION_SUBWORDS = "javaCompletionSubwords"; //NOI18N
private static final boolean JAVA_COMPLETION_SUBWORDS_DEFAULT = false;
private static boolean caseSensitive = COMPLETION_CASE_SENSITIVE_DEFAULT;
private static boolean showDeprecatedMembers = SHOW_DEPRECATED_MEMBERS_DEFAULT;
private static boolean javaCompletionExcluderMethods = JAVA_COMPLETION_EXCLUDER_METHODS_DEFAULT;
private static boolean javaCompletionSubwords = JAVA_COMPLETION_SUBWORDS_DEFAULT;
private static final AtomicBoolean inited = new AtomicBoolean(false);
private static Preferences preferences;
private static final PreferenceChangeListener preferencesTracker = new PreferenceChangeListener() {
@Override
public void preferenceChange(PreferenceChangeEvent evt) {
String settingName = evt == null ? null : evt.getKey();
if (settingName == null || COMPLETION_CASE_SENSITIVE.equals(settingName)) {
caseSensitive = preferences.getBoolean(COMPLETION_CASE_SENSITIVE, COMPLETION_CASE_SENSITIVE_DEFAULT);
}
if (settingName == null || SHOW_DEPRECATED_MEMBERS.equals(settingName)) {
showDeprecatedMembers = preferences.getBoolean(SHOW_DEPRECATED_MEMBERS, SHOW_DEPRECATED_MEMBERS_DEFAULT);
}
if (settingName == null || JAVA_COMPLETION_BLACKLIST.equals(settingName)) {
String blacklist = preferences.get(JAVA_COMPLETION_BLACKLIST, EMPTY);
updateExcluder(excludeRef, blacklist);
}
if (settingName == null || JAVA_COMPLETION_WHITELIST.equals(settingName)) {
String whitelist = preferences.get(JAVA_COMPLETION_WHITELIST, EMPTY);
updateExcluder(includeRef, whitelist);
}
if (settingName == null || JAVA_COMPLETION_EXCLUDER_METHODS.equals(settingName)) {
javaCompletionExcluderMethods = preferences.getBoolean(JAVA_COMPLETION_EXCLUDER_METHODS, JAVA_COMPLETION_EXCLUDER_METHODS_DEFAULT);
}
if (settingName == null || JAVA_COMPLETION_SUBWORDS.equals(settingName)) {
javaCompletionSubwords = preferences.getBoolean(JAVA_COMPLETION_SUBWORDS, JAVA_COMPLETION_SUBWORDS_DEFAULT);
}
}
};
private static String cachedPrefix = null;
private static Pattern cachedCamelCasePattern = null;
private static Pattern cachedSubwordsPattern = null;
public static boolean startsWith(String theString, String prefix) {
if (theString == null || theString.length() == 0 || ERROR.equals(theString)) {
return false;
}
if (prefix == null || prefix.length() == 0) {
return true;
}
// sub word completion
if (javaCompletionSubwords) {
// example:
// 'out' produces '.*?[o|O].*?[u|U].*?[t|T].*?'
// org.openide.util.Utilities.acoh -> actionsForPath
// java.lang.System.out -> setOut
// argex -> IllegalArgumentException
// java.util.Collections.que -> asLifoQueue
// java.lang.System.sin -> setIn, getSecurityManager, setSecurityManager
// check whether user input matches the regex
if (!prefix.equals(cachedPrefix)) {
cachedCamelCasePattern = null;
cachedSubwordsPattern = null;
}
if (cachedSubwordsPattern == null) {
cachedPrefix = prefix;
String patternString = createSubwordsPattern(prefix);
cachedSubwordsPattern = patternString != null ? Pattern.compile(patternString) : null;
}
if (cachedSubwordsPattern != null && cachedSubwordsPattern.matcher(theString).matches()) {
return true;
}
}
return isCaseSensitive() ? theString.startsWith(prefix)
: theString.toLowerCase(Locale.ENGLISH).startsWith(prefix.toLowerCase(Locale.ENGLISH));
}
static String createSubwordsPattern(String prefix) {
StringBuilder sb = new StringBuilder(3 + 8 * prefix.length());
sb.append(".*?");
for (int i = 0; i < prefix.length(); i++) {
char charAt = prefix.charAt(i);
if (!Character.isJavaIdentifierPart(charAt)) {
return null;
}
if (Character.isLowerCase(charAt)) {
sb.append("[");
sb.append(charAt);
sb.append(Character.toUpperCase(charAt));
sb.append("]");
} else {
//keep uppercase characters as beacons
// for example: java.lang.System.sIn -> setIn
sb.append(charAt);
}
sb.append(".*?");
}
return sb.toString();
}
public static boolean startsWithCamelCase(String theString, String prefix) {
if (theString == null || theString.length() == 0 || prefix == null || prefix.length() == 0) {
return false;
}
if (!prefix.equals(cachedPrefix)) {
cachedCamelCasePattern = null;
cachedSubwordsPattern = null;
}
if (cachedCamelCasePattern == null) {
StringBuilder sb = new StringBuilder();
int lastIndex = 0;
int index;
do {
index = findNextUpper(prefix, lastIndex + 1);
String token = prefix.substring(lastIndex, index == -1 ? prefix.length() : index);
sb.append(token);
sb.append(index != -1 ? "[\\p{javaLowerCase}\\p{Digit}_\\$]*" : ".*"); // NOI18N
lastIndex = index;
} while (index != -1);
cachedPrefix = prefix;
cachedCamelCasePattern = Pattern.compile(sb.toString());
}
return cachedCamelCasePattern.matcher(theString).matches();
}
private static int findNextUpper(String text, int offset) {
for (int i = offset; i < text.length(); i++) {
if (Character.isUpperCase(text.charAt(i))) {
return i;
}
}
return -1;
}
public static boolean isCaseSensitive() {
lazyInit();
return caseSensitive;
}
public static boolean isSubwordSensitive() {
lazyInit();
return javaCompletionSubwords;
}
public static boolean isShowDeprecatedMembers() {
lazyInit();
return showDeprecatedMembers;
}
static private final AtomicReference<Collection<String>> excludeRef = new AtomicReference<>();
static private final AtomicReference<Collection<String>> includeRef = new AtomicReference<>();
private static void updateExcluder(AtomicReference<Collection<String>> existing, String updated) {
Collection<String> nue = new LinkedList<>();
if (updated == null || updated.length() == 0) {
existing.set(nue);
return;
}
String[] entries = updated.split(","); //NOI18N
for (String entry : entries) {
if (entry.length() != 0) {
nue.add(entry);
}
}
existing.set(nue);
}
/**
* @return the user setting for whether the excluder should operate on
* methods
*/
public static boolean isExcludeMethods() {
lazyInit();
return javaCompletionExcluderMethods;
}
/**
* @param fqn Fully Qualified Name (including method names). Packages names
* are expected to end in a trailing "." except the default package.
* @return
*/
public static boolean isExcluded(final CharSequence fqn) {
if (fqn == null || fqn.length() == 0) {
return true;
}
lazyInit();
String s = fqn.toString();
Collection<String> include = includeRef.get();
Collection<String> exclude = excludeRef.get();
if (include != null && !include.isEmpty()) {
for (String entry : include) {
if (s.endsWith(".") && entry.startsWith(s)) {
return false;
}
if ((entry.endsWith("*") && entry.length() - 1 <= s.length()
&& s.startsWith(entry.substring(0, entry.length() - 1)))
|| s.equals(entry)) {
return false;
}
}
}
if (exclude != null && !exclude.isEmpty()) {
for (String entry : exclude) {
if ((entry.endsWith("*") && entry.length() - 1 <= s.length() //NOI18N
&& s.startsWith(entry.substring(0, entry.length() - 1)))
|| s.equals(entry)) {
return true;
}
}
}
return false;
}
public static void exclude(final CharSequence fqn) {
if (fqn != null && fqn.length() > 0) {
lazyInit();
String blacklist = preferences.get(JAVA_COMPLETION_BLACKLIST, JAVA_COMPLETION_BLACKLIST_DEFAULT);
blacklist += (blacklist.length() > 0 ? "," + fqn : fqn); //NOI18N
preferences.put(JAVA_COMPLETION_BLACKLIST, blacklist);
}
}
public static List<String> varNamesSuggestions(TypeMirror type, ElementKind kind, Set<Modifier> modifiers, String suggestedName, String prefix, Types types, Elements elements, Iterable<? extends Element> locals, CodeStyle codeStyle) {
List<String> result = new ArrayList<>();
if (type == null && suggestedName == null) {
return result;
}
List<String> vnct = suggestedName != null ? Collections.singletonList(suggestedName) : varNamesForType(type, types, elements, prefix);
boolean isConst = false;
String namePrefix = null;
String nameSuffix = null;
switch (kind) {
case FIELD:
if (modifiers.contains(Modifier.STATIC)) {
if (codeStyle != null) {
namePrefix = codeStyle.getStaticFieldNamePrefix();
nameSuffix = codeStyle.getStaticFieldNameSuffix();
}
isConst = modifiers.contains(Modifier.FINAL);
} else {
if (codeStyle != null) {
namePrefix = codeStyle.getFieldNamePrefix();
nameSuffix = codeStyle.getFieldNameSuffix();
}
}
break;
case LOCAL_VARIABLE:
case EXCEPTION_PARAMETER:
case RESOURCE_VARIABLE:
if (codeStyle != null) {
namePrefix = codeStyle.getLocalVarNamePrefix();
nameSuffix = codeStyle.getLocalVarNameSuffix();
}
break;
case PARAMETER:
if (codeStyle != null) {
namePrefix = codeStyle.getParameterNamePrefix();
nameSuffix = codeStyle.getParameterNameSuffix();
}
break;
}
if (isConst) {
List<String> ls = new ArrayList<>(vnct.size());
for (String s : vnct) {
ls.add(getConstName(s));
}
vnct = ls;
}
if (vnct.isEmpty() && prefix != null && prefix.length() > 0
&& (namePrefix != null && namePrefix.length() > 0
|| nameSuffix != null && nameSuffix.length() >0)) {
vnct = Collections.singletonList(prefix);
}
String p = prefix;
while (p != null && p.length() > 0) {
List<String> l = new ArrayList<>();
for (String name : vnct) {
if (startsWith(name, p)) {
l.add(name);
}
}
if (l.isEmpty()) {
p = nextName(p);
} else {
vnct = l;
prefix = prefix.substring(0, prefix.length() - p.length());
p = null;
}
}
for (String name : vnct) {
boolean isPrimitive = type != null && type.getKind().isPrimitive();
if (prefix != null && prefix.length() > 0) {
if (isConst) {
name = prefix.toUpperCase(Locale.ENGLISH) + '_' + name;
} else {
name = prefix + name.toUpperCase(Locale.ENGLISH).charAt(0) + name.substring(1);
}
}
int cnt = 1;
String baseName = name;
name = CodeStyleUtils.addPrefixSuffix(name, namePrefix, nameSuffix);
while (isClashing(name, type, locals)) {
if (isPrimitive) {
char c = name.charAt(namePrefix != null ? namePrefix.length() : 0);
name = CodeStyleUtils.addPrefixSuffix(Character.toString(++c), namePrefix, nameSuffix);
if (c == 'z' || c == 'Z') { //NOI18N
isPrimitive = false;
}
} else {
name = CodeStyleUtils.addPrefixSuffix(baseName + cnt++, namePrefix, nameSuffix);
}
}
result.add(name);
}
return result;
}
private static List<String> varNamesForType(TypeMirror type, Types types, Elements elements, String prefix) {
switch (type.getKind()) {
case ARRAY:
TypeElement iterableTE = elements.getTypeElement("java.lang.Iterable"); //NOI18N
TypeMirror iterable = iterableTE != null ? types.getDeclaredType(iterableTE) : null;
TypeMirror ct = ((ArrayType) type).getComponentType();
if (ct.getKind() == TypeKind.ARRAY && iterable != null && types.isSubtype(ct, iterable)) {
return varNamesForType(ct, types, elements, prefix);
}
List<String> vnct = new ArrayList<>();
for (String name : varNamesForType(ct, types, elements, prefix)) {
vnct.add(name.endsWith("s") ? name + "es" : name + "s"); //NOI18N
}
return vnct;
case BOOLEAN:
case BYTE:
case CHAR:
case DOUBLE:
case FLOAT:
case INT:
case LONG:
case SHORT:
String str = type.toString().substring(0, 1);
return prefix != null && !prefix.equals(str)
? Collections.<String>emptyList()
: Collections.<String>singletonList(str);
case TYPEVAR:
return Collections.<String>singletonList(type.toString().toLowerCase(Locale.ENGLISH));
case ERROR:
String tn = ((ErrorType) type).asElement().getSimpleName().toString();
if (tn.toUpperCase(Locale.ENGLISH).contentEquals(tn)) {
return Collections.<String>singletonList(tn.toLowerCase(Locale.ENGLISH));
}
StringBuilder sb = new StringBuilder();
ArrayList<String> al = new ArrayList<>();
if ("Iterator".equals(tn)) { //NOI18N
al.add("it"); //NOI18N
}
while ((tn = nextName(tn)).length() > 0) {
al.add(tn);
sb.append(tn.charAt(0));
}
if (sb.length() > 0) {
String s = sb.toString();
if (prefix == null || prefix.length() == 0 || s.startsWith(prefix)) {
al.add(s);
}
}
return al;
case DECLARED:
iterableTE = elements.getTypeElement("java.lang.Iterable"); //NOI18N
iterable = iterableTE != null ? types.getDeclaredType(iterableTE) : null;
tn = ((DeclaredType) type).asElement().getSimpleName().toString();
if (tn.toUpperCase(Locale.ENGLISH).contentEquals(tn)) {
return Collections.<String>singletonList(tn.toLowerCase(Locale.ENGLISH));
}
sb = new StringBuilder();
al = new ArrayList<>();
if ("Iterator".equals(tn)) { //NOI18N
al.add("it"); //NOI18N
}
while ((tn = nextName(tn)).length() > 0) {
al.add(tn);
sb.append(tn.charAt(0));
}
if (iterable != null && types.isSubtype(type, iterable)) {
List<? extends TypeMirror> tas = ((DeclaredType) type).getTypeArguments();
if (tas.size() > 0) {
TypeMirror et = tas.get(0);
if (et.getKind() == TypeKind.ARRAY || (et.getKind() != TypeKind.WILDCARD && types.isSubtype(et, iterable))) {
al.addAll(varNamesForType(et, types, elements, prefix));
} else {
for (String name : varNamesForType(et, types, elements, prefix)) {
al.add(name.endsWith("s") ? name + "es" : name + "s"); //NOI18N
}
}
}
}
if (sb.length() > 0) {
String s = sb.toString();
if (prefix == null || prefix.length() == 0 || s.startsWith(prefix)) {
al.add(s);
}
}
return al;
case WILDCARD:
TypeMirror bound = ((WildcardType) type).getExtendsBound();
if (bound == null) {
bound = ((WildcardType) type).getSuperBound();
}
if (bound != null) {
return varNamesForType(bound, types, elements, prefix);
}
}
return Collections.<String>emptyList();
}
private static String getConstName(String s) {
StringBuilder sb = new StringBuilder();
boolean prevUpper = true;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isUpperCase(c)) {
if (!prevUpper) {
sb.append('_');
}
sb.append(c);
prevUpper = true;
} else {
sb.append(Character.toUpperCase(c));
prevUpper = false;
}
}
return sb.toString();
}
private static String nextName(CharSequence name) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c)) {
char lc = Character.toLowerCase(c);
sb.append(lc);
sb.append(name.subSequence(i + 1, name.length()));
break;
}
}
return sb.toString();
}
private static boolean isClashing(String varName, TypeMirror type, Iterable<? extends Element> locals) {
try {
if (JavaTokenId.valueOf(varName).primaryCategory().startsWith("keyword")) {
return true;
}
} catch (Exception e) {
}
if (type != null && type.getKind() == TypeKind.DECLARED && ((DeclaredType) type).asElement().getSimpleName().contentEquals(varName)) {
return true;
}
for (Element e : locals) {
if ((e.getKind().isField() || e.getKind() == ElementKind.LOCAL_VARIABLE || e.getKind() == ElementKind.RESOURCE_VARIABLE
|| e.getKind() == ElementKind.PARAMETER || e.getKind() == ElementKind.EXCEPTION_PARAMETER) && varName.contentEquals(e.getSimpleName())) {
return true;
}
}
return false;
}
private static void lazyInit() {
if (inited.compareAndSet(false, true)) {
preferences = MimeLookup.getLookup(JavaTokenId.language().mimeType()).lookup(Preferences.class);
preferences.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, preferencesTracker, preferences));
preferencesTracker.preferenceChange(null);
}
}
private Utilities() {
}
}