welcome: please sign in
location: MinibufferReadWalkthrough

Minibuffer.read Walkthrough

This article is a practical walkthrough of coroutine usage. To help understand how to use coroutines, and also how reading from the minibuffer works, we are going to make use of the simple example from WritingCommands, the echo-message command. We'll step through what happens from the point that the user calls echo-message. Here is the source code of that command.

function echo_message (window, message) {
    window.minibuffer.message(message);
}

interactive("echo-message",
    "echo a user-supplied message in the minibuffer",
    function (I) {
        echo_message(
            I.window,
            (yield I.minibuffer.read($prompt = "message: ")));
    });

An event occurs.

In a simple event-driven program, events come in from devices like keyboard and mouse to an underlying platform (in our case Xulrunner), which calls event handlers in the application, which in turn do interesting stuff. Event handlers run in the same order as the events that came in to trigger them, and one event handler cannot start until the previous one has completed.

Conkeror's coroutine library lets us put a small twist on this scenario. We can suspend a handler in mid-computation, and process other events, then resume the computation we suspended at a later time, from the handler of a different event.

To get ourselves in the right frame of mind for this, think of a complete computation, as triggered by a single event, as a "thread". These are not real threads in the sense of multi-threaded programming, but they are an emulation of that effect. So let us begin to step through the computation of a thread, kicked off by a keypress that runs our echo-message command.

An event occurs, which causes Xulrunner to call Conkeror's keypress_handler, which looks up what command to run, and calls call_interactively with that command name. If the handler of that command, or anything that it calls in turn uses the javascript keyword yield, that means that the handler is a generator. call_interactively uses Conkeror's coroutine library to run that generator in a special loop that gives us the power to suspend execution (cue creepy theremin effects). Any yield expression throughout the entire computation, unless dealt with locally, will fall through to the special loop in the coroutine library, which will in turn take action based on the thing yielded.

You can see how call_interactively uses co_call to wrap the generator in the special loop.

// modules/interactive.js: call_interactively
...
    var result = handler(I);
    if (is_coroutine(result)) {
        co_call(function() {
            try {
                yield result;
            } catch (e) {
                handle_interactive_error(window, e);
            }
        }());
    }
...

If we strip away all the error-handling in the code above, we would have something very simple:

co_call(handler(I));

The handler for echo-message will now run until any part of it requests to be suspended.

// command-handler of echo-message
function (I) {
    echo_message(
        I.window,
        (yield I.minibuffer.read($prompt = "message: ")));
}

The first things that happen in our handler are the computation of the arguments to pass to echo_message. The interesting one looks like this:

(yield I.minibuffer.read($prompt = "message: "))

I.minibuffer.read is another coroutine. This syntax is how to have one coroutine (our command handler) call another coroutine synchronously. Our command handler will be paused until I.minibuffer.read returns a value. So we have finally traced evaluation to the point where something is about to happen in the minibuffer!

minibuffer.read looks like this:

// modules/minibuffer-read.js
function () {
    var s = new text_entry_minibuffer_state((yield CONTINUATION),
                                            forward_keywords(arguments));
    this.push_state(s);
    var result = yield SUSPEND;
    yield co_return(result);
};

You can ignore the forward_keywords(arguments) for now—suffice it to say that it sends our $prompt = "message: " into the new text_entry_minibuffer_state so that deeper levels of minibuffer code can put up the prompt.

There is something interesting going on with that (yield CONTINUATION) and the yield SUSPEND. These two expressions can be viewed as a pair. We are going to tell the special loop to suspend this thread of computation. But first we need a way to resume the thread later. (yield CONTINUATION) is a request to the special loop to send us a kind of handle for the current thread—in common parlance, a continuation. We need to stash this handle away somewhere so that we can use it later, from a different thread, to resume the computation. We pass this continuation into the constructor of text_entry_minibuffer_state, which stashes it away.

