package org.netbeans.freemarker;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyledDocument;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.editor.NbEditorUtilities;
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.java.classpath.ClassPathProvider;
import org.openide.util.Exceptions;
import freemarker.core.FMParserConstants;
/**
*
* @author rostanek
*/
@MimeRegistration(mimeType = "text/x-ftl", service = CompletionProvider.class)
public class FTLCompletionProvider implements CompletionProvider {
private final String[] ftlParameters = "encoding strip_whitespace strip_text strict_syntax ns_prefixes attributes".split(" ");
private final String[] settingNames = "locale number_format boolean_format date_format time_format datetime_format time_zone sql_date_and_time_time_zone url_escaping_charset output_encoding classic_compatible".split(" ");
private final freemarker.template.Configuration configuration;
private final Set<String> directives;
private final Set<String> builtins;
public FTLCompletionProvider() {
configuration = new freemarker.template.Configuration();
directives = configuration.getSupportedBuiltInDirectiveNames();
builtins = configuration.getSupportedBuiltInNames();
}
@Override
public CompletionTask createTask(int queryType, JTextComponent jtc) {
if (queryType != COMPLETION_QUERY_TYPE) {
return null;
}
return new AsyncCompletionTask(new AsyncCompletionQuery() {
@Override
protected void query(CompletionResultSet completionResultSet, Document document, int caretOffset) {
int lineStartOffset;
String filter;
int startOffset = caretOffset - 1; // poczatek uzupelnianego slowa
String currentLine;
String text;
Set<String> idents = new HashSet<String>();
HashMap<String, Set<String>> variables = new HashMap<String, Set<String>>();
Token prevToken;
TokenSequence ts;
try {
((AbstractDocument) document).readLock();
ts = TokenHierarchy.get(document).tokenSequence();
while (ts.moveNext()) {
Token token = ts.token();
if (token.id().ordinal() == FMParserConstants.ID) {
idents.add(token.text().toString());
}
}
ts.move(caretOffset);
ts.movePrevious();
prevToken = ts.token();
final StyledDocument bDoc = (StyledDocument) document;
text = bDoc.getText(0, bDoc.getLength());
lineStartOffset = getRowFirstNonWhite(bDoc, caretOffset); // poczatek bieżącej linii
currentLine = getCurrentLine(bDoc, caretOffset);
//System.out.println(currentLine);
final char[] line = bDoc.getText(lineStartOffset, caretOffset - lineStartOffset).toCharArray();
currentLine = String.valueOf(line); // tekst od poczatku linii do kursora
//System.out.println(currentLine);
final int whiteOffset = indexOfWhite(line); // ostatnia spacja
filter = new String(line, whiteOffset + 1, line.length - whiteOffset - 1); // uzupelniane slowo
if (whiteOffset > 0) {
startOffset = lineStartOffset + whiteOffset + 1;
} else {
startOffset = lineStartOffset;
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
return;
} finally {
((AbstractDocument) document).readUnlock();
}
try {
Project project = FileOwnerQuery.getOwner(NbEditorUtilities.getFileObject(document));
ClassPathProvider cpp = project.getLookup().lookup(ClassPathProvider.class);
ClassPath cp = cpp.findClassPath(project.getProjectDirectory().getFileObject("src"), ClassPath.EXECUTE);
String ftlvariable = "(<|\\[)#--\\s@ftlvariable\\sname=\"(\\w+)\"\\stype=\"((\\w+\\.)+\\w+)\"\\s--(>|\\])";
Pattern ftlvarpattern = Pattern.compile(ftlvariable);
Matcher ftlvarmatcher = ftlvarpattern.matcher(text);
while (ftlvarmatcher.find()) {
String name = ftlvarmatcher.group(2);
String type = ftlvarmatcher.group(3);
Set<String> varFields = variables.get(name);
if (varFields == null) {
varFields = new HashSet<String>();
variables.put(name, varFields);
}
Class<?> typeClass = Class.forName(type, false, cp.getClassLoader(true));
for (java.lang.reflect.Field f : typeClass.getDeclaredFields()) {
varFields.add(f.getName());
}
}
} catch (Exception ex) {
//ex.printStackTrace();
}
// directives
if (currentLine.matches(".*(<|\\[)#\\w*")) {
filter = currentLine.substring(currentLine.lastIndexOf("#") + 1);
for (String keyword : directives) {
if (keyword.startsWith(filter)) {
completionResultSet.addItem(FTLCompletionItem.directive(keyword, caretOffset - filter.length(), caretOffset));
}
}
} else if (currentLine.matches(".*(<|\\[)@\\w*")) {
// unified call
filter = currentLine.substring(currentLine.lastIndexOf("@") + 1);
Set<String> names = new HashSet<String>();
Pattern pattern = Pattern.compile("(<|\\[)#assign\\s+(\\w+)");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
String name = matcher.group(2);
if (name.startsWith(filter)) {
names.add(name);
}
}
pattern = Pattern.compile("(<|\\[)#import\\s.+\\sas\\s+(\\w+)");
matcher = pattern.matcher(text);
while (matcher.find()) {
String name = matcher.group(2);
if (name.startsWith(filter)) {
names.add(name);
}
}
for (String name : names) {
completionResultSet.addItem(new FTLCompletionItem(name, caretOffset - filter.length(), caretOffset));
}
} else if (currentLine.matches("(<|\\[)#ftl\\s.*")) {
filter = currentLine.substring(currentLine.lastIndexOf(' ') + 1);
for (String param : ftlParameters) {
if (param.startsWith(filter) && !currentLine.contains(param)) {
completionResultSet.addItem(new FTLCompletionItem(param, "=", startOffset, caretOffset));
}
}
} else if (currentLine.matches(".*(<|\\[)#setting\\s+[a-z_]*")) {
filter = currentLine.substring(currentLine.lastIndexOf(' ') + 1);
for (String param : settingNames) {
if (param.startsWith(filter)) {
completionResultSet.addItem(new FTLCompletionItem(param, "=", startOffset, caretOffset));
}
}
}
if (currentLine.matches(".*(<#|\\$\\{)[^>}]*\\w+\\?\\w*$")) { // builtins only inside interpolations or directives
filter = currentLine.substring(currentLine.lastIndexOf("?") + 1);
for (String builtin : builtins) {
if (builtin.startsWith(filter)) {
completionResultSet.addItem(FTLCompletionItem.builtin(builtin, lineStartOffset + currentLine.lastIndexOf("?") + 1, caretOffset));
}
}
}
if (currentLine.matches(".*\\$\\{(\\w*)")) {
filter = currentLine.substring(currentLine.lastIndexOf("{") + 1);
for (String ident : idents) {
if (ident.startsWith(filter)) {
completionResultSet.addItem(new FTLCompletionItem(ident, lineStartOffset + currentLine.lastIndexOf("{") + 1, caretOffset));
}
}
}
if (prevToken != null) {
/*if (prevToken.id().ordinal() == FMParserConstants.DOLLAR_INTERPOLATION_OPENING) {
for (String ident : idents) {
if (ident.startsWith(filter)) {
completionResultSet.addItem(new FTLCompletionItem(ident, caretOffset, caretOffset));
}
}
}*/
if (prevToken.id().ordinal() == FMParserConstants.DOT) {
if (ts.movePrevious()) {
Set<String> varFields = variables.get(ts.token().text().toString());
if (varFields != null) {
for (String field : varFields) {
completionResultSet.addItem(new FTLCompletionItem(field, caretOffset, caretOffset));
}
}
}
}
}
completionResultSet.finish();
}
}, jtc);
}
@Override
public int getAutoQueryTypes(JTextComponent jtc, String string) {
if ("#@?".contains(string)) {
return COMPLETION_QUERY_TYPE;
}
return 0;
}
/**
* Gets current line as String, trimmed.
*
* @param doc document
* @param offset caret position
* @return text from current line
*/
static String getCurrentLine(StyledDocument doc, int offset) {
try {
Element lineElement = doc.getParagraphElement(offset);
return doc.getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset()).trim();
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
return "";
}
}
static int getRowFirstNonWhite(StyledDocument doc, int offset)
throws BadLocationException {
Element lineElement = doc.getParagraphElement(offset);
int start = lineElement.getStartOffset();
while (start + 1 < lineElement.getEndOffset()) {
try {
if (doc.getText(start, 1).charAt(0) != ' ') {
break;
}
} catch (BadLocationException ex) {
throw (BadLocationException) new BadLocationException(
"calling getText(" + start + ", " + (start + 1)
+ ") on doc of length: " + doc.getLength(), start
).initCause(ex);
}
start++;
}
return start;
}
static int indexOfWhite(char[] line) {
int i = line.length;
while (--i > -1) {
final char c = line[i];
if (Character.isWhitespace(c)) {
return i;
}
}
return -1;
}
}