Vim Mode: Theory of Operation

This is the theory of operation of Leo’s vim mode, contained in leo/core/leoVim.py. It discusses everything you need to understand the code and to add new vim commands.

The big picture

Leo’s vim mode dispatches keystrokes sent to it from k.masterKeyHandler to key handlers.

Each key handler handles the incoming key and then calls either vc.accept(), vc.done(), vc.ignore() or vc.quit(). These methods tell k.masterKeyHandler whether vim-mode has completely handled the key. If so, k.masterKeyHandler simply returns. Otherwise, k.masterKeyHandler handles the key as usual.

A simple key handler

The handler for the G command moves or extends the cursor depending on vc.state:

def vim_G(vc):
    '''Put the cursor on the last character of the body pane.'''
    if vc.is_text_widget(vc.w):
        if vc.state == 'visual':
            vc.do('end-of-buffer-extend-selection')
        else:
            vc.do('end-of-buffer')
        vc.done()
    else:
        vc.quit()

On entry, the dispatcher has set vc.w to the widget with focus. First, the code ensures that this widget is a text widget. If so, the code uses the vc.do method, a thin wrapper for c.k.simulateCommand, to execute Leo commands by name.

A more complex key handler

The vc.vim_d method and its follow-on methods handle vim’s d commands.

The vc.vis_d method handles the d keystroke that ends visual mode.

The following sections examine each piece of the code in detail. If you understand how it works you should know everything you need to write any other key handler.

vim_d

def vim_d(vc):
    '''
    N dd      delete N lines
    d{motion} delete the text that is moved over with {motion}
    '''
    if vc.is_text_widget(vc.w):
        vc.n = 1
        vc.accept(handler=vc.vim_d2)
    else:
        vc.quit()

This is the key handler for the ‘d’ key in normal mode.

The entry in vc.normal_dispatch_d for ‘d’ is: ‘d’:vc.vim_d.

Because this command changes text, vc.is_text_widget(vc.w) must be True. If so, this handler simply calls vc.accept(handler=vc.vim_d2) to queue up the follow-on handler. Otherwise, the handler calls vc.quit() to end the command.

vim_d2

def vim_d2(vc):
    if vc.is_text_widget(vc.w):
        if vc.stroke == 'd':
            w = vc.w
            i = w.getInsertPoint()
            for z in range(vc.n1*vc.n):
                # It's simplest just to get the text again.
                s = w.getAllText()
                i,j = g.getLine(s,i)
                # Special case for end of buffer only for n == 1.
                # This is exactly how vim works.
                if vc.n1*vc.n == 1 and i == j == len(s):
                    i = max(0,i-1)
                w.delete(i,j)
            vc.done()
        else:
            vc.d_stroke = vc.stroke # A scratch var.
            vc.begin_motion(vc.vim_d3)
    else:
        vc.quit()

This is the follow-on handler for the ‘d’ command. It will be called when the user types a second character following the ‘d’ command in normal mode.

All forms of the ‘d’ command alter text, so this handler calls vc.quit if vc.w is not a text widget.

If the second character is another ‘d’, we have the ‘dd’ command. The code uses the high-level interface to delete a line, then calls vc.done() to end the command.

If the second character is not a ‘d’, it should be a following motion, such as “2j” in “d2j”.

vc.vim_d2 remembers the character that started the motion in a scratch ivar, vc.d_stroke. Such ivars are not inited or touched outside of vim_d and its follow-on key handlers. This code must remember this character so that the vim_d3 handler will know whether to expand the deleted text to a line.

Finally, vc.vim_d2 calls vc.begin_motion, which does the following:

  • Calls vc.ignore if the second character doesn’t really start a motion.

  • Sets vc.handler to vc.do_inner_motion. This handles the motion.

  • Sets the vc.after_motion to the next follow-on handler: vc.vim_d3. vc.vim_d3 will be called when the motion is complete. The details are complicated, but happily the key handlers don’t have to know about them!

vim_d3

