/* * [The "BSD license"] * Copyright (c) 2011 Terence Parr * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.stringtemplate.v4.gui; import org.antlr.runtime.CommonToken; import org.antlr.runtime.tree.CommonTree; import org.antlr.runtime.tree.CommonTreeAdaptor; import org.stringtemplate.v4.InstanceScope; import org.stringtemplate.v4.Interpreter; import org.stringtemplate.v4.ST; import org.stringtemplate.v4.STGroup; import org.stringtemplate.v4.STGroupFile; import org.stringtemplate.v4.STGroupString; import org.stringtemplate.v4.debug.EvalExprEvent; import org.stringtemplate.v4.debug.EvalTemplateEvent; import org.stringtemplate.v4.debug.InterpEvent; import org.stringtemplate.v4.misc.ErrorManager; import org.stringtemplate.v4.misc.Interval; import org.stringtemplate.v4.misc.Misc; import org.stringtemplate.v4.misc.STMessage; import org.stringtemplate.v4.misc.STRuntimeMessage; import javax.swing.*; import javax.swing.border.Border; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultHighlighter; import javax.swing.text.Highlighter; import javax.swing.text.JTextComponent; import javax.swing.tree.TreePath; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class STViz { protected static final String WINDOWS_LINE_ENDINGS = "WINDOWS_LINE_ENDINGS"; //public ST currentST; // current ST selected in template tree public EvalTemplateEvent root; public InterpEvent currentEvent; public InstanceScope currentScope; public List<InterpEvent> allEvents; public JTreeSTModel tmodel; public ErrorManager errMgr; public Interpreter interp; public String output; public List<String> trace; public List<STMessage> errors; public STViewFrame viewFrame; private final AtomicInteger updateDepth = new AtomicInteger(); public STViz(ErrorManager errMgr, EvalTemplateEvent root, String output, Interpreter interp, List<String> trace, List<STMessage> errors) { this.errMgr = errMgr; this.currentEvent = root; this.currentScope = root.scope; this.output = output; this.interp = interp; this.allEvents = interp.getEvents(); this.trace = trace; this.errors = errors; } public void open() { viewFrame = new STViewFrame(); updateStack(currentScope, viewFrame); updateAttributes(currentScope, viewFrame); List<InterpEvent> events = currentScope.events; tmodel = new JTreeSTModel(interp, (EvalTemplateEvent)events.get(events.size()-1)); viewFrame.tree.setModel(tmodel); viewFrame.tree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent treeSelectionEvent) { int depth = updateDepth.incrementAndGet(); try { if ( depth!=1 ) { return; } currentEvent = ((JTreeSTModel.Wrapper)viewFrame.tree.getLastSelectedPathComponent()).event; currentScope = currentEvent.scope; updateCurrentST(viewFrame); } finally { updateDepth.decrementAndGet(); } } }); JTreeASTModel astModel = new JTreeASTModel(new CommonTreeAdaptor(), currentScope.st.impl.ast); viewFrame.ast.setModel(astModel); viewFrame.ast.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent treeSelectionEvent) { int depth = updateDepth.incrementAndGet(); try { if ( depth!=1 ) { return; } TreePath path = treeSelectionEvent.getNewLeadSelectionPath(); if ( path==null ) return; CommonTree node = (CommonTree)treeSelectionEvent.getNewLeadSelectionPath().getLastPathComponent(); //System.out.println("select AST: "+node); CommonToken a = (CommonToken)currentScope.st.impl.tokens.get(node.getTokenStartIndex()); CommonToken b = (CommonToken)currentScope.st.impl.tokens.get(node.getTokenStopIndex()); highlight(viewFrame.template, a.getStartIndex(), b.getStopIndex()); } finally { updateDepth.decrementAndGet(); } } }); // Track selection of attr but do nothing for now // viewFrame.attributes.addListSelectionListener( // new ListSelectionListener() { // public void valueChanged(ListSelectionEvent e) { // int minIndex = viewFrame.attributes.getMinSelectionIndex(); // int maxIndex = viewFrame.attributes.getMaxSelectionIndex(); // for (int i = minIndex; i <= maxIndex; i++) { // if (viewFrame.attributes.isSelectedIndex(i)) { // //System.out.println("index="+i); // } // } // } // } // ); CaretListener caretListenerLabel = new CaretListener() { @Override public void caretUpdate(CaretEvent e) { int depth = updateDepth.incrementAndGet(); try { if ( depth!=1 ) { return; } int dot = toEventPosition((JTextComponent)e.getSource(), e.getDot()); currentEvent = findEventAtOutputLocation(allEvents, dot); if ( currentEvent==null ) currentScope = tmodel.root.event.scope; else currentScope = currentEvent.scope; // update tree view of template hierarchy // compute path from root to currentST, create TreePath for tree widget List<EvalTemplateEvent> stack = Interpreter.getEvalTemplateEventStack(currentScope, true); //System.out.println("\nselect path="+stack); Object[] path = new Object[stack.size()]; int j = 0; for (EvalTemplateEvent s : stack) { path[j++] = new JTreeSTModel.Wrapper(s); } TreePath p = new TreePath(path); viewFrame.tree.setSelectionPath(p); viewFrame.tree.scrollPathToVisible(p); updateCurrentST(viewFrame); } finally { updateDepth.decrementAndGet(); } } }; viewFrame.output.addCaretListener(caretListenerLabel); // ADD ERRORS if ( errors==null || errors.size()==0 ) { viewFrame.errorScrollPane.setVisible(false); // don't show unless errors } else { final DefaultListModel errorListModel = new DefaultListModel(); for (STMessage msg : errors) { errorListModel.addElement(msg); } viewFrame.errorList.setModel(errorListModel); } viewFrame.errorList.addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { int depth = updateDepth.incrementAndGet(); try { if ( depth!=1 ) { return; } int minIndex = viewFrame.errorList.getMinSelectionIndex(); int maxIndex = viewFrame.errorList.getMaxSelectionIndex(); int i = minIndex; while ( i<=maxIndex ) { if ( viewFrame.errorList.isSelectedIndex(i) ) break; i++; } ListModel model = viewFrame.errorList.getModel(); STMessage msg = (STMessage)model.getElementAt(i); if ( msg instanceof STRuntimeMessage ) { STRuntimeMessage rmsg = (STRuntimeMessage)msg; Interval I = rmsg.self.impl.sourceMap[rmsg.ip]; currentEvent = null; currentScope = ((STRuntimeMessage)msg).scope; updateCurrentST(viewFrame); if ( I!=null ) { // highlight template highlight(viewFrame.template, I.a, I.b); } } } finally { updateDepth.decrementAndGet(); } } }); Border empty = BorderFactory.createEmptyBorder(); viewFrame.treeContentSplitPane.setBorder(empty); viewFrame.outputTemplateSplitPane.setBorder(empty); viewFrame.templateBytecodeTraceTabPanel.setBorder(empty); viewFrame.treeAttributesSplitPane.setBorder(empty); viewFrame.treeContentSplitPane.setOneTouchExpandable(true); viewFrame.outputTemplateSplitPane.setOneTouchExpandable(true); viewFrame.treeContentSplitPane.setDividerSize(10); viewFrame.outputTemplateSplitPane.setDividerSize(8); viewFrame.treeContentSplitPane.setContinuousLayout(true); viewFrame.treeAttributesSplitPane.setContinuousLayout(true); viewFrame.outputTemplateSplitPane.setContinuousLayout(true); viewFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); viewFrame.pack(); viewFrame.setSize(900, 700); setText(viewFrame.output, output); setText(viewFrame.template, currentScope.st.impl.template); setText(viewFrame.bytecode, currentScope.st.impl.disasm()); setText(viewFrame.trace, Misc.join(trace.iterator(), "\n")); viewFrame.setVisible(true); } public void waitForClose() throws InterruptedException { final Object lock = new Object(); Thread t = new Thread() { @Override public void run() { synchronized ( lock ) { while ( viewFrame.isVisible() ) { try { lock.wait(); } catch (InterruptedException e) { } } } } }; t.start(); viewFrame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent arg0) { synchronized ( lock ) { viewFrame.setVisible(false); lock.notify(); } } }); t.join(); } private void updateCurrentST(STViewFrame m) { // System.out.println("updateCurrentST(): currentScope.st="+currentScope.st); // update all views according to currentScope.st updateStack(currentScope, m); // STACK updateAttributes(currentScope, m); // ATTRIBUTES setText(m.bytecode, currentScope.st.impl.disasm()); // BYTECODE DIS. setText(m.template, currentScope.st.impl.template); // TEMPLATE SRC JTreeASTModel astModel = new JTreeASTModel(new CommonTreeAdaptor(), currentScope.st.impl.ast); viewFrame.ast.setModel(astModel); // highlight output text and, if {...} subtemplate, region in ST src // get last event for currentScope.st; it's the event that captures ST eval if ( currentEvent instanceof EvalExprEvent ) { EvalExprEvent exprEvent = (EvalExprEvent)currentEvent; highlight(m.output, exprEvent.outputStartChar, exprEvent.outputStopChar); highlight(m.template, exprEvent.exprStartChar, exprEvent.exprStopChar); } else { EvalTemplateEvent templateEvent; if ( currentEvent instanceof EvalTemplateEvent ) { templateEvent = (EvalTemplateEvent)currentEvent; } else { List<InterpEvent> events = currentScope.events; templateEvent = (EvalTemplateEvent)events.get(events.size()-1); } if ( templateEvent!=null ) { highlight(m.output, templateEvent.outputStartChar, templateEvent.outputStopChar); } if ( currentScope.st.isAnonSubtemplate() ) { Interval r = currentScope.st.impl.getTemplateRange(); //System.out.println("currentScope.st src range="+r); //m.template.moveCaretPosition(r.a); highlight(m.template, r.a, r.b); } } } protected void setText(JEditorPane component, String text) { List<Integer> windowsLineEndingsList = new ArrayList<Integer>(); for (int i = 0; i<text.length(); i+=2) { i = text.indexOf("\r\n", i); if ( i<0 ) { break; } windowsLineEndingsList.add(i); } int[] windowsLineEndings = new int[windowsLineEndingsList.size()]; for (int i = 0; i<windowsLineEndingsList.size(); i++) { windowsLineEndings[i] = windowsLineEndingsList.get(i); } component.setText(text); component.getDocument().putProperty(WINDOWS_LINE_ENDINGS, windowsLineEndings); } protected int toComponentPosition(JTextComponent component, int position) { int[] windowsLineEndings = (int[])component.getDocument().getProperty(WINDOWS_LINE_ENDINGS); if ( windowsLineEndings==null || windowsLineEndings.length==0 ) { return position; } int index = Arrays.binarySearch(windowsLineEndings, position); if ( index>=0 ) { return position-index; } return position-(-index-1); } protected int toEventPosition(JTextComponent component, int position) { int result = position; while ( toComponentPosition(component, result)< position ) { result++; } return result; } protected final void highlight(JTextComponent comp, int i, int j) { highlight(comp, i, j, true); } protected void highlight(JTextComponent comp, int i, int j, boolean scroll) { Highlighter highlighter = comp.getHighlighter(); highlighter.removeAllHighlights(); try { i = toComponentPosition(comp, i); j = toComponentPosition(comp, j); highlighter.addHighlight(i, j+1, DefaultHighlighter.DefaultPainter); if ( scroll ) { if ( comp.getCaretPosition()< i || comp.getCaretPosition()>j ) { comp.moveCaretPosition(i); comp.scrollRectToVisible(comp.modelToView(i)); } } } catch (BadLocationException ble) { errMgr.internalError(tmodel.root.event.scope.st, "bad highlight location", ble); } } protected void updateAttributes(final InstanceScope scope, final STViewFrame m) { //System.out.println("updateAttributes: "+Interpreter.getEnclosingInstanceStackString(scope) ); m.attributes.setModel(new JTreeScopeStackModel(scope)); m.attributes.setRootVisible(false); m.attributes.setShowsRootHandles(true); //System.out.println("add events="+ st.addAttrEvents); // ST st = scope.st; // final DefaultListModel attrModel = new DefaultListModel(); // final Map<String,Object> attrs = st.getAttributes(); // if ( attrs!=null ) { // for (String a : attrs.keySet()) { // if ( st.debugState!=null && st.debugState.addAttrEvents!=null ) { // List<AddAttributeEvent> events = st.debugState.addAttrEvents.get(a); // StringBuilder locations = new StringBuilder(); // int i = 0; // if ( events!=null ) { // for (AddAttributeEvent ae : events) { // if ( i>0 ) locations.append(", "); // locations.append(ae.getFileName()+":"+ae.getLine()); // i++; // } // } // if ( locations.length()>0 ) { // attrModel.addElement(a+" = "+attrs.get(a)+" @ "+locations.toString()); // } // else { // attrModel.addElement(a+" = "+attrs.get(a)); // } // } // else { // attrModel.addElement(a+" = "+attrs.get(a)); // } // } // } // m.attributes.setModel(attrModel); } protected void updateStack(InstanceScope scope, STViewFrame m) { List<ST> stack = Interpreter.getEnclosingInstanceStack(scope, true); m.setTitle("STViz - ["+ Misc.join(stack.iterator(), " ")+"]"); // // also do source stack // StackTraceElement[] trace = st.newSTEvent.stack.getStackTrace(); // StringWriter sw = new StringWriter(); // for (StackTraceElement e : trace) { // sw.write(e.toString()+"\n"); // } } public InterpEvent findEventAtOutputLocation(List<InterpEvent> events, int charIndex) { for (InterpEvent e : events) { if ( e.scope.earlyEval ) { continue; } if ( charIndex>= e.outputStartChar && charIndex<= e.outputStopChar ) return e; } return null; } public static void main(String[] args) throws IOException { // test rig if ( args.length>0 && args[0].equals("1") ) test1(); else if ( args.length>0 && args[0].equals("2") ) test2(); else if ( args.length>0 && args[0].equals("3") ) test3(); else if ( args.length>0 && args[0].equals("4") ) test4(); } public static void test1() throws IOException { // test rig String templates = "method(type,name,locals,args,stats) ::= <<\n"+"public <type> <name>(<args:{a| int <a>}; separator=\", \">) {\n"+" <if(locals)>int locals[<locals>];<endif>\n"+" <stats;separator=\"\\n\">\n"+"}\n"+">>\n"+"assign(a,b) ::= \"<a> = <b>;\"\n"+"return(x) ::= <<return <x>;>>\n"+"paren(x) ::= \"(<x>)\"\n"; String tmpdir = System.getProperty("java.io.tmpdir"); writeFile(tmpdir, "t.stg", templates); STGroup group = new STGroupFile(tmpdir+"/"+"t.stg"); ST st = group.getInstanceOf("method"); st.impl.dump(); st.add("type", "float"); st.add("name", "foo"); st.add("locals", 3); st.add("args", new String[] {"x", "y", "z"}); ST s1 = group.getInstanceOf("assign"); ST paren = group.getInstanceOf("paren"); paren.add("x", "x"); s1.add("a", paren); s1.add("b", "y"); ST s2 = group.getInstanceOf("assign"); s2.add("a", "y"); s2.add("b", "z"); ST s3 = group.getInstanceOf("return"); s3.add("x", "3.14159"); st.add("stats", s1); st.add("stats", s2); st.add("stats", s3); STViz viz = st.inspect(); System.out.println(st.render()); // should not mess up ST event lists } public static void test2() throws IOException { // test rig String templates = "t1(q1=\"Some\\nText\") ::= <<\n"+ "<q1>\n" +">>\n"+"\n"+"t2(p1) ::= <<\n"+ "<p1>\n"+ ">>\n"+ "\n"+ "main() ::= <<\n" +"START-<t1()>-END\n"+"\n"+"START-<t2(p1=\"Some\\nText\")>-END\n"+">>\n"; String tmpdir = System.getProperty("java.io.tmpdir"); writeFile(tmpdir, "t.stg", templates); STGroup group = new STGroupFile(tmpdir+"/"+"t.stg"); ST st = group.getInstanceOf("main"); STViz viz = st.inspect(); } public static void test3() throws IOException { String templates = "main() ::= <<\n"+ "Foo: <{bar};format=\"lower\">\n" +">>\n"; String tmpdir = System.getProperty("java.io.tmpdir"); writeFile(tmpdir, "t.stg", templates); STGroup group = new STGroupFile(tmpdir+"/"+"t.stg"); ST st = group.getInstanceOf("main"); st.inspect(); } public static void test4() throws IOException { String templates = "main(t) ::= <<\n"+"hi: <t>\n"+">>\n"+"foo(x,y={hi}) ::= \"<bar(x,y)>\"\n"+ "bar(x,y) ::= << <y> >>\n" +"ignore(m) ::= \"<m>\"\n"; STGroup group = new STGroupString(templates); ST st = group.getInstanceOf("main"); ST foo = group.getInstanceOf("foo"); st.add("t", foo); ST ignore = group.getInstanceOf("ignore"); ignore.add("m", foo); // embed foo twice! st.inspect(); st.render(); } public static void writeFile(String dir, String fileName, String content) { try { File f = new File(dir, fileName); if ( !f.getParentFile().exists() ) f.getParentFile().mkdirs(); FileWriter w = new FileWriter(f); BufferedWriter bw = new BufferedWriter(w); bw.write(content); bw.close(); w.close(); } catch (IOException ioe) { System.err.println("can't write file"); ioe.printStackTrace(System.err); } } }