NetBeans RCP: Editable diff viewer using custom base source

Some days ago Geertjan showed you how to create an editable diff viewer [1] . His approach was based on the integration of the standard “Diff to…”-action (org.netbeans.modules.diff.DiffAction), which requires the selection of nodes.

As a sequel i will show you today how to create a diff action too, BUT this time you are free to define the base you are comparing to. This hasn’t to be another node. Such base can be another file, a revision from source code management, text from a DB and so on. The base source will be shown on the left side and the modified (and editable) source (based on your Node/DataObject, where we attach our action to) will be shown on the right side of the diff window.

The following sample app will compare the given FileObject (from the Lookup) with its base. The base  – in this very simplified example – is a String read from a file.

The main point is to implement your own editable StreamSource for the modified source. Return true from isEditable() and provide a Reader for your FileObject (from node of the modified source)  in createReader(). That’s all. The rest of the sourcecode is only plain standard stuff like creating a diff control, adding this control to a new TopComponent and creating an action, which will open the TopComponent. Geertjan already blogged about it at [2].

example[1]

@ActionID(category = "Edit", id = "de.markiewb.netbeans.sample.editablediff.EditableDiffAction")
@ActionRegistration(displayName = "#CTL_DiffAction")
@ActionReferences({
    @ActionReference(path = "Editors/Popup")})
@Messages("CTL_DiffAction=Editable diff...")
public final class EditableDiffAction implements ActionListener {

    private final FileObject file;

    public EditableDiffAction(FileObject context) {
	this.file = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
	final String baseText = getOriginalText(file);
	final StreamSource base = StreamSource.createSource("name1", "base", file.getMIMEType(), new StringReader(baseText));
	final StreamSource modified = EditableStreamSource.createEditableSource("name2", "modified", file.getMIMEType(), file);

	openDiffWindow(file, modified, base, "Diff of " + file.getNameExt() + " to original");
    }

    public void openDiffWindow(final FileObject localFile, final StreamSource local, final StreamSource remote, final String title) {
	SwingUtilities.invokeLater(new Runnable() {
	    @Override
	    public void run() {
		try {
		    final TopComponent tc = new TopComponent();
		    tc.setDisplayName(title);
		    tc.setLayout(new BorderLayout());
		    makeDiffWindowSaveable(tc, localFile);
		    tc.add(DiffController.createEnhanced(remote, local).getJComponent(), BorderLayout.CENTER);
		    tc.open();
		    tc.requestActive();
		} catch (IOException ex) {
		}
	    }
	});
    }

    /**
     * Put the node of dataObject of the fileObject into "globallookup". This
     * allows saving via CTRL-S shortkey from within the editable diff TC. See
     * http://netbeans.org/bugzilla/show_bug.cgi?id=223703
     *
     * @param tc
     * @param fileObject
     */
    private void makeDiffWindowSaveable(TopComponent tc, FileObject fileObject) {
	if (tc != null) {
	    Node node;
	    try {
		node = DataObject.find(fileObject).getNodeDelegate();
	    } catch (DataObjectNotFoundException e) {
		node = new AbstractNode(Children.LEAF, Lookups.singleton(fileObject));
	    }
	    tc.setActivatedNodes(new Node[]{node});
	}
    }

    public String getOriginalText(FileObject file) {
	// TODO this is only a mockup
	// TODO get original text from other sources like SCM, DB, template files..
	try {
	    return file.asText("UTF-8").replace("public ", "public final ");
	} catch (IOException ex) {
	    Exceptions.printStackTrace(ex);
	}
	return "";
    }

    public static class EditableStreamSource extends StreamSource {

	private String name, title, mimeType;
	private FileObject fileObject;

	private EditableStreamSource(String name, String title, String mimeType, FileObject fileObject) {
	    this.name = name;
	    this.title = title;
	    this.mimeType = mimeType;
	    this.fileObject = fileObject;
	}

	public static StreamSource createEditableSource(String name, String title, String mimeType, FileObject fileObject) {
	    return new EditableStreamSource(name, title, mimeType, fileObject);
	}

	@Override
	public String getName() {
	    return this.name;
	}

	@Override
	public String getTitle() {
	    return this.title;
	}

	@Override
	public Lookup getLookup() {
	    return Lookups.fixed(fileObject);
	}

	@Override
	public boolean isEditable() {
	    return fileObject.canWrite();
	}

	@Override
	public String getMIMEType() {
	    return mimeType;
	}

	@Override
	public Reader createReader() throws IOException {
	    return new FileReader(FileUtil.toFile(fileObject));
	}

	@Override
	public Writer createWriter(Difference[] conflicts) throws IOException {
	    return null;
	}
    }
}

PS: There is a small trick to enable the “save”-action (Menubar File->Save / CTRL-S) for your new editable diff, which won’t get enabled after changing content in the right editor pane of the diff viewer by default. You have to associate your node to the TopComponent – see makeDiffWindowSaveable(). Thanks to  Ondrej Vrabec for the solution – see [3].

The full source code of this sample is available at [4] and also documented at [5]

