Hyperlink for FreeMarker

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.

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
24
25
26
27
28
29
30
   
       
Today