def vim_d3(vc):
    '''Complete the d command after the cursor has moved.'''
    # d2w doesn't extend to line.  d2j does.
    trace = False and not g.unitTesting
    if vc.is_text_widget(vc.w):
        extend_to_line = vc.d_stroke in ('jk')
        w = vc.w
        s = w.getAllText()
        i1,i2 = vc.motion_i,w.getInsertPoint()
        if i1 == i2:
            if trace: g.trace('no change')
        elif i1 < i2:
            for z in range(vc.n1*vc.n):
                if extend_to_line:
                    i2 = vc.to_eol(s,i2)
                    if i2 < len(s) and s[i2] == '\n':
                        i2 += 1
                    if trace: g.trace('extend i2 to eol',i1,i2)
            w.delete(i1,i2)
        else: # i1 > i2
            i1,i2 = i2,i1
            for z in range(vc.n1*vc.n):
                if extend_to_line:
                    i1 = vc.to_bol(s,i1)
                    if trace: g.trace('extend i1 to bol',i1,i2)
            w.delete(i1,i2)
        vc.done()
    else:
        vc.quit()

This is the second and last follow-on handler for the d command. The dispatcher that handles vim motions will call this handler after the motions have actually happened.

First, the code double-checks that we are still in a text widget, calling vc.quit() if not.

Next, the code compares the present insertion point, w,getInsertPoint(), with the insertion point before the motion happened, vc.motion_i. It extends the selection range if the scratch ivar, vc.d_stroke, is in (‘jk’). The code then deletes the selected text.

Finally, this method calls vc.done().

vis_d

def vis_d(vc):
    '''Delete the highlighted text and terminate visual mode.'''
    w  = vc.vis_mode_w
    if vc.is_text_widget(w):
        i1 = vc.vis_mode_i
        i2 = w.getInsertPoint()
        w.delete(i1,i2)
        vc.state = 'normal'
        vc.done()
    else:
        vc.quit()

This is the key handler for the ‘d’ key in normal mode.

It is not a follow-on method of vim_d. The dispatcher calls this method after visual mode has highlighted text. Here is the entry for ‘d’ in vc.visual_dispatch_d: ‘d’:vc.vis_d.

Visual mode has already highlighted the text to be deleted, so this code simply deletes the highlighted text and calls vc.done().

Code level details

The VimCommands class in leoVim.py implements Leo’s vim mode. Vim mode is active only if @bool vim-mode = True.

The following sections will be of interest only to those seeking a deep knowledge of how vim mode’s dispatchers work. Such knowledge should rarely be required because dispatchers and key handlers are completely unaware of each other. Dispatch dicts and acceptance methods shield dispatchers and key handlers of all knowledge of each other. In particular, acceptance methods handle the sometimes tricky details of ending a key handler.

Leo’s vim code is spectacularly different from the real vim’s code. Wherever possible, Leo uses methods to hide implementation details.

Ironically, now that everything is hard coded in tables, it would be easy for plugins to customize the workings of vim-mode.

Initialization

The init code for each Leo commander c assigns an instance of VimCommands to c.vimCommands. This is done regardless of the @bool vim-mode setting.

Each ivar of the VimCommands class is inited by exactly one of the following:

vc.init_constant_ivars()
vc.init_dot_ivars()
vc.init_persistent_ivars()
vc.init_state_ivars()
vc.create_dispatch_dicts()

In effect, this code partitions each ivar into disjoint sets. This partitioning simplifies code that must re-init some ivars but not others.

The init code creates dispatch dicts used by dispatchers.

Dispatchers

Depending on various state date, dispatchers route incoming keys to the proper key handler. Dispatchers use dispatch dicts to assign handlers to incoming keys. These dicts eliminate almost all special case code.

vc.do_key is the top-level dispatcher. k.masterKeyHandler calls it for all keys except Ctrl-G. Note: k.masterKeyHandler calls vc.do_key only when there no key state in effect, that is, when the minibuffer is not active.

As discussed below, the value returned by vc.do_key tells k.masterKeyHandler whether vim mode has completely handled the key.

Depending on the vc.handler ivar, vc.do_key can route the incoming key either to an inner dispatcher or directly to a key handler.

Inner dispatchers handle keys for a particular vim mode using dispatch dicts. Inner dispatchers the following ivars behind the scenes:

vc.handler, vc.next_func, vc.return_value
vc.in_motion and vc.motion_func

Handling these ivars can be tricky; hiding the details greatly simplifies all key handlers.

About key handlers

Key handlers handle a single key during the parsing of a vim command. Key handlers can either complete a command, thereby actually doing something, or change state so as to be able to parse (and possibly complete) the next incoming keystroke.

