Now that there's syntax coloring for FreeMarker, let's take a look at hyperlink support:
So, when there's an import statement, you can hold down the Ctrl key, and move the mouse over the file reference. When you do so, you automatically see a tooltip. The tooltip can be formatted via HTML, to create effects such as this:
You also see a hyperlink. Click the hyperlink and the referenced file opens.
For the first time I used org.netbeans.lib.editor.hyperlink.spi.HyperlinkProviderExt, which I learned about from reading some NetBeans sources. The main thing it does for me is give me access to the tooltip.
Here's all the code:
import java.io.File;
import java.util.EnumSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.lib.editor.hyperlink.spi.HyperlinkProviderExt;
import org.netbeans.lib.editor.hyperlink.spi.HyperlinkType;
import org.openide.awt.StatusDisplayer;
import org.openide.cookies.OpenCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.util.Exceptions;
@MimeRegistration(mimeType = "text/x-ftl", service = HyperlinkProviderExt.class)
public class FTLHyperlinkProvider implements HyperlinkProviderExt {
private int startOffset, endOffset;
private String identifier = ".*\\<#import (.*?) ";
@Override
public Set<HyperlinkType> getSupportedHyperlinkTypes() {
return EnumSet.of(HyperlinkType.GO_TO_DECLARATION);
}
@Override
public boolean isHyperlinkPoint(Document doc, int offset, HyperlinkType type) {
return getHyperlinkSpan(doc, offset, type) != null;
}
@Override
public int[] getHyperlinkSpan(Document doc, int offset, HyperlinkType type) {
return getIdentifierSpan(doc, offset);
}
@Override
public String getTooltipText(Document doc, int offset, HyperlinkType type) {
String text = null;
try {
text = doc.getText(startOffset, endOffset - startOffset);
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
return "Click to open " + text;
}
@Override
public void performClickAction(Document doc, int offset, HyperlinkType ht) {
try {
String text = doc.getText(startOffset, endOffset - startOffset);
FileObject fo = getFileObject(doc);
String pathToFileToOpen = fo.getParent().getPath()+text;
File fileToOpen = FileUtil.normalizeFile(new File(pathToFileToOpen));
if (fileToOpen.exists()) {
try {
FileObject foToOpen = FileUtil.toFileObject(fileToOpen);
DataObject.find(foToOpen).getLookup().lookup(OpenCookie.class).open();
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
} else {
StatusDisplayer.getDefault().setStatusText(fileToOpen.getName() + " doesn't exist!");
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
private static FileObject getFileObject(Document doc) {
DataObject od = (DataObject) doc.getProperty(Document.StreamDescriptionProperty);
return od != null ? od.getPrimaryFile() : null;
}
private int[] getIdentifierSpan(Document doc, int offset) {
Matcher matcher = null;
try {
matcher = Pattern.compile(identifier).matcher(doc.getText(0, doc.getLength()));
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
while (matcher.find()) {
startOffset = matcher.start() + 10;
endOffset = matcher.end() - 2;
if (offset == startOffset) {
try {
String text = doc.getText(startOffset, endOffset - startOffset);
return new int[]{offset, offset + text.length()};
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
}
return null;
}
}
The final method above would be better like this:
private int[] getIdentifierSpan(Document doc, int offset) {
TokenHierarchy<?> th = TokenHierarchy.get(doc);
TokenSequence ts = th.tokenSequence(Language.find("text/x-ftl"));
if (ts == null) {
return null;
}
ts.move(offset);
if (!ts.moveNext()) {
return null;
}
Token t = ts.token();
if (t.id().name().equals("STRING_LITERAL")) {
//Correction for quotation marks around the token:
startOffset = ts.offset() + 1;
endOffset = ts.offset() + t.length() - 1;
//Check that the previous token was an import statement,
//otherwise we don't want our string literal hyperlinked:
ts.movePrevious();
Token prevToken = ts.token();
if (prevToken.id().name().equals("IMPORT")) {
return new int[]{startOffset, endOffset};
} else {
return null;
}
}
return null;
}
Thanks to NetBeans Dream Team member Emilian Bold for some advice and insights into this scenario, which is based on Hyperlink in a Plain Text File.
At this point, would be good to get a prioritized list of requirements from FreeMarker users out there.