How does DevTools show the functions popup?

Member List / Symbol List / Function List

When cmd+shift+o or cmd+shift+p is pressed the callback SourcesView._showOutlineDialog is fired.

_showOutlineDialog: function(event) {
  var uiSourceCode = this._editorContainer.currentFile();
  WebInspector.JavaScriptOutlineDialog.show(this, uiSourceCode, this.showSourceLocation);
}

the uiSourceCode property is interesting. It’s type is WebInspector.UiSourceCode properties include:

  • _project: => WebInspector.Project
  • _parentPath: => ‘’
  • _name
  • _originURL => “http://localhost:3000/bundle.js”
  • _url => “http://localhost:3000/bundle.js”
  • _contentType => ResourceType … basically just “Script”
  • _requestContentCallbacks
  • history => actions
  • _listeners
  • _contentLoaded
  • _content => “(function e(t,n,r){fun…” basically all the text
  • Symbol(target) => I think it’s used for resolving lookups
  • Symbol(NetworkContentType) => I think it’s used for resolving lookups

JavaScriptOutlineDialog

The JavaScriptOutlineDialog is the object responsible for showing the list of functions.

Of course, finding the function definitions for an arbitrary string of javascript is not easy work. This is why the dialog delegates to a web worker called _outlineWorker.

WebInspector.JavaScriptOutlineDialog = function(uiSourceCode, selectItemCallback) {
    WebInspector.SelectionDialogContentProvider.call(this);

    this._functionItems = [];
    this._selectItemCallback = selectItemCallback;

    this._outlineWorker = new WorkerRuntime.Worker("script_formatter_worker");
    this._outlineWorker.onmessage = this._didBuildOutlineChunk.bind(this);
    this._outlineWorker.postMessage({
      method: "javaScriptOutline",
      params: { content: uiSourceCode.workingCopy() }
    });
}

script_formatter_worker

The worker is setup here script_formatter_worker and will respond to messages with a method javaScriptOutline. When the worker is done it’ll call the dialog’s _didBuildOutlineChunk callback.

I’m not going to go into too much depth on how the worker builds the list of function identifiers, but here’s a quick sneak peak. The important things to note are:

  • it uses a javascript tokenizer
  • it tokenizes line by line
  • the tokenizer calls a processToken callback which looks for functions
FormatterWorker.javaScriptOutline = function(params)
{
    var lines = params.content.split("\n");
    var tokenizer = FormatterWorker.createTokenizer("text/javascript");

    for (var i = 0; i < lines.length; ++i) {
        var line = lines[i];
        tokenizer(line, processToken);
    }

    // look for either:
    // A named function: "function f...".
    // Anonymous function assigned to an identifier: "...f = function..."
    // or "funcName: function...".
    function processToken(tokenValue, tokenType, column, newColumn) {
       if (isJavaScriptIdentifier(tokenType)) {
            return { line: i, column: column, name: tokenValue };
        } else if (tokenType === "keyword" && tokenValue === "function" &&
            (previousToken === "=" || previousToken === ":")) {
          return { line: i, column: column, name: previousIdentifier };
        }
    }

    postMessage({ chunk: outlineChunk, isLastChunk: true });
}

_didBuildOutlineChunk

When the worker is done with some of the work, it calls the javascript dialog’s callback _didBuildOutlineChunk.

the callback is passed in an event, with a data param that has a list of chunks. The chunks are our function definitions!

we do not have function bodies here, which is kind of a bummer.

[
  {
    "arguments": "(fragment, forcePushState)",
    "column": 15,
    "line": 75,
    "name": "getFragment"
  }
]

Chunk is used liberally here because the worker is smart enough to batch it’s work. If for example, it’s parsing a massive javascript file it might do the first thousand lines, return the function definitions and then continue.

Filtering the list

When the list comes back, there are three things to consider: filtering, sorting, updating the list.

  • filtering will be done in the search field on the top
  • scoring is a complicated algorithm, which determines which functions to show first
_filterItems: function() {
    this._query = this.rewriteQuery(this._promptElement.value.trim());
    scoreItems.call(this, 0);
}

19 Mar 2015