On this page:
1.1 A Simple Example
1.2 The ’keybinding-info Property
1.3 A More Involved Example
1.4 Looping Forms

1 Getting Started with Keybindings🔗ℹ

This language is designed to be used in conjunction with Racket’s syntax-propertys, to extend DrRacket with new keybindings. To do that, macro authors need to follow a specific syntax-property interface for macros they want to attach keybindings to.

1.1 A Simple Example🔗ℹ

What follows is a very simple keybinding which uses CTRL+a to insert the text "watermelon" into the buffer (the body of the macro is unimportant for this example), feel free to paste this into your DrRacket once you have the plugin installed and try it out:

(define-syntax (my-macro stx)
  (syntax-property #'1
                   'keybinding-info
                   (make-kb "c:a"
                            (insert "watermelon")
                            "a-very-silly-keybinding"
                            'global
                            stx)))

1.2 The ’keybinding-info Property🔗ℹ

There are five key pieces of information in the ’keybinding-info syntax property. From the previous example we have:

(vector "c:a"
        (insert "watermelon")
        "a-very-silly-keybinding"
        'global
        (make-srcloc (syntax-source stx)
                     (syntax-line stx)
                     (+ 1 (syntax-column stx))
                     (+ 1 (syntax-position stx))
                     (syntax-span stx)))

The first element of the vector is a string representing the key combination that should be mapped to your keybinding, it follows the same conventions in Racket’s keymap%.

The second element of the vector should be a program written in this keybinding language, you can see the full list of operations later in this guide.

The third element of the vector is a name for your keybinding as a string.

The fourth element of the vector represents the active range for the keybinding inside the current editor window. It can be one of: the symbol 'global, the symbol 'local, or a vector with two numbers that represent the start and end positions of the active range. If 'global is provided, the keybinding is active for the entire editor window. If 'local is provided, the keybinding is active inside the body of the macro usage.

The fifth element is a srcloc used by the drracket-custom-keybindings plugin to correctly extract ’keybinding-info properties from the expanded syntax in the editor.

1.3 A More Involved Example🔗ℹ

Maybe inserting "watermelon" into the buffer isn’t a great keybinding for your use case, and you want some more complex keybindings related to your macro. Luckily this tool supports more than just simple fruit-related string insertion, so lets consider a more interesting example. Maybe you’re writing an cond expression with two clauses, like the one below, and you want to move some things from the first clause into the second else clause. In this case we’ll say everything after the insertion point that is inside the clause should be moved, so the keybinding will work in relation to the current position (in this case imagine the cursor is preceding the s-expression "(body-2)" in the first clause). We start with something like:

(cond [(some-condition-expr)
       (body-1)
       (body-2)
       (body-3)]
      [else
       (else-body)])

and we want to end up with:

(cond [(some-condition-expr)
       (body-1)]
      [else
       (else-body)
       (body-2)
       (body-3)])

The idea of this keybinding is to basically loop over each of the expressions in the cond-clause following the current insertion point until we hit the end the of the clause. At this point we can copy the text between the point where we started and where we ended and move it to the next clause (following any sub-expressions in that clause). First off lets look at the macro we might write to attach this keybinding to:

(define-syntax (my-cond stx)
  (syntax-property (...)
                   'keybinding-info
                   (make-kb "c:b"
                            move-exprs-into-next-cond-clause
                            "my-cond-keybinding"
                            'local
                            stx)))

I’ve omitted some details including the implementation of my-cond and exactly what move-exprs-into-next-cond-clause is referring to, but lets look at the other pieces of the ’keybinding-info property. The key combination is pretty self-explanatory: it means that the keybinding is activated by CTRL+b. The name is simply "my-cond-keybinding". The activation range is the only other interesting property, as it is now 'local instead 'global, meaning this keybinding is only active inside a usage of my-cond. Although this keybinding may be useful in other contexts we are really designing it for my-cond, so it’s a good idea to make it 'local to avoid conflicts with other keybindings. Now we can look at move-exprs-into-next-cond-clause:

(define-for-syntax move-exprs-into-next-cond-clause
  (kb-let (['start-pos (get-position)])
          (seek-while (forward-sexp-exists?)
                      1
                      'sexp)
          (kb-let (['end-pos (get-position)]
                   ['text-to-move (get-text 'start-pos 'end-pos)])
                  (up-sexp)
                  (forward-sexp)
                  (down-sexp)
                  (seek-while (forward-sexp-exists?)
                              1
                              'sexp)
                  (insert-return)
                  (insert 'text-to-move)
                  (delete 'start-pos 'end-pos)
                  (set-position 'start-pos))))

This program starts off by storing its starting position so it knows where to begin copying the body of the clause. Then it uses the seek-while form to step over each subsequent s-expression until it hits the end of the clause and stops. seek-while can be thought of as a way to move over the buffer until a condition returns false or it hits one end of the buffer. The rest of program stores the end position to copy, moves to the next cond clause, and pastes the correct text into it. Once this is done, the original text is deleted and the position is reset to the start position. In the next section we’ll see how to use kb-base’s looping forms to accomplish the same thing, and then in section 2 we’ll see more examples of keybinding programs.

1.4 Looping Forms🔗ℹ

The previous example could be accomplished using the looping forms do-times and count-iters instead of seek-while (indeed seek-while is just intended to be a nice way to seek through the buffer without use these looping forms). First let’s think about the reason for these two unconventional looping constructs.

One desirable property of keybinding programs running inside your editor is that they should always terminate. For this reason the keybinding language does not support a traditional looping construct like those you might have seen in imperative languages. Instead, this language provides two forms: do-times and count-iters, which are designed to work in conjunction with one another to approximate looping an uncertain number of times.

The purpose of do-times is to perform some body expression a fixed number of times:

(test-kb-program (do-times 10 (insert "a")))

produces:

(values void 10 "aaaaaaaaaa")

While do-times doesn’t seem expressive, it is powerful enough to approximate loopping when combined with count-iters. count-iters gets a condition-expr, step-size-expr, and step-type, and uses these to do the following: take one step, increment the count, and evaluate condition-expr. If it evaluates to #t, count-iters keeps going, otherwise if it evaluates to #f, count-iters stops and returns the number of times it iterated. If at any time count-iters tries to take a step and it cannot make progress, for example when we have hit one end of the buffer, the loop terminates and the count is returned regardless of the condition.

(test-kb-program (count-iters (forward-sexp-exists?)
                              1
                              'sexp)
                 #:setup-proc (λ (editor)
                                (send editor insert "() () ()")
                                (send editor set-position 0)))

produces:

(values 3 0 "() () ()")

The idea is that count-iters cannot change the buffer at all, meaning the position and content will stay the same no matter what. Combining these two constructs, we can take in some information at runtime about the editor’s state and then use that in do-times to loop some unknown number of times. We will now look at some more complex keybindings, some of which require the use of both count-iters and do-times. As a final example for this section, consider the following rewrite of the cond example in the previous section:

(define-for-syntax move-exprs-into-next-cond-clause
  (kb-let (['start-pos (get-position)])
          (do-times (count-iters (forward-sexp-exists?)
                                 1
                                 'sexp)
                    (forward-sexp))
          (kb-let (['end-pos (get-position)]
                   ['text-to-move (get-text 'start-pos 'end-pos)])
                  (up-sexp)
                  (forward-sexp)
                  (down-sexp)
                  (do-times (count-iters (forward-sexp-exists?)
                                         1
                                         'sexp)
                            (forward-sexp))
                  (insert-return)
                  (insert 'text-to-move)
                  (delete 'start-pos 'end-pos)
                  (set-position 'start-pos))))