Firefox DevTools - Showing a File

Firefox DevTools - Showing a File

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 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);
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"
  }
}
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 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:
  • it's blackboxed
  • it's loading
  • something went wrong
  • 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:
  • There's one click handler that kicks everything off with a Redux Action
  • There's a Redux reducer that handles fetching the source and showing the source
  • The DebuggerView renders the text into the editor. It also does a hundred other things :)