On this page:
2.1 Example 1:   Swapping cond Branches
2.2 Example 2:   Converting Between Contract Forms
2.3 Example 3:   Pattern Matching Code Generation

2 Some Example Keybindings🔗ℹ

Often it is easier to get an understanding of how something works by looking at examples, so in this section we will look at a few example keybindings made for existing forms in Racket.

2.1 Example 1: Swapping cond Branches🔗ℹ

First lets look at an example my-cond macro which we’ll pretend behaves like cond. Notice how we can attach a keybinding using the ’keybinding-info property. The program swap-cond-branches will be defined later.

(define-syntax (my-cond stx)
  (syntax-property #'(cond ...)
                   'keybinding-info
                   (make-kb "c:space"
                            swap-cond-branches
                            "swap-cond-branches"
                            'local
                            stx)))

This simple keybinding takes into account the cursor position inside a cond, and swaps the first branch following the cursor with the one following it. For example, if we have this before the the keybinding:

(my-cond [(some-predicate? x) #t]
         [(some-other-predicate? x y) #f])

with the current cursor position preceding the first branch, then after the keybinding we’d have:

(my-cond [(some-other-predicate? x y) #f]
         [(some-predicate? x) #t])

Let’s look at the full code for this keybinding program:

(define swap-cond-branches
  (seq (down-sexp)
       (kb-let (['first-sexp-start (get-position)]
                ['first-sexp-end (seq (up-sexp)
                                      (forward-sexp)
                                      (sub (get-position) 1))]
                ['second-sexp-start (seq (down-sexp)
                                         (get-position))]
                ['second-sexp-end (seq (up-sexp)
                                       (forward-sexp)
                                       (sub (get-position) 1))]
                ['first-sexp-content (get-text 'first-sexp-start 'first-sexp-end)]
                ['second-sexp-content (get-text 'second-sexp-start 'second-sexp-end)])
               (set-position 'second-sexp-start)
               (delete 'second-sexp-start 'second-sexp-end)
               (insert 'first-sexp-content)
               (set-position 'first-sexp-start)
               (delete 'first-sexp-start 'first-sexp-end)
               (insert 'second-sexp-content)
               (set-position 'first-sexp-start)
               (up-sexp))))

The high level idea of this keybinding program is to start right before the opening parenthesis of the first branch you want to move, then from there we copy the content from that branch and the one following it, then we just insert the content of the second branch over the first and the content of the first over the second, effectively swapping the two branches.

The various bindings inside the kb-let expression take care of getting the content, which gets bound to 'first-sexp-content and 'second-sexp-content. Once we have these, the body of the let just takes care of moving to the correct positions, deleting the original content and replacing it with the content we’re swapping in.

More generally, you can read a keybinding program as sort of a sequence of steps you might take when making this code transformation manually, or put another way you can walk through each instruction and how it will affect the buffer as you read the program. As you can see everything is quite low-level, so the level of reasoning is in terms of very discrete small operations on the buffer like "insert" and "delete".

2.2 Example 2: Converting Between Contract Forms🔗ℹ

Once again we’ll look at the macro definition for our own version of -> called my-> with a keybinding attached. The actual keybinding program is explained later in this example.

(define-syntax (my-> stx)
  (syntax-property #'(-> ...)
                   'keybinding-info
                   (make-kb "c:space"
                            arrow-to-arrow-star
                            "arrow-to-arrow-star-kb"
                            'local
                            stx)))

Another code transformation one might like to make is updating the contract on a function they’ve written. Maybe initially the function took no optional arguments and now it needs a different contract form to represent the optional argument’s contract. Remembering the syntax for the various contract combinators provided by Racket can be difficult, so the keybindings we see in this section take one contract combinator and convert it to the other form automatically. Imagine, for example, you have the following function:

(define/contract (my-func a b)
  (my-> number? string? boolean?)
  ...)

Converting this to the ->* syntax can be done easily with a keybinding, resulting in:

(define/contract (my-func a b)
  (->* (number? string?)
       ()
       boolean?)
  ...)

Let’s look at the full code for this keybinding program:

(define arrow-to-arrow-star
  (seq (delete 4)
       (insert "->* ")
       (kb-let (['num-of-args (sub (count-iters (forward-sexp-exists?) 1 'sexp) 1)])
               (insert "(")
               (delete 1)
               (do-times 'num-of-args (move-position 1 'sexp))
               (insert ")")
               (insert-return)
               (insert "()")
               (insert-return))))

This keybinding should be invoked with the current position immediately preceding my->. Using that position, we simply delete the name and insert the new one with a star. This is the call to (delete 4).

The next task is to use count-iters to compute how many arguments the function takes, which we can do by iterating one s-expression at a time and subtracting the total by 1 (the return value contract). Once we have this we use a do-times to wrap the arguments in one s-expression, insert an empty () for the optional paramters we’ll expect the user to add, and end up with these pieces of the ->* contract on their own line, where the last thing is the return type. We could easily decide to insert less newlines or customize exactly how we want the ->* expression to look once it has been transformed.

2.3 Example 3: Pattern Matching Code Generation🔗ℹ

This last example is for a macro like |#lang| plai’s define-type, here is the macro with a keybinding attached:

(define-syntax (my-define-type stx)
  (syntax-parse stx
    [(_ type-name
        [var-name (field-name field-c) field-clause ...]
        ...)
     (define first-var-pos (- (syntax-position (third (syntax->list stx))) 1))
     (syntax-property
      #'(define-type type-name [var-name (field-name field-c) field-clause ...] ...)
      'keybinding-info
      (make-kb "c:space"
               (gen-type-case (symbol->string (syntax-e #'type-name))
                              first-var-pos)
               "generate-type-case"
               'global
               stx))]))

This macro’s definition warrants a little more explanation. We’ll see a bit later on, but we need to pass to this keybinding program two things: the position of the first variant’s s-expression, and the type name for the define-type. We can get this information from the macro usage and it’s passed to the call to gen-type-case.

Now lets look at what this keybinding should do. Given the following defined in our buffer:

(define-type Animal
  [dog (bark-volume number?)]
  [cat (favorite-treat string?)
   (coat-length string?)]
  [lizard (eats-bugs boolean?)])

We want to generate a type-case that looks like this:

(type-case Animal ...
  [dog (bark-volume) ...]
  [cat (favorite-treat coat-length) ...]
  [lizard (eats-bugs) ...])

The idea of this keybinding is that it will use some information from the define-type macro to get the name of the type and the position where the first variant’s sexpr starts. It will then use this information to construct a type case from the definition. The full code is as follows, and we can walk through it in detail:

(define (gen-type-case type-name var-pos)
  (seq (set-position (last-position))
       (insert-return)
       (insert-return)
       (insert "(type-case ")
       (insert type-name)
       (insert " ...")
       (insert-return)
       (kb-let
        (['insert-posn (get-position)])
        (set-position var-pos)
        (do-times (count-iters (forward-sexp-exists?)
                               1
                               'sexp)
                  (down-sexp)
                  (kb-let
                   (['current-variant-name-posn (get-position)])
                   (set-position 'insert-posn)
                   (insert "[")
                   (insert (get-forward-word 'current-variant-name-posn))
                   (insert " (")
                   (kb-set! 'insert-posn (get-position))
                   (set-position 'current-variant-name-posn)
                   (forward-sexp)
                   (do-times (count-iters (forward-sexp-exists?)
                                          1
                                          'sexp)
                             (down-sexp)
                             (kb-let
                              (['current-field-name-posn (get-position)])
                              (set-position 'insert-posn)
                              (insert (get-forward-word 'current-field-name-posn))
                              (insert " ")
                              (kb-set! 'insert-posn (get-position))
                              (set-position 'current-field-name-posn)
                              (up-sexp)
                              (forward-sexp)))
                   (set-position 'insert-posn)
                   (delete (sub (get-position) 1) (get-position))
                   (insert ") ...]")
                   (insert-return)
                   (kb-set! 'insert-posn (get-position))
                   (set-position 'current-variant-name-posn)
                   (up-sexp)
                   (forward-sexp)))
        (set-position 'insert-posn)
        (seek-while (kb-not (kb-equal? (get-character) #\])) -1 'simple)
        (move-position 1 'simple)
        (insert ")"))))

This is definitely the most complex keybinding program we’ve looked at, and rather than walking through it line by line we can see it as a few high level tasks. The first thing to notice is we take in type-name and var-pos, which represent the name of the type we’d like to generate the type-case for and the starting position of the s-expression containing the first variant in the type’s definition. This is all information we can get from the macro we’re attaching this keybinding program to.

The first part of the program sets up the start of the call to type-case, by inserting "(type-case <type-name> ..." where the "..." is left for the user to fill in with an expression producing the correct type. The program then sets up a variable 'insert-posn, which will be sort of a running track of where to insert text throughout the course of the program (in this case we start generating the type-case at the end of the buffer and we just update it each time we do an insert).