Jump to Declaration for FreeMarker

While working on the "Jump to Declaration" feature, I tried to get the coloring of the editor to be somewhat useful and harmonious (click to enlarge the image below):

As you can see, above, you can hold down the mouse, move over MAX_AGE, or any other variable declared by the #assign statements at the top of the file, and then click the hyperlink, which jumps the cursor to the declaration.

Code:

import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
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.LineCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.Line;
import org.openide.util.Exceptions;

@MimeRegistration(mimeType = "text/x-ftl", service = HyperlinkProviderExt.class)
public class AssignHyperlinkProvider implements HyperlinkProviderExt {

    private int literalStartOffset, literalEndOffset;
    private int lineNumber;

    @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(literalStartOffset, literalEndOffset - literalStartOffset);
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
        return "Click to jump to declaration";
    }

    @Override
    public void performClickAction(Document doc, int offset, HyperlinkType ht) {
        try {
            FileObject fo = getFileObject(doc);
            LineCookie lc = DataObject.find(fo).getLookup().lookup(LineCookie.class);
            Line line = lc.getLineSet().getOriginal(lineNumber);
            line.show(Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FRONT);
        } catch (DataObjectNotFoundException 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) {
        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("ID") || t.id().name().equals("PRINTABLE_CHARS")) {
            int start = ts.offset();
            int end = ts.offset() + t.length();
            getCurrentLineNumber(doc, t);
            if (getDeclaration(ts, t, start, end)) {
                ts.move(offset);
                ts.movePrevious();
                if (!ts.token().id().name().equals("ASSIGN")) {
                    return new int[]{literalStartOffset, literalEndOffset};
                }
            }
        }
        return null;
    }

    private void getCurrentLineNumber(Document doc, Token t) {
        //Get the current line number:
        FileObject fo = getFileObject(doc);
        LineCookie lc;
        try {
            lc = DataObject.find(fo).getLookup().lookup(LineCookie.class);
            List<? extends Line> lines = lc.getLineSet().getLines();
            for (Line line : lines) {
                if (line.getText().contains("<#assign") && line.getText().contains(t.text().toString())) {
                    lineNumber = line.getLineNumber();
                }
            }
        } catch (DataObjectNotFoundException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    private boolean getDeclaration(TokenSequence ts, Token t, int start, int end) {
        for (int i = 0; i < ts.tokenCount(); i++) {
            ts.moveIndex(i);
            ts.moveNext();
            if (ts.token() != null && ts.token().id().name().equals("ASSIGN")) {
                //Get the next token, which is the matching ID:
                ts.moveNext();
                Token assignLiteral = ts.token();
                if (assignLiteral.text().toString().equals(t.text().toString())) {
                    literalStartOffset = start;
                    literalEndOffset = end;
                    StatusDisplayer.getDefault().setStatusText(assignLiteral.text().toString() + "/" + literalStartOffset + "/" + literalEndOffset);
                    return true;
                }
            }
        }
        return false;
    }
 
}
 

I also discovered I'm not the first working on FreeMarker support for NetBeans IDE:

http://code.google.com/p/freemarkerfornetbeans/

Since the above uses the same FTL.jj file that I'm using, I was able to reuse its handy utility class. However, features such as the hyperlinks I'm working on are not part of that plugin.

Comments:

Great work with Freemarker. I remember when I was starting to use NetBeans for java web development my first project was using sitemesh + freemarker. Unable to have text parsing and code completion for freemarker template was a bit pita. I hope this will end in a near future :)

Posted by guest on May 18, 2012 at 02:49 PM PDT #

Hi Geertjan, many thanks for these freemarker posts. I tend to prefer ftl over jsp, so highlighting would be nice.

One question though: when trying to build your project Netbeans indeed generates a .nbm file, however when I try to add this plugin to netbeans via "tools > plugins > add plugins..." I receive an error message stating that I need the "lexer to Netbeans bridge plugin" or something along that line. I'm not familiar with this plugin or where to get it. Any idea what I'm doing wrong? I'm using 7.3beta, perhaps thats the problem?

Posted by guest on October 25, 2012 at 10:00 PM PDT #

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
« July 2014
SunMonTueWedThuFriSat
  
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  
       
Today