If you build your Xtext DSL using Xbase for your expressions and implementing a JvmModelInferrer
for the Java Mapping you get Debugging in Eclipse for free. But what about if your DSL is not using Xbase but maps to Java anyway? With the Tracing Code Generator in Xtext 2.12 and its debugging extensions in Xtext 2.13 and a few lines of Gluecode you can achieve this as well.
Setting up the Project, Grammar and Code Generator
We create a new Xtext project using the wizard and the default grammar.
grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals generate myDsl "http://www.xtext.org/example/mydsl/MyDsl" Model: greetings+=Greeting*; Greeting: 'Hello' name=ID '!';
We like to map an example model like
Hello World! Hello Debug! Hello Reader!
to this simple Java code
package demo; public class Greeter_xxxx { public static void main(String[] args) { System.out.println("World"); System.out.println("Debug"); System.out.println("Reader"); } }
A traditional code generator would look like
class MyDslGenerator extends AbstractGenerator { override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { for (model : resource.allContents.filter(Model).toIterable) { val name = resource.URI.trimFileExtension.lastSegment fsa.generateFile("demo/Greeter_" + name + ".java", ''' package demo; public class Greeter_«name» { public static void main(String[] args) { «FOR g : model.greetings» System.out.println("«g.name»"); «ENDFOR» } } ''') } } }
For debugging this is not sufficient. We have no information which element of the generated code maps back to which elements in the source model. Nor do we have information which parts of the generated code are interesting for debugging and which are not. This is where the Tracing Code Generation comes into place. It was introduced in Xtext 2.12 and extended with debugging features in Xtext 2.13 .
We first add a TracedAccessors
extension to our generator
@TracedAccessors(MyDslFactory) static class MyDslTraceExtensions { } @Inject extension MyDslTraceExtensions
That gives us convenience accessors and methods like _name
and _name(useForDebugging)
inside our code generator which allow us to generate traced files.
override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { for (model : resource.allContents.filter(Model).toIterable) { val name = resource.URI.trimFileExtension.lastSegment fsa.generateTracedFile("demo/Greeter_" + name + ".java", model, ''' package demo; public class Greeter_«name» { public static void main(String[] args) { «FOR g : model.greetings» System.out.println("«g._name(true)»"); «ENDFOR» } } ''') } }
If we now start a runtime eclipse, additionally to the java file there will be a .Greeter_xxxx.java._trace
file with following content (visualized)
Regions are surrounded by [N[ ... ]N]. Regions on the left and right with the same N are associated. ----------- Greeter_xxxx.java ----------- | -- demo/xxxx.mydsl --- [1[package demo; | [1[Hello [2[World]2]! | Hello [3[Debug]3]! public class Greeter_xxxx { | Hello [4[Reader]4]!]1] public static void main(String[] args) { | System.out.println("[2[World]2]"); | System.out.println("[3[Debug]3]"); | System.out.println("[4[Reader]4]"); | } | } | ]1] | ------------------------------------------------------------------ <N>: <isDebug> <offset>-<length> <RegionJavaClass> -> <LocationJavaClass>[<offset>,<length>,<uri>] 1: 000-184 DebugTraceBasedRegion -> LocationData[0,39,demo/xxxx.mydsl] { 2: D 107-005 DebugTraceBasedRegion -> LocationData[6,5,demo/xxxx.mydsl] 3: D 138-005 DebugTraceBasedRegion -> LocationData[19,5,demo/xxxx.mydsl] 4: D 169-006 DebugTraceBasedRegion -> LocationData[32,6,demo/xxxx.mydsl] 1: }
This file will be picked up by the Xtext builder infrastructure and weaved up into the class file produced by Eclipse JDT (DebugSourceInstallingCompilationParticipant
). If we now start debugging, we can already "step into" our DSL files but be cannot set breakpoints yet. How to do that is described in the following section.
Writing the Gluecode
There are only a few thing that need to be done. First we create our own subclass of XtextEditor
import org.eclipse.core.runtime.CoreException; import org.eclipse.ui.IEditorInput; import org.eclipse.xtext.ui.editor.XtextEditor; import org.eclipse.xtext.xbase.ui.editor.XbaseEditorInputRedirector; import com.google.inject.Inject; public class MyDslEditor extends XtextEditor { @Inject private XbaseEditorInputRedirector editorInputRedirector; @Override protected void doSetInput(IEditorInput input) throws CoreException { try { IEditorInput inputToUse = editorInputRedirector.findOriginalSource(input); super.doSetInput(inputToUse); return; } catch (CoreException e) { // ignore } super.doSetInput(input); } }
and bind it in MyDslUiModule
class MyDslUiModule extends AbstractMyDslUiModule { def Class<? extends XtextEditor> bindXtextEditor() { MyDslEditor } }
Then we need to implement and bind an IStratumBreakpointSupport
to tell Xtext where breakpoints are allowed.
package org.xtext.example.mydsl; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.debug.IStratumBreakpointSupport; import org.eclipse.xtext.nodemodel.ICompositeNode; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.parser.IParseResult; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.util.ITextRegionWithLineInformation; import org.xtext.example.mydsl.myDsl.Greeting; public class MyDslStratumBreakpointSupport implements IStratumBreakpointSupport { @Override public boolean isValidLineForBreakPoint(XtextResource resource, int line) { IParseResult parseResult = resource.getParseResult(); if (parseResult == null) return false; ICompositeNode node = parseResult.getRootNode(); return isValidLineForBreakpoint(node, line); } protected boolean isValidLineForBreakpoint(ICompositeNode node, int line) { for (INode n : node.getChildren()) { ITextRegionWithLineInformation textRegion = n.getTextRegionWithLineInformation(); if (textRegion.getLineNumber()<= line && textRegion.getEndLineNumber() >= line) { EObject eObject = n.getSemanticElement(); if (eObject instanceof Greeting) { return true; } if (n instanceof ICompositeNode && isValidLineForBreakpoint((ICompositeNode) n, line)) { return true; } } if (textRegion.getLineNumber() > line) { return false; } } return false; } }
class MyDslRuntimeModule extends AbstractMyDslRuntimeModule { def Class<? extends IStratumBreakpointSupport> bindIStratumBreakpointSupport() { return MyDslStratumBreakpointSupport; } }
Then we need to implement an IToggleBreakpointsTargetExtension
to tell Eclipse how to toggle the breakpoint. To make it easy we subclass the existing Xbase implementation
package org.xtext.example.mydsl.ui; import org.eclipse.xtext.builder.smap.StratumBreakpointAdapterFactory; import org.eclipse.xtext.resource.XtextResource; public class MyDslStratumBreakpointAdapterFactory extends StratumBreakpointAdapterFactory { @Override protected String getClassNamePattern(XtextResource state) { String name = "demo.Greeter_"+state.getURI().trimFileExtension().lastSegment()+"*"; return name; } public Object getAdapter(Object adaptableObject, Class adapterType) { if (adaptableObject instanceof MyDslEditor) { return this; } return null; } }
Finally we do some wiring inside the plugin.xml
<extension point="org.eclipse.core.runtime.adapters"> <factory class="org.xtext.example.mydsl.ui.MyDslExecutableExtensionFactory:org.xtext.example.mydsl.ui.MyDslStratumBreakpointAdapterFactory" adaptableType="org.xtext.example.mydsl.ui.MyDslEditor"> <adapter type="org.eclipse.debug.ui.actions.IToggleBreakpointsTarget"/> </factory> </extension> <extension point="org.eclipse.ui.editorActions"> <editorContribution targetID="org.xtext.example.mydsl.MyDsl" id="org.xtext.example.mydsl.MyDsl.rulerActions"> <action label="Not Used" class="org.xtext.example.mydsl.ui.MyDslExecutableExtensionFactory:org.eclipse.debug.ui.actions.RulerToggleBreakpointActionDelegate" style="push" actionID="RulerDoubleClick" id="org.xtext.example.mydsl.MyDsl.doubleClickBreakpointAction"/> </editorContribution> </extension> <extension point="org.eclipse.ui.popupMenus"> <viewerContribution targetID="org.xtext.example.mydsl.MyDsl.RulerContext" id="org.xtext.example.mydsl.MyDsl.RulerPopupActions"> <action label="Toggle Breakpoint" class="org.xtext.example.mydsl.ui.MyDslExecutableExtensionFactory:org.eclipse.debug.ui.actions.RulerToggleBreakpointActionDelegate" menubarPath="debug" id="org.xtext.example.mydsl.MyDsl.rulerContextMenu.toggleBreakpointAction"> </action> <action label="Not used" class="org.xtext.example.mydsl.ui.MyDslExecutableExtensionFactory:org.eclipse.debug.ui.actions.RulerEnableDisableBreakpointActionDelegate" menubarPath="debug" id="org.xtext.example.mydsl.MyDsl.rulerContextMenu.enableDisableBreakpointAction"> </action> </viewerContribution> </extension>
That's all we need to do.
Use the Debugger
If we start a new runtime Eclipse we can now set Breakpoints and have fun debugging.
You can find the example code here.
Comments