TheClapp/ blog/ posts/ Architecture of Vim-mode global, buffer, and window variables

I thought I'd take a few minutes and touch on a piece of my Vim-mode code that I rather like, the specification of Vim variables. (This is all about Vim variables used in the code so far, not about Vim-user-level variables, which I don't even have yet, but if/when I do they may well just use this same framework.) Knowing a little bit about Vim or vi will come in handy in this discussion, but hopefully I can explain most of it.

Background: Vim-mode operations happen in (at least) three contexts: global, buffer, and window. For example, it's a global option whether you want to highlight all the matches of the last thing you searched for. (This corresponds to Vim's hlsearch option.)1 Second, Vim allows you to perform a command, and then a movement, and have the command apply to the moved-over text. To facilitate this, I store the before and after character positions in buffer-local variables -- each buffer has its very own before and after variables. Last, Vim allows you to preceed most commands with a count just by starting to type in some digits, and you can be in the middle of typing a count in one window and doing something else in another window, so I have a window-local flag that tells me whether I should consider a digit as part of a count, or part of some other command. (This is particularly important to process 0 (zero) correctly -- 0 normally moves to the beginning of the line.)

So to store and keep track of all this, I have four things:

The class looks like this, and has a name, a type, a value, an initialization function, and a doc string:

(defclass vim-var ()
  ((name :initarg :name :accessor name-of)
   (var-type :initarg :type :accessor var-type-of)
   (values :initarg :values :accessor values-of)
   (init-func :initarg :init-func :accessor init-func-of)
   (doc :initarg :doc :accessor doc-of)))

The values slot is not the value of the variable, it's actually a hash table storing the value (or values) of the variable in all the appropriate contexts, e.g. for buffer variables, a value for each buffer.

The hash table that stores all the variable records is keyed on the name of the variable and its type. I could just key on name, and look up the type from there, but Vim lets you have same-named user-level variables in different contexts, so I wanted to allow that.

Since each kind of variable (global, buffer, window) needs to know its context (for example a buffer variable needs to know the currently active buffer), first you need to define each kind of variable, and specify how to find out the context. I call the thing that defines the context the "selector", and I store the selector in a hash table keyed on the type.

So accessing a variable is a three step process: given its name and type, look up the vim-var instance that has all the values for variables of that name and type, then look up and call the selector for that type, then use the selector value to look up the actual value of the variable in the current context. Here's the code to do all that:

(defun vim-var-key (name type)
  (cons name type))
(defun vim-var-lookup (name type #| &optional create |#)
  (let* ((key (vim-var-key name type))
         (var (gethash key *vim-vars*)))
    (cond (var var)
          #+nil
          (create (setf (gethash key *vim-vars*)
                        (make-instance 'vim-var :name name :type type)))
          (t (error "vim-var-lookup: vim-var ~S of type ~S not found" name type)))))

(defun vim-var-selector (type)
  (multiple-value-bind (selector present) (gethash type *vim-var-selectors*)
    (if present
      (funcall selector)
      (error "vim-var-selector: unknown selector: ~S" type))))

(defun vim-var (name type)
  (let ((var (vim-var-lookup name type)))
    (if var
      (let ((selector (vim-var-selector type)))
        (multiple-value-bind (selected-val present)
            (gethash selector (values-of var))
          (if present
            selected-val
            (setf (gethash selector (values-of var))
                  (funcall (init-func-of var))))))
      (error "vim-var: vim-var ~S of type ~S not found" name type))))

I also have a function to set a variable, of course, and the defsetf that allows you to use setf instead:

(defun vim-var-set (name type value)
  (setf (gethash (vim-var-selector type)
                 (values-of (vim-var-lookup name type)))
        value))
(defsetf vim-var vim-var-set)

I also define a macro per context that makes it easy to define variables of that type. The definer macro creates an instance of vim-var, puts it in the hash table, and (here's the handy part) creates a symbol macro so access to the variable -- which is actually via a function call -- looks like a normal variable reference.

Last, defining all this by hand would be a drag, so I have a macro that defines a new context, which sets up the selector for the type and defines a macro to define instances of the type:

(defmacro def-vim-var-definer (definer-name type selector)
  `(progn
     (setf (gethash ,type *vim-var-selectors*) (lambda () ,selector))
     (setup-indent ',definer-name 2)
     (defmacro ,definer-name (name init-code &optional (doc "Vim variable"))
       `(progn
          (setf (gethash (vim-var-key ',name ,,type) *vim-vars*)
                (make-instance 'vim-var
                               :name ',name
                               :type ,,type
                               :values (make-hash-table)
                               :init-func (lambda () ,init-code)
                               :doc ,doc))
          (eval-when (:compile-toplevel :load-toplevel :execute)
            (define-symbol-macro ,name
              (vim-var ',name ,,type)))))))

This may be my first macro-defining-macro. :)

Here's the definition of buffer-local variables:

(def-vim-var-definer def-vim-buffer-var :buffer (current-buffer))

This means that to define a buffer-local variable, call def-vim-buffer-var, and to use a buffer-local variable, figure out what the buffer is by calling current-buffer.

Here's the definition of a buffer-local variable:

(def-vim-buffer-var b-vim-point-before-movement (copy-point (current-point))
  "The point before a movement command.")

This defines b-vim-point-before-movement as a buffer-local variable, and says that when you first access b-vim-point-before-movement in a new buffer, use (copy-point (current-point)) to initialize the value for that buffer.


note 1: I don't actually use this option yet, but I will eventually.