Tuesday, November 19, 2013

Debugger 7: Source lookup

Having stepping and breakpoint support is nice, but the user needs some visual feedback where the debugger currently suspended. The source code should be opened in an editor and the current line should be highlighted.

The debug framework comes with a dedicated solution that looks for source files in dedicated project folders. If you have a project setup where you define source lookup folders this is the way to go for you. Another option is to implement source lookup completely from scratch. In our case the latter option is the easier one.
  1. A fictional interpreter
  2. The launch framework
  3. A tale of debuggers, processes and threads
  4. Stepping, suspending and other actions
  5. UI breakpoint integration
  6. Debugger breakpoint integration
  7. Display current source code - source lookup
  8. Run to line support
  9. Displaying variables
  10. Displaying memory areas

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.


Step 1: The big picture

When a debug thread starts processing, it typically calls files, methods and functions. Such elements denote the call stack. The debugger framework uses this stack to resolve the current location within the source code. Each stack element may have a dedicated source location (editor content + selection within editor) attached to it.

An ISourceLocator is attached to a launch extension, typically by using the org.eclipse.debug.core.sourceLocators extension point. This locator converts an IStackFrame to a source element by means of getSourceElement(IStackFrame stackFrame). This element is used to retrieve an IEditorInput and an editorID from the debug model. Now an editor can be opened. The line selection is retrieved from the stack frame.


Step 2: Adding StackFrames

The IStackFrame implementation we use is really simple: we only allow to store the current line number and to retrieve the source file. As we have only one source file available, we store it directly within our debug target.

Create a new class TextStackFrame:
package com.codeandme.textinterpreter.debugger.model;

import org.eclipse.core.resources.IFile;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IRegisterGroup;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IVariable;

public class TextStackFrame extends TextDebugElement implements IStackFrame {

 private final IThread mThread;
 private int mLineNumber = 1;

 public TextStackFrame(IDebugTarget target, IThread thread) {
  super(target);
  mThread = thread;
 }

 @Override
 public IThread getThread() {
  return mThread;
 }

 @Override
 public IVariable[] getVariables() {
  return new IVariable[0];
 }

 @Override
 public boolean hasVariables() {
  return getVariables().length > 0;
 }

 @Override
 public int getLineNumber() {
  return mLineNumber;
 }

 @Override
 public int getCharStart() {
  return -1;
 }

 @Override
 public int getCharEnd() {
  return -1;
 }

 @Override
 public String getName() {
  return getSourceFile().getName() + ", line " + getLineNumber();
 }

 @Override
 public IRegisterGroup[] getRegisterGroups() {
  return new IRegisterGroup[0];
 }

 @Override
 public boolean hasRegisterGroups() {
  return getRegisterGroups().length > 0;
 }

 public void setLineNumber(int lineNumber) {
  mLineNumber = lineNumber;
 }

 public IFile getSourceFile() {
  return (getDebugTarget()).getFile();
 }
}
For a line oriented language getLineNumber() is important as it is used for the line marker when our code is suspended. Make sure both getCharStart() and getCharEnd() return -1 in that case. If you want to mark a section within a line, implement getCharStart() and getCharEnd().

As we do not have multiple source files, function calls or similar things we can stick to one static StackFrame for the whole debugging session. It is registered and updated from the TextDebugTarget:
package com.codeandme.textinterpreter.debugger.model;

public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor {

 private final IFile mFile;

 @Override
 public void handleEvent(final IDebugEvent event) {

  if (!isDisconnected()) {
   System.out.println("Target.handleEvent() " + event);

   if (event instanceof DebuggerStartedEvent) {
    // create debug thread
    TextThread thread = new TextThread(this);
    mThreads.add(thread);
    thread.fireCreationEvent();

    // create stack frame
    TextStackFrame stackFrame = new TextStackFrame(this, thread);
    thread.addStackFrame(stackFrame);
    stackFrame.fireCreationEvent();

    // add breakpoint listener
    DebugPlugin.getDefault().getBreakpointManager().addBreakpointListener(this);

    // attach deferred breakpoints to debugger
    IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager().getBreakpoints(getModelIdentifier());
    for (IBreakpoint breakpoint : breakpoints)
     breakpointAdded(breakpoint);

    // resume execution after setting breakpoints
    resume();

   } else if (event instanceof SuspendedEvent) {
    // breakpoint hit
    setState(State.SUSPENDED);

    getThreads()[0].getTopStackFrame().setLineNumber(((SuspendedEvent) event).getLineNumber());
    getThreads()[0].getTopStackFrame().fireChangeEvent(DebugEvent.CONTENT);

    // inform eclipse of suspended state
    fireSuspendEvent(DebugEvent.CLIENT_REQUEST);
   }
  }
 }
}
 It is important that the StackFrame is registered before the debug UI suspends for the first time. If this is not the case the Debug view will not fully expand all its nodes and therefore not display the suspended StackFrame and the according source file in the first place. A user would have to manually expand and select the StackFrame.

Step 3: Resolving source files

Resolving of source elements is handled by a SourceLocator. We have to register a new one in com.codeandme.textinterpreter.debugger/plugin.xml:

The implementation of TextSourceLocator is straight forward, we only need to deal with getSourceElement():
package com.codeandme.textinterpreter.debugger.model;

public class TextSourceLocator implements IPersistableSourceLocator {

 @Override
 public Object getSourceElement(IStackFrame stackFrame) {
  if (stackFrame instanceof TextStackFrame)
   return ((TextStackFrame) stackFrame).getSourceFile();

  return null;
 }
}
Having  a source locator we now may register it to the launch configuration. Open com.codeandme.textinterpreter.ui/plugin.xml, navigate to the Text Interpreter launchConfigurationType and set sourceLocatorId to com.codeandme.textinterpreter.debugger.sourceLocator.

Step 4: Adding editor support

The final step is to define the editor to be used. This is handled by TextDebugModelPresentation:
package com.codeandme.textinterpreter.debugger.model;

public class TextDebugModelPresentation implements IDebugModelPresentation {

 @Override
 public IEditorInput getEditorInput(Object element) {
  if (element instanceof IFile)
   return new FileEditorInput((IFile) element);

  return null;
 }

 @Override
 public String getEditorId(IEditorInput input, Object element) {
  if (element instanceof IFile)
   return PlatformUI.getWorkbench().getEditorRegistry().getDefaultEditor(((IFile) element).getName()).getId();

  return null;
 }
}
Using the default editor for the source file is a good choice as it is up to the user to define a dedicated editor. We might add some fallback code to open the default text editor instead of returning null in an error case.

Code lookup should work by now. Give it a try and step through some sample scripts.

No comments:

Post a Comment