Firefox DevTools - Showing a File

How are sources shown in the debugger?

In this post, we're going to trace the steps for showing a source file in the debugger. To do this, we should first look at two components of the debugger: the Editor and the Sources pane. The Editor is the component responsible for showing a source's file. In the case of Firefox, it's a CodeMirror editor. Chrome DevTools and many other popular tools use CodeMirror. The Sources component is in the left sidebar and shows a file list or tree of all the sources.


The Beginning: Setting up the Sources Component

The Sources component is a widget that's setup and managed by the SourcesView.

this.widget = new SideMenuWidget(document.getElementById("sources"), {
  contextMenu: document.getElementById("debuggerSourcesContextMenu"),
  showArrows: true
});

The click handler for selecting a source file is bound to the widget in the SourcesView initialization logic.

this.widget.addEventListener("select", this._onSourceSelect, false);

The Click: What happens when I select a source?

When a source is selected in the sidebar pane the _onSourceSelect handler is called with a sourceItem parameter. The debugger follows Redux patterns, so our handler can be very simple: essentially invoke the select source action.

_onSourceSelect: function({ detail: sourceItem }) {
  this.actions.selectSource(sourceItem.attachment);
},

The sourceItem.attachment property is a serializable representation of our source. It's important that it has an actor id and other useful data about being blackboxed, pretty printed, etc.

{
  "label": "/source/src/books/index/route.js",
  "group": "",
  "checkboxState": true,
  "checkboxTooltip": "Toggle black boxing",
  "source": {
    "actor": "server1.conn2.child1/60",
    "generatedUrl": "http://www.marionettewires.com/bundle.js",
    "url": "/source/src/books/index/route.js",
    "isBlackBoxed": false,
    "isPrettyPrinted": false,
    "isSourceMapped": true,
    "introductionUrl": null,
    "introductionType": "scriptElement"
  }
}

Action Time: Lets do something real

The selectSource action does two things for us, it starts loading the source text and it does the UI work of showing the selected source.

When the debugger is showing a source file for the first time, the process looks something like this. Start fetching the source, and while we're waiting show a loading message. When we get the source back from the backend, go ahead and show it. There two corresponding events here: "source-selected" and "source-text-loaded". We'll get to these events in a minute.

function selectSource(source, opts) {
  return (dispatch, getState) => {
    source = getSource(getState(), source.actor);
    dispatch(loadSourceText(source));
    dispatch(selectSource(source));
  };
}

The Debugger View does the heavy lifting

The DebuggerView handles both events ("source-selected" and "source-text-loaded"). They're setup in this onReducerEvents.

onReducerEvents(this.controller, {
  "source-text-loaded": this.renderSourceText,
  "source-selected": this.renderSourceText
}, this);

If you're wondering what the DebuggerView is responsible for, the answer is a lot. Here is a list of components that are initialized when the DebuggerView starts up.

Panes
Editor
Toolbar
Options
Filtering
StackFrames
Workers
Sources
WatchExpressions
EventListeners
GlobalSearch
VariablesView

Render Time: How are sources shown?

Both events "source-text-loaded" and "source-selected" are handled by the same came callback renderSourceText, which boils down to four different scenarios for the source:

  1. it's blackboxed
  2. it's loading
  3. something went wrong
  4. it's fine, and we should show it
function renderSourceText (source, textInfo) {
  if (source.isBlackBoxed) {
    this.showBlackBoxMessage();
  } else if (textInfo.isLoading) {
    this.showLoadingMessage();
  } else if (textInfo.error) {
    this.showErrorMessage(textInfo.error)
  } else {
    this.showSourceFile(source, textInfo)
  }
}

In all four cases, we have some text that is ready to be shown in the editor. We show it by first creating a CodeMirror document for the source if one doesn't exist, and then setting the text in the document. If we already have a document for the source, we assume it's uptodate and show the document.

Overview

We've traced the codepath from clicking on a source in the sources sidebar to the text showing up in the editor. What have we learned:

1) There's one click handler that kicks everything off with a Redux Action 2) There's a Redux reducer that handles fetching the source and showing the source 3) The DebuggerView renders the text into the editor. It also does a hundred other things :)

16 Mar 2016