X

Geertjan's Blog

  • May 17, 2012

Hyperlink for FreeMarker

Geertjan Wielenga
Product Manager

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.

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.