blob: 2207381ab7b2015f531eb40e596dea96e299ca40 [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.php.editor.csl;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.Icon;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.IndexSearcher;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport.Kind;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.completion.PHPCompletionItem;
import org.netbeans.modules.php.editor.api.ElementQuery.Index;
import org.netbeans.modules.php.editor.api.ElementQueryFactory;
import org.netbeans.modules.php.editor.api.NameKind;
import org.netbeans.modules.php.editor.api.QualifiedName;
import org.netbeans.modules.php.editor.api.QualifiedNameKind;
import org.netbeans.modules.php.editor.api.QuerySupportFactory;
import org.netbeans.modules.php.editor.api.elements.FullyQualifiedElement;
import org.netbeans.modules.php.editor.api.elements.InterfaceElement;
import org.netbeans.modules.php.editor.api.elements.PhpElement;
import org.netbeans.modules.php.editor.api.elements.TypeElement;
import org.netbeans.modules.php.project.api.PhpProjectUtils;
import org.netbeans.modules.php.project.api.PhpSourcePath;
import org.netbeans.spi.project.ui.support.ProjectConvertors;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
/**
*
* @author Radek Matous, Jan Lahoda
*/
public class PHPTypeSearcher implements IndexSearcher {
//TODO: no supported: came cases, regular expressions in queries (needs improve PHPIndex methods)
@Override
public Set<? extends Descriptor> getSymbols(Project project, String textForQuery, Kind originalkind, Helper helper) {
// XXX: use PHP specific path ids
EnumSet<Kind> regexpKinds = EnumSet.of(Kind.CAMEL_CASE, Kind.CASE_INSENSITIVE_CAMEL_CASE, Kind.CASE_INSENSITIVE_REGEXP);
EnumSet<Kind> insensitiveKinds = EnumSet.of(Kind.CASE_INSENSITIVE_CAMEL_CASE, Kind.CASE_INSENSITIVE_REGEXP, Kind.CASE_INSENSITIVE_PREFIX);
// PHP isn't Java so we may need to overrule the chosen kind
// in case the query looks like it may be a camel-case/wildcard pattern
// fix for #167687
Kind kind = originalkind;
if ((kind == Kind.CASE_INSENSITIVE_PREFIX || kind == Kind.PREFIX) && isCamelCasePattern(textForQuery)) {
kind = Kind.CAMEL_CASE;
}
final Collection<FileObject> findRoots = QuerySupport.findRoots(project,
Collections.singleton(PhpSourcePath.SOURCE_CP),
Arrays.asList(PhpSourcePath.BOOT_CP, PhpSourcePath.PROJECT_BOOT_CP),
Collections.<String>emptySet());
final Index index = ElementQueryFactory.createIndexQuery(QuerySupportFactory.get(findRoots));
Set<PHPTypeDescriptor> result = new HashSet<>();
if (index != null && textForQuery.trim().length() > 0) {
final boolean isVariable = textForQuery.startsWith("$"); //NOI18N
String query = prepareIdxQuery(textForQuery, regexpKinds, kind);
final Kind useKind = kind.equals(Kind.EXACT) ? Kind.EXACT : Kind.CASE_INSENSITIVE_PREFIX;
if (!kind.equals(Kind.EXACT)) {
query = query.toLowerCase();
}
final NameKind prefix = NameKind.create(query, useKind);
if (!isVariable) {
for (PhpElement indexedElement : index.getTopLevelElements(prefix)) {
if (!CodeUtils.isSyntheticTypeName(indexedElement.getName())) {
result.add(new PHPTypeDescriptor(indexedElement, helper));
}
}
for (PhpElement indexedElement : index.getMethods(prefix)) {
result.add(new PHPTypeDescriptor(indexedElement, helper));
}
}
for (PhpElement indexedElement : index.getTopLevelVariables(prefix)) {
result.add(new PHPTypeDescriptor(indexedElement, helper));
}
for (PhpElement indexedElement : index.getFields(prefix)) {
result.add(new PHPTypeDescriptor(indexedElement, helper));
}
for (PhpElement indexedElement : index.getTypeConstants(prefix)) {
result.add(new PHPTypeDescriptor(indexedElement, helper));
}
}
if (regexpKinds.contains(kind)) {
//handles wildcards and camelCases
Set<PHPTypeDescriptor> originalResult = result;
result = new HashSet<>();
Pattern pattern = queryToPattern(textForQuery, insensitiveKinds.contains(originalkind));
for (PHPTypeDescriptor typeDescriptor : originalResult) {
String typeName = typeDescriptor.getElement().getName();
if (pattern.matcher(typeName).matches()) {
result.add(typeDescriptor);
}
}
}
return result;
}
@Override
public Set<? extends Descriptor> getTypes(Project project, String textForQuery, Kind originalkind, Helper helper) {
// XXX: use PHP specific path ids
EnumSet<Kind> regexpKinds = EnumSet.of(Kind.CAMEL_CASE, Kind.CASE_INSENSITIVE_CAMEL_CASE, Kind.CASE_INSENSITIVE_REGEXP);
EnumSet<Kind> insensitiveKinds = EnumSet.of(Kind.CASE_INSENSITIVE_CAMEL_CASE, Kind.CASE_INSENSITIVE_REGEXP, Kind.CASE_INSENSITIVE_PREFIX);
// PHP isn't Java so we may need to overrule the chosen kind
// in case the query looks like it may be a camel-case/wildcard pattern
// fix for #167687
Kind kind = originalkind;
if ((kind == Kind.CASE_INSENSITIVE_PREFIX || kind == Kind.PREFIX) && isCamelCasePattern(textForQuery)) {
kind = Kind.CAMEL_CASE;
}
final Collection<FileObject> findRoots = QuerySupport.findRoots(project,
Collections.singleton(PhpSourcePath.SOURCE_CP),
Arrays.asList(PhpSourcePath.BOOT_CP, PhpSourcePath.PROJECT_BOOT_CP),
Collections.<String>emptySet());
final Index index = ElementQueryFactory.createIndexQuery(QuerySupportFactory.get(findRoots));
Set<PHPTypeDescriptor> result = new HashSet<>();
QualifiedName queryName = QualifiedName.create(textForQuery);
QualifiedNameKind qnk = queryName.getKind();
if (index != null) {
String query = qnk.isUnqualified() ? prepareIdxQuery(textForQuery, regexpKinds, kind).toLowerCase() : textForQuery;
NameKind nameKind = NameKind.prefix(QualifiedName.create(query));
for (PhpElement indexedElement : index.getTypes(nameKind)) {
if (!CodeUtils.isSyntheticTypeName(indexedElement.getName())) {
result.add(new PHPTypeDescriptor(indexedElement, helper));
}
}
}
if (qnk.isUnqualified() && regexpKinds.contains(kind)) {
//handles wildcards and camelCases
Set<PHPTypeDescriptor> originalResult = result;
result = new HashSet<>();
if (isCaseInsensitiveExactMatch(kind, textForQuery)) {
for (PHPTypeDescriptor typeDescriptor : originalResult) {
String typeName = typeDescriptor.getElement().getName();
if (textForQuery.equalsIgnoreCase(typeName)) {
result.add(typeDescriptor);
}
}
} else {
Pattern pattern = queryToPattern(textForQuery, insensitiveKinds.contains(originalkind));
for (PHPTypeDescriptor typeDescriptor : originalResult) {
String typeName = typeDescriptor.getElement().getName();
if (pattern.matcher(typeName).matches()) {
result.add(typeDescriptor);
}
}
}
}
return result;
}
private static boolean isCaseInsensitiveExactMatch(Kind kind, String textForQuery) {
return kind == Kind.CASE_INSENSITIVE_REGEXP && !textForQuery.endsWith("*"); //NOI18N
}
private static class PHPTypeDescriptor extends Descriptor {
private final PhpElement element;
private final PhpElement enclosingClass;
private String projectName;
private Icon projectIcon;
private final Helper helper;
private FileObject projectDirectory;
public PHPTypeDescriptor(PhpElement element, Helper helper) {
this(element, null, helper);
}
public PHPTypeDescriptor(PhpElement element, PhpElement enclosingClass, Helper helper) {
this.element = element;
this.enclosingClass = enclosingClass;
this.helper = helper;
}
@Override
public Icon getIcon() {
if (projectName == null) {
initProjectInfo();
}
if (element instanceof InterfaceElement) {
return PHPCompletionItem.getInterfaceIcon();
}
return helper.getIcon(element);
}
@Override
public String getTypeName() {
return element.getName();
}
@Override
public String getProjectName() {
if (projectName == null) {
initProjectInfo();
}
return projectName;
}
private void initProjectInfo() {
FileObject fo = element.getFileObject();
if (fo != null) {
Project p = ProjectConvertors.getNonConvertorOwner(fo);
if (p != null) {
if (PhpProjectUtils.isPhpProject(p)) {
projectDirectory = p.getProjectDirectory();
}
ProjectInformation pi = ProjectUtils.getInformation(p);
projectName = pi.getDisplayName();
projectIcon = pi.getIcon();
}
}
if (projectName == null) {
projectName = "";
}
}
@Override
public Icon getProjectIcon() {
if (projectName == null) {
initProjectInfo();
}
return projectIcon;
}
@Override
public FileObject getFileObject() {
return element.getFileObject();
}
@Override
public void open() {
FileObject fileObject = element.getFileObject();
if (fileObject != null) {
GsfUtilities.open(fileObject, element.getOffset(), element.getName());
} else {
Logger logger = Logger.getLogger(PHPTypeSearcher.class.getName());
logger.log(Level.INFO, String.format("%s: cannot find %s", PHPTypeSearcher.class.getName(), element.getFilenameUrl())); //NOI18N
}
}
@Override
public String getContextName() {
if (projectName == null) {
initProjectInfo();
}
StringBuilder sb = new StringBuilder();
boolean s = false;
if (element instanceof FullyQualifiedElement) {
FullyQualifiedElement fqnElement = (FullyQualifiedElement) element;
if (!fqnElement.getNamespaceName().isDefaultNamespace()) {
if (element instanceof TypeElement) {
sb.append(fqnElement.getFullyQualifiedName());
} else {
sb.append(fqnElement.getNamespaceName());
}
s = true;
}
} else {
if (enclosingClass != null) {
if ((enclosingClass instanceof FullyQualifiedElement) && (!((FullyQualifiedElement) enclosingClass).getNamespaceName().isDefaultNamespace())) {
sb.append(((FullyQualifiedElement) enclosingClass).getFullyQualifiedName().toString());
} else {
sb.append(enclosingClass.getName());
}
s = true;
}
}
FileObject file = getFileObject();
if (file != null) {
if (s) {
sb.append(" in ");
}
String filePath = FileUtil.getFileDisplayName(file);
String pathToDisplay = filePath;
if (projectDirectory != null) {
String projectPath = FileUtil.getFileDisplayName(projectDirectory);
if (projectPath.length() > 0 && projectPath.length() < filePath.length()) {
pathToDisplay = getProjectName() + " ." + filePath.substring(projectPath.length()); //NOI18N
}
}
sb.append(pathToDisplay); //NOI18N
}
if (sb.length() > 0) {
return sb.toString();
}
return null;
}
@Override
public ElementHandle getElement() {
return element;
}
@Override
public int getOffset() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String getSimpleName() {
return element.getName();
}
@Override
public String getOuterName() {
return null;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PHPTypeDescriptor other = (PHPTypeDescriptor) obj;
if (this.element != other.element && (this.element == null || !this.element.equals(other.element))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 19 * hash + (this.element != null ? this.element.hashCode() : 0);
return hash;
}
}
private static Pattern queryToPattern(String query, boolean caseInsensitive) {
StringBuilder sb = new StringBuilder();
char[] chars = query.toCharArray();
boolean incamel = false;
if (!query.startsWith("$")) {
sb.append("[$]*"); //NOI18N
}
for (int i = 0; i < chars.length; i++) {
if (i + 1 < chars.length && chars[i] == '.' && chars[i + 1] == '*') { //NOI18N
sb.append(".*"); //NOI18N
i++;
} else if (chars[i] == '?') { //NOI18N
sb.append('.'); //NOI18N
} else if (chars[i] == '*') {
sb.append(".*"); //NOI18N
} else if (chars[i] == '.') {
sb.append("."); //NOI18N
} else if (Character.isUpperCase(chars[i])) {
if (incamel) {
sb.append("[a-z0-9_]*"); //NOI18N
}
sb.append(chars[i]);
incamel = true;
} else if (i == 0 && chars[i] == '$') {
sb.append('\\').append(chars[i]); //NOI18N
} else {
sb.append(Pattern.quote(String.valueOf(chars[i])));
}
}
sb.append(".*"); //NOI18N
String patternString = sb.toString();
return caseInsensitive ? Pattern.compile(patternString, Pattern.CASE_INSENSITIVE) : Pattern.compile(patternString);
}
private String prepareIdxQuery(String textForQuery, EnumSet<Kind> regexpKinds, Kind kind) {
String query = textForQuery.toLowerCase();
if (regexpKinds.contains(kind)) {
final char charAt = textForQuery.charAt(0);
final int length = textForQuery.length();
if (Character.isLetter(charAt) && length > 0) {
query = query.substring(0, 1); //NOI18N
} else if (charAt == '$' && length > 1) {
query = query.substring(0, 1); //NOI18N
} else {
query = ""; //NOI18N
}
}
return query;
}
private static boolean isCamelCasePattern(String query) {
char[] chars = query.toCharArray();
for (char c : chars) {
if (c == '*' || c == '?' || Character.isUpperCase(c)) {
return true;
}
}
return false;
}
}