[1] https://blogs.oracle.com/geertjan/entry/how_to_create_an_editable
[2] https://blogs.oracle.com/geertjan/entry/netbeans_diff_api
[3] http://netbeans.org/bugzilla/show_bug.cgi?id=223703
[3] https://github.com/markiewb/nb-api-samples/tree/master/CustomEditableDiff
[4] http://wiki.netbeans.org/DevFaqEditorHowToAddDiffView

Updated NetBeans plugin “Show path in title” and project groups

It was a very productive weekend, so here again another short note about a plugin update as a result. I extended my popular plugin “show path in title” to show the name of current project group too. (It was simple after some investigations i documented at [1].)

You do not know what “project groups” are? They are a mix of an eclipse workspace and a working set. This NetBeans feature IMHO is underestimated. Have a look at the following blogs for some more kudos [2], [3] and [4].

FYI: There is also a good old plugin from Aljoscha Rittner at [5] which allows you to switch the group right from the toolbar.

Now back on topic: The plugin can be downloaded manually at the plugin portal [6] (for versions from 6.9 up to 7.3). The plugin is also verified for NB 7.2, so it can be downloaded from the plugin dialog in your IDE. Feel free to give feedback and feature/bug reports.

New NetBeans plugin: “Sort line tools”

Only a short note today. I repackaged the good old Line Tools plugin from sandipchitale and made it compatible to NB 7.x as good as i could. Because of some exceptions at runtime i had to disable some of the known features. But nonetheless it is still useful.

What’s left? Well, some actions and a toolbar for the editor which allow you to

  • Sort lines spanned by selection in ascending/descending (in-)case-sensitive order
  • Remove duplicate lines while sorting.

1357991789_screenshot[3][1]

The plugin can be downloaded manually at the plugin portal http://plugins.netbeans.org/plugin/45925/line-tools-nb-7-x-compatible. The plugin is also verified for NB 7.2, so it can be downloaded from the plugin dialog in your IDE.

NetBeans IDE 7.3: How to extend the context menu of the members and hierarchy view?

Today i will show you how to extend the context menu of the newly redesigned members and hierarchy view in NetBeans IDE 7.3.

Plugin your action at the following extension points

Navigator/Actions/Members/text/x-java
Navigator/Actions/Hierarchy/text/x-java

and get the TreePathHandle from the Node’s lookup. Easy isn’t it? Please note that these extension points are new in NB IDE 7.3 – see the issues [1] and [2].

This allows the integration of current/future actions which react on
TreePathHandle like

  • toggle method breakpoint (when a method is selected)
  • toggle class breakpoint (when a class is selected)
  • run/debug main method (when a class with a main method is selected)
  • run/debug test (when a class with testmethods is selected or when a testmethod is selected)
  • add as profiling root (when a method is selected)
  • integration of my own “Copy FQN” plugin (i will blog about it next time)

But now a more concrete example:

package de.markiewb.netbeans.sample.extendMembersAndHierarchyView;

import java.util.ArrayList;
import java.util.List;
import static javax.swing.Action.NAME;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import org.netbeans.api.java.source.TreePathHandle;
import org.openide.awt.*;
import org.openide.nodes.Node;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.CookieAction;
import org.openide.util.actions.Presenter;

@ActionID(category = "Edit", id = "de.markiewb.netbeans.sample.extendMembersAndHierarchyView.SampleAction")
@ActionRegistration(displayName = "SampleAction")
@ActionReferences({
    @ActionReference(path = "Navigator/Actions/Members/text/x-java", position = 1150),
    @ActionReference(path = "Navigator/Actions/Hierarchy/text/x-java", position = 1150)
})
public final class SampleAction extends CookieAction implements Presenter.Popup {

    public SampleAction() {
	putValue(NAME, "Hello TreePathHandle(s)");
    }

    @Override
    public String getName() {
	return "Hello TreePathHandle(s)";
    }

    @Override
    public JMenuItem getPopupPresenter() {
	return new JMenuItem(this);
    }

    @Override
    public HelpCtx getHelpCtx() {
	return null;
    }

    @Override
    protected boolean enable(Node[] activatedNodes) {
	//.. use tph from lookup in node
	for (Node node : activatedNodes) {
	    if (null != node.getLookup().lookup(TreePathHandle.class)) {
		return true;
	    };
	}
	return false;
    }

    @Override
    protected int mode() {
	return CookieAction.MODE_ALL;
    }

    @Override
    protected Class[] cookieClasses() {
	return new Class[]{Node.class};
    }

    @Override
    protected void performAction(Node[] nodes) {
	List<TreePathHandle> treePathHandles = new ArrayList<TreePathHandle>();
	for (Node node : nodes) {
	    treePathHandles.add(node.getLookup().lookup(TreePathHandle.class));
	}
	//show all treePathHandles
	JOptionPane.showMessageDialog(null, "Hello\n" + treePathHandles);
    }
}

The result:

example

The full sample project is available at [3]

[1] http://netbeans.org/bugzilla/show_bug.cgi?id=220057
[2] http://netbeans.org/bugzilla/show_bug.cgi?id=224499
[3] https://github.com/markiewb/nb-api-samples/tree/master/ExtendMembersAndHierarchyView
[4] http://wiki.netbeans.org/DevFaqAddActionToMembersOrHierarchyView