blob: 9bc71f8ac64b527805bb68bfb25dab619aa79824 [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.completion;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.beans.BeanInfo;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.ImageIcon;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.editor.completion.Completion;
import org.netbeans.api.queries.VisibilityQuery;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.php.editor.NavUtils;
import org.netbeans.modules.php.editor.parser.astnodes.ASTNode;
import org.netbeans.modules.php.editor.parser.astnodes.Include;
import org.netbeans.modules.php.editor.parser.astnodes.ParenthesisExpression;
import org.netbeans.modules.php.editor.parser.astnodes.Scalar;
import org.netbeans.modules.php.editor.parser.astnodes.Scalar.Type;
import org.netbeans.modules.php.project.api.PhpSourcePath;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.netbeans.spi.editor.completion.CompletionProvider;
import org.netbeans.spi.editor.completion.CompletionResultSet;
import org.netbeans.spi.editor.completion.CompletionTask;
import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery;
import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
import org.netbeans.spi.editor.completion.support.CompletionUtilities;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.util.Exceptions;
import org.openide.util.Utilities;
/**
* Base on code from contrib/editor.fscompletion.
* @author Jan Lahoda
*/
public class FSCompletion implements CompletionProvider {
public FSCompletion() {
}
@Override
public CompletionTask createTask(int queryType, JTextComponent component) {
return new AsyncCompletionTask(new AsyncCompletionQuery() {
@Override
protected void query(final CompletionResultSet resultSet, final Document doc, final int caretOffset) {
try {
FileObject file = NavUtils.getFile(doc);
if (file == null || caretOffset == -1) {
return;
}
final List<FileObject> includePath = PhpSourcePath.getIncludePath(file);
try {
Source source = Source.create(file);
if (source == null) {
// the create source checks, whether the file is valid and whether is not a folder
// in such case returns null.
return;
}
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
Parser.Result parserResult = resultIterator.getParserResult();
if (parserResult instanceof ParserResult) {
ParserResult parameter = (ParserResult) parserResult;
List<ASTNode> path = NavUtils.underCaret(parameter, caretOffset);
if (path.size() < 2) {
return;
}
ASTNode d1 = path.get(path.size() - 1);
ASTNode d2 = path.get(path.size() - 2);
if (d2 instanceof ParenthesisExpression) {
if (path.size() < 3) {
return;
}
d2 = path.get(path.size() - 3);
}
if (!(d1 instanceof Scalar) || !(d2 instanceof Include)) {
return;
}
Scalar s = (Scalar) d1;
if (s.getScalarType() != Type.STRING || !NavUtils.isQuoted(s.getStringValue())) {
return;
}
int startOffset = s.getStartOffset() + 1;
if (startOffset > caretOffset || startOffset < 0 || caretOffset < 0) {
return;
}
final String prefix = parameter.getSnapshot().getText().subSequence(startOffset, caretOffset).toString();
List<FileObject> relativeTo = new LinkedList<>();
if (!prefix.startsWith("../")) { //NOI18N
relativeTo.addAll(includePath);
}
final PHPIncludesFilter filter = new PHPIncludesFilter(parameter.getSnapshot().getSource().getFileObject());
final FileObject parent = parameter.getSnapshot().getSource().getFileObject().getParent();
if (parent != null) {
relativeTo.add(parent);
}
resultSet.addAllItems(computeRelativeItems(relativeTo, prefix, startOffset, filter));
}
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
} finally {
resultSet.finish();
}
}
}, component);
}
@Override
public int getAutoQueryTypes(JTextComponent component, String typedText) {
return 0;
}
@org.netbeans.api.annotations.common.SuppressWarnings({"DMI_HARDCODED_ABSOLUTE_FILENAME"})
private static List<? extends CompletionItem> computeRelativeItems(
Collection<? extends FileObject> relativeTo,
final String prefix,
int anchor,
FileObjectFilter filter) throws IOException {
final String goUp = "../";
assert relativeTo != null;
List<CompletionItem> result = new LinkedList<>();
int lastSlash = prefix.lastIndexOf('/');
String pathPrefix;
String filePrefix;
if (lastSlash != (-1)) {
pathPrefix = prefix.substring(0, lastSlash);
filePrefix = prefix.substring(lastSlash + 1);
} else {
pathPrefix = null;
filePrefix = prefix;
}
Set<FileObject> directories = new HashSet<>();
File prefixFile = null;
if (pathPrefix != null && !pathPrefix.startsWith(".")) { //NOI18N
if (pathPrefix.length() == 0 && prefix.startsWith("/")) {
prefixFile = new File("/"); //NOI18N
} else {
prefixFile = new File(pathPrefix);
}
}
if (prefixFile != null && prefixFile.exists()) {
//absolute path
File normalizeFile = FileUtil.normalizeFile(prefixFile);
FileObject fo = FileUtil.toFileObject(normalizeFile);
if (fo != null) {
directories.add(fo);
}
} else {
//relative path
for (FileObject f : relativeTo) {
if (pathPrefix != null) {
File toFile = FileUtil.toFile(f);
if (toFile != null) {
URI resolve = Utilities.toURI(toFile).resolve(pathPrefix).normalize();
File normalizedFile = FileUtil.normalizeFile(Utilities.toFile(resolve));
f = FileUtil.toFileObject(normalizedFile);
} else {
f = f.getFileObject(pathPrefix);
}
}
if (f != null) {
directories.add(f);
}
}
}
for (FileObject dir : directories) {
FileObject[] children = dir.getChildren();
for (int cntr = 0; cntr < children.length; cntr++) {
FileObject current = children[cntr];
if (VisibilityQuery.getDefault().isVisible(current) && current.getNameExt().toLowerCase().startsWith(filePrefix.toLowerCase()) && filter.accept(current)) {
result.add(new FSCompletionItem(current, pathPrefix != null ? pathPrefix + "/" : "./", anchor)); //NOI18N
}
}
}
if (goUp.startsWith(filePrefix) && directories.size() == 1) {
final FileObject parent = directories.iterator().next();
if (parent.getParent() != null && VisibilityQuery.getDefault().isVisible(parent.getParent()) && filter.accept(parent.getParent())) {
result.add(new FSCompletionItem(parent, "", anchor) {
@Override
public void render(Graphics g, Font defaultFont, Color defaultColor, Color backgroundColor, int width, int height, boolean selected) {
CompletionUtilities.renderHtml(super.icon, goUp, null, g, defaultFont, defaultColor, width, height, selected);
}
@Override
protected String getText() {
return (!prefix.equals("..") && !prefix.equals(".") ? prefix : "") + goUp; //NOI18N
}
});
}
}
return result;
}
private static class PHPIncludesFilter implements FileObjectFilter {
private FileObject currentFile;
public PHPIncludesFilter(FileObject currentFile) {
this.currentFile = currentFile;
}
@Override
public boolean accept(FileObject file) {
if (file.equals(currentFile) || isNbProjectMetadata(file)) {
return false; //do not include self in the cc result
}
if (file.isFolder()) {
return true;
}
String mimeType = FileUtil.getMIMEType(file);
return mimeType != null && mimeType.startsWith("text/");
}
private static boolean isNbProjectMetadata(FileObject fo) {
final String metadataName = "nbproject"; //NOI18N
if (fo.getPath().indexOf(metadataName) != -1) {
while (fo != null) {
if (fo.isFolder()) {
if (metadataName.equals(fo.getNameExt())) {
return true;
}
}
fo = fo.getParent();
}
}
return false;
}
}
static class FSCompletionItem implements CompletionItem {
private FileObject file;
private ImageIcon icon;
private int anchor;
private String prefix;
public FSCompletionItem(FileObject file, String prefix, int anchor) throws IOException {
this.file = file;
DataObject od = DataObject.find(file);
icon = new ImageIcon(od.getNodeDelegate().getIcon(BeanInfo.ICON_COLOR_16x16));
this.anchor = anchor;
this.prefix = prefix;
}
private void doSubstitute(final JTextComponent component, String toAdd, final int backOffset) {
final BaseDocument doc = (BaseDocument) component.getDocument();
final int caretOffset = component.getCaretPosition();
final String value = getText() + (toAdd != null && (!toAdd.equals("/") || (toAdd.equals("/") && !getText().endsWith(toAdd))) ? toAdd : ""); //NOI18N
// Update the text
doc.runAtomic(new Runnable() {
@Override
public void run() {
try {
String pfx = doc.getText(anchor, caretOffset - anchor);
doc.remove(caretOffset - pfx.length(), pfx.length());
doc.insertString(caretOffset - pfx.length(), value, null);
component.setCaretPosition(component.getCaretPosition() - backOffset);
} catch (BadLocationException e) {
Exceptions.printStackTrace(e);
}
}
});
}
@Override
public void defaultAction(JTextComponent component) {
doSubstitute(component, null, 0);
if (!file.isFolder()) {
Completion.get().hideAll();
}
}
@Override
public void processKeyEvent(KeyEvent evt) {
if (evt.getID() == KeyEvent.KEY_TYPED) {
String strToAdd = "/";
if (evt.getKeyChar() == '/') {
doSubstitute((JTextComponent) evt.getSource(), strToAdd, strToAdd.length() - 1);
evt.consume();
}
}
}
@Override
public int getPreferredWidth(Graphics g, Font defaultFont) {
return CompletionUtilities.getPreferredWidth(file.getNameExt(), null, g, defaultFont);
}
@Override
public void render(Graphics g, Font defaultFont, Color defaultColor, Color backgroundColor, int width, int height, boolean selected) {
CompletionUtilities.renderHtml(icon, file.getNameExt(), null, g, defaultFont, defaultColor, width, height, selected);
}
@Override
public CompletionTask createDocumentationTask() {
return null;
}
@Override
public CompletionTask createToolTipTask() {
return null;
}
@Override
public boolean instantSubstitution(JTextComponent component) {
return false; //????
}
@Override
public int getSortPriority() {
return -1000;
}
@Override
public CharSequence getSortText() {
return getText();
}
@Override
public CharSequence getInsertPrefix() {
return getText();
}
protected String getText() {
return prefix + file.getNameExt() + (file.isFolder() ? "/" : "");
}
@Override
public int hashCode() {
return getText().hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof FSCompletionItem)) {
return false;
}
FSCompletionItem remote = (FSCompletionItem) o;
return getText().equals(remote.getText());
}
}
interface FileObjectFilter {
boolean accept(FileObject file);
}
}