Ever notice how in Conkeror you can be in a minibuffer prompt, say after hitting C-x C-f (find-url-new-buffer), and you can then hit C-h k to find out what some key does—the minibuffer changes to a prompt for a keypress, and after you hit a key, it goes back to the prompt you were in before, asking for an URL. This kind of recursive use of the minibuffer is possible because the minibuffer keeps track of a stack of states. The prompt that C-x C-f put up was one state, and then when you hit C-h k, another state got pushed onto the stack. When that state was finished collecting a keypress, it was disposed, and the previous state was restored: back to that URL prompt.

In the code listing above, we see that minibuffer.read creates a new text_entry_minibuffer_state, and then pushes that state onto the stack of states I mentioned. (The variable this refers to the minibuffer.) So the minibuffer is now in a state displaying a prompt, and a text field ready to be typed into. But as long as our javascript code is busy doing something, no events, such as keypresses can be processed. Once minibuffer.read has set up the minibuffer for input, it needs to step aside so that lower levels of code in Xulrunner can respond to key presses.

Which brings us to the very next line:

yield SUSPEND;

When we yield the value SUSPEND to the special loop, the special loop performs a yield of its own, which suspends the special loop so that the platform (Xulrunner) can process more events. Our computation still exists in memory, and we were clever enough to stash a reference to it with (yield CONTINUATION), but it isn't doing anything. The thread that began with that initial event is now in a deep sleep. But Conkeror is now in a state with a minibuffer input window open and focused. And Xulrunner can listen to events, and send them to Conkeror's keyboard system for interactive processing.

Typing occurs.

The user types some text, and then hits return. In the minibuffer, return is bound to the command exit-minibuffer. We'll put an ellided version of the command's source code here so you can see the interesting bits:

// modules/minibuffer-read.js
function exit_minibuffer (window) {
    var m = window.minibuffer;
    var s = m.current_state;
    // ...
    var val = m._input_text;
    // ...
    var cont = s.continuation;
    delete s.continuation;
    m.pop_state();
    if (cont) {
       // ...
            cont(val);
    }
}
interactive("exit-minibuffer", null,
    function (I) { exit_minibuffer(I.window); });

First, exit_minibuffer gets the current state object from the minibuffer, and val, the text that has been typed into the minibuffer. That s.continuation is where text_entry_minibuffer_state stashed the object we got earlier from (yield CONTINUATION). This is our handle to ressurect the command that we were in process of running. The line delete s.continuation is a bit of manual garbage-collecting necessary when working with coroutine references in javascript. The line that brings our command back to life is this one:

cont(val);

This resumes the coroutine thread of our command. So we are back to:

// modules/minibuffer-read.js
function () {
    var s = new text_entry_minibuffer_state((yield CONTINUATION),
                                            forward_keywords(arguments));
    this.push_state(s);
    var result = yield SUSPEND;
    yield co_return(result);
};

The yield SUSPEND now returns, evaluating to the value passed to cont in exit_minibuffer. That value is the text that the user entered into the minibuffer.

The last line of minibuffer.read is yield co_return(result). co_return creates an object of a particular type, that, when the special loop sees one of, it closes the coroutine and sends the given value to the previous coroutine. Remember how we made a synchronous call to minibuffer.read? minibuffer.read has just told the special loop that it's done, and to send the result to the thing that called minibuffer.read. This brings us back to our command handler for echo-message, which has been paused at a yield expression all this time. But that yield expression now evaluates to the thing that minibuffer.read sent back via co_return, with the special loop acting as the go-between.

interactive("echo-message",
    "echo a user-supplied message in the minibuffer",
    function (I) {
        echo_message(
            I.window,
            (yield I.minibuffer.read($prompt = "message: ")));
    });

We now have all the information needed to call echo_message.

Summary

In summary, to understand minibuffer interaction, it helps to remember that Conkeror is an event-driven program. There was an original event at the beginning of this walk-through that resulted in a call to our echo-message command. The echo-message command handler made a synchronous call to the coroutine minibuffer.read, which stashed a handle to the current thread ((yield CONTINUATION)) and then suspended that thread (yield SUSPEND). One event-handler thus having run to completion, we arranged for a later event to call exit-minibuffer, which resumed our sleeping thread with the value it was waiting for (text from the minibuffer input field), thus allowing it to run to completion.

Conkeror.org: MinibufferReadWalkthrough (last edited 2013-01-06 16:09:50 by retroj)