For example, the key handler for the G command handles the command completely. In contrast, two key handlers are needed to handle the gg command. The first handler, vc.vim_g, simply calls vc.accept(handler=vc.vim_g2). This call changes the vc.handler ivar to point to the follow-on handler, vim_g2. vim_g2 handles all commands after the user has typed ‘g’ in normal mode.

Each key handler must end with a call to an acceptance method. vc.accept is one such method. Acceptance methods prepare for the next keystroke by setting internal state ivars used by the various dispatchers.

Many key handlers simply call vc.done(). This method handles all the details of completing a key handler: it hides the details of parsing vim command.

Important: Any key handler that wants to change vc.state should set vc.state before calling vc.done()

Key handlers can call either direct acceptance methods, vc.accept, vc.delegate, vc.done, vc.ignore, vc.not_ready, vc.quit, and vc.reset, or indirect acceptance methods: vc.begin_insert_mode, vc.begin_motion, vc.end_insert_mode, and vc.vim_digits. Indirect acceptance methods must eventually call direct acceptance methods.

Ivars for key handlers

Dispatchers set the following ivars for each key handler:

vc.w is the widget that has focus. Key handlers may use convenience methods to determine the location and type of vc.w. The most important are:

  • vc.is_text_widget(w): True if w is any text widget, including headlines, body text and log pane.

  • vc.in_headline(w): True if w is a headline widget in edit mode.

vc.stroke is a standard Leo stroke representing the incoming key. Note that the spelling of the stoke using the Tk spellings. Take a look at entries in the dispatch dicts to see such spellings. When in doubt, enable the trace in vc.do_key to see the incoming strokes.

vc.n1 and vc.n are the repeat counts in effect for each key handler. Dispatchers and their allies handle most details of setting these repeat counts, so most key handlers can simply use vc.n1*vc.n as the ultimate repeat count.

vc.motion_i is the insertion point before the motion has taken place.

Handling tabs

Various vim commands advertise, just by having a tab_callback method, that they want to handle a tab that follows their name. ga.do_tab then defers to the vim command. Vim’s tab handler no longer knows anything about colon commands, or what any command intends to do with the tab. If the command handler has a tab_callback attribute, vim’s tab handler just calls it.

Here is the flattened form of the class that handles the :tabnew command. Note that the __call__ and tab_callback methods are trivial:

class Tabnew:
    '''
    A class to handle Vim's :tabnew command.
    This class supports the do_tab callback.
    '''
    def __init__(self,vc):
        '''Ctor for VimCommands.tabnew class.'''
        self.vc = vc
    __name__ = ':tabnew'
        # Required.

    def __call__(self,event=None):
        '''Prompt for a file name, the open a new Leo tab.'''
        self.vc.c.k.getFileName(event,callback=self.open_file_by_name)

    def tab_callback(self):
        '''Called when the user types :tabnew<tab>'''
        self.vc.c.k.getFileName(event=None,callback=self.open_file_by_name)

    def open_file_by_name(self,fn):
        c = self.vc.c
        if fn and not g.os_path_isdir(fn):
            c2 = g.openWithFileName(fn,old_c=c)
            try:
                g.app.gui.runAtIdle(c2.treeWantsFocusNow)
            except Exception:
                pass
        else:
            c.new()

This pattern is particularly well suited to Leo, because the various getPublicCommands methods reference those functions in their command dictionaries. Here, the new entries are:

':r':       vc.LoadFileAtCursor(vc),
':tabnew':  vc.Tabnew(vc),

API’s for key handlers

The simplest way of moving the cursor or changing text is to use the vc.do method, a thin wrapper for c.k.simulateCommand. For example:

if vc.state == 'visual':
    vc.do('end-of-buffer-extend-selection')
else:
    vc.do('end-of-buffer')

Key handlers may also use the high-level interface. This is the API used throughout Leo’s core. For details, see the HighLevelInterface class in leoFrame.py and various subclasses in qtGui.py.

vc.return_value and internal error checking

vc.do_key returns the value of vc.return_value. Most the acceptance functions set vc.return_value to True, indicating that vim mode has completely handled the key and that k.masterKeyHandler should simply return. k.masterKeyHandler handles the key as usual if vc.do_key returns False.

Each key handler sets vc.return_value indirectly by calling an acceptance method. A simple check in vc.do_key ensures that every key handler, has, in fact, called an acceptance method. In practice, this check has been